最近一直在捣鼓 Agent Skills发现网上的讨论大多数停在“什么是技能”这种概念层真正能把一个自定义 Skill 从设计到落地讲清楚的太少。这玩意儿说白了就是给 Agent 配上一套“可复用、可组合、可测试”的任务级能力包让模型不只靠临场发挥而是像老员工一样按 SOP 干活。这篇文章我想把它掰开揉碎讲一遍先讲清楚 Agent Skills 到底解决什么问题和插件、工具调用有什么区别再从我实际做的一个“销售周报结构化抽取”Skill 出发把整套设计、编写、测试、排错流程完整过一遍。适合正在做 Agent 应用、或者打算给自己的 Bot 加技能能力的开发者参考新手也能跟得上。1. 先搞清楚 Agent Skills 到底是什么1.1 模型再聪明也得有“手”和“操作规程”大模型本身是个“超级大脑”但它有一个非常尴尬的短板只会生成文本不会真正去执行。你说“帮我统计当前目录下所有 Python 文件的行数”模型如果硬答只能给你一个大概的估算甚至胡编一个数字。这时候就得给它工具让它去跑 shell、读文件、算结果。工具解决了“手”的问题但光有手还不够。你让一个聪明但没经验的新人去做数据统计他知道能用终端但不知道先看哪些目录、过滤哪些文件、遇到权限报错该怎么处理。Agent Skills 解决的就是这个“操作规程”问题把一类任务的标准做法、边界条件、异常处理都固化下来模型拿到 Skill 之后只需要照着流程走不需要每次重新推理一遍该怎么做。我之前做过一个实验同一个 Agent不挂 Skill 的时候让它从一段混乱的周报里提取关键指标它一会儿输出 Markdown 表格一会儿输出 JSON键名还不固定。挂上 Skill 之后它稳定输出同一个 JSON 结构缺字段还会主动说明原因。差别就来自 Skill 对输出契约的约束。1.2 它和普通工具、插件、Function Call 有什么不一样很多人会把 Agent Skills 和 Plugin、Function Calling、Tool 这些概念混在一起它们的边界确实有点模糊但内核不一样。概念粒度典型内容解决问题Function Call / Tool单次函数调用函数名、参数、返回结构让模型能调用某个具体能力Plugin外部服务扩展API 端点、鉴权、请求格式接入某个第三方服务Agent Skill任务级能力封装使用说明、参数契约、执行步骤、示例、错误恢复、后处理脚本让模型稳定完成一整类任务打个比方Function Call 是“给你一把扳手”Plugin 是“给你一个接好的水管龙头”而 Agent Skill 是“给你一份完整的维修手册工具箱验收标准”。Skill 内部往往也要调用函数、脚本、外部 API但它的核心价值在于把“什么时候用、怎么用、用错了怎么办”这些都写清楚了。所以不要试图用一把扳手去要求模型完成一次复杂维修那是没用的。1.3 Skill 的四个核心组成一个标准的 Agent Skill 一般包含四层说明文件、参数契约、执行脚本、测试用例。说明文件是门面用来告诉模型这个技能是干嘛的、在什么场景下触发、具体分哪几步做。参数契约定义了 Skill 的输入和输出格式相当于两个人之间的“接口协议”。执行脚本是真正干活的代码可以是 Python、Shell 或者一组命令模型按说明调用它。测试用例则是用来保证质量的手段因为模型输出有随机性没有测试的 Skill 改一次坏一次非常痛苦。这四层缺一不可。我见过有人只写一段 Markdown 就算一个 Skill结果模型在复杂场景下根本不会用因为缺少触发条件和参数定义。也有人写了脚本却不写说明模型甚至不知道什么时候该调这个脚本。所以完整结构很重要。1.4 一个 Skill 从请求到生效的完整调用链路理解调用链路对排错特别重要。通常流程是这样的用户输入一句请求Agent 框架先根据所有 Skill 的描述信息做语义匹配选出可能相关的候选技能。然后模型加载该 Skill 的说明文件判断当前用户请求是否真的符合“使用条件”。如果符合就按照说明去执行过程中可能调用脚本或工具。最后模型把执行结果整理成用户能看懂的回答返回出去。这里最容易被忽视的是“语义匹配”这一步。技能描述写得不行Agent 根本不会把它选出来后面设计得再好也白搭。所以我习惯把 Skill 的 description 当成一个“门面招牌”前两句话必须说清楚“这个技能专门处理什么类型的输入、输出什么结果。”不要写“这是一个强大的工具”这种废话模型看到这种描述会直接跳过。2. 动手写 Skill 之前先把这几件事想清楚2.1 契约先于实现写 Skill 第一步不是写代码而是把输入输出的“契约”定下来。所谓契约就是明确这个技能接收什么参数、每个参数的类型和含义是什么、最终输出长什么样。没有契约的 Skill 就像没有菜单的餐厅厨师全凭感觉做菜客人吃到什么全看运气。以销售周报抽取为例我的输入契约是一个必填参数raw_text类型为 string代表非结构化的周报正文一个可选参数week类型为 string代表报告所属周用于填充输出。输出契约是一份固定结构的 JSON包含region、revenue、user_count、week_over_week、risks这些字段。这样定清楚之后模型抽取时就知道该往哪个字段填东西而不是自由发挥。定契约要注意颗粒度太粗了模型不知道你要什么太细了会限制表达。我的经验是输出字段控制在 5 到 8 个之间最合适既覆盖业务需求又不至于让模型记不住。字段名用英文小写下划线避免中文键名在不同编码环境下出问题。2.2 为 Skill 划清“该干什么、不该干什么”Agent Skills 比传统程序更需要“负向约束”。普通代码写了 if else 就能控制逻辑但模型不是通过代码路径执行的它是靠自然语言理解来触发的。你如果不明确告诉它“什么时候不要用”它很容易在错误场景下强行调用。比如我这个周报抽取 Skill我会在说明里明确写如果输入是图片格式或者是一份完整的多页 PDF 报表不要使用本技能应该提示用户先转成文本。如果用户问的是“下周预测”而不是“从本周周报里提取指标”也不要调用这个技能因为它的能力边界是抽取现状不是做预测。把边界写清楚之后模型在判断是否使用 Skill 时就有了依据。你会发现误用率下降得非常明显。2.3 技能也要算 Token 账每个 Skill 的说明文本最终都会进入上下文窗口。如果写得又长又啰嗦一个 5000 字的 Skill 加载进来对话还没开始 token 就吃掉一大截。更要命的是模型在长文本里寻找关键信息反而更容易忽略重点。所以我建议遵循“摘要在前、详情按需读取”的原则。主描述文件里只放最核心的触发条件、参数表、输出格式和 3 个以内步骤。那些详细的正则规则、命令示例、异常处理模板全部放到单独的 reference 文档或脚本注释里。Agent 在执行到需要的时候再主动读取详情。这样既省 token也降低了干扰。我踩过的一个坑是一开始为了“让模型理解得更细”把所有规则都写进主文件结果模型被大量细节淹没连基本触发条件都判断错了。精简之后准确率反而提高了。这个反直觉的现象已经出现过很多次所以要克制。2.4 可测试性没有测试用例的 Skill 都是废的模型的不确定性决定了 Skill 必须可测试。你不能说“我试了一次能跑通”就结束了因为换一段输入、换一次采样结果可能完全不一样。任何 Skill 都应该配套至少 5 组测试输入每一组包含输入文本、期望输出、通过条件。这样做的价值有二一是改 Skill 时能做回归确认改动没有破坏旧场景二是多个 Skill 共存时可以对比评估看哪个技能更值得保留。我习惯把测试用例放在tests/目录下格式用 JSONL一条一行方便自动化跑。后面我会在实战章节给出具体例子。3. 实战从零自定义一个“销售周报结构化抽取”Skill3.1 先定义业务场景和能力边界这次我要做的 Skill 是“从非结构化销售周报文本中提取结构化指标”。业务背景是某公司每周都有销售团队在文档里写周报格式千奇百怪有人写“华东这周做了 120 万环比涨了 5%”有人写“本周用户数 8000基本持平”还有人夹杂各种废信息。如果让运营人员手动整理每周都要浪费一两个小时。这个 Skill 的输入是一段自由文本输出是固定 JSON。能力边界是只做信息抽取和规范化不负责计算业务指标。比如文本里没写环比那我就在字段里填null不能自己瞎填一个推算值。只有当文本里明确出现“环比增长/下降 X%”时才把数值提取到week_over_week字段。这样设定是为了避免模型“脑补”保证数据可信。我还定义了不能处理的场景输入如果是语音转写带大量噪音的文本或者超过 2000 字的报告全文直接提示“需要先精简或分段”。这不是偷懒而是在给模型减负。任务范围越小稳定性越高。3.2 搭出技能目录结构一个清晰的项目目录能让 Skill 的维护成本大幅降低。我的目录长这样sales_report_extractor/ ├── SKILL.md ├── schema.json ├── scripts/ │ └── validate_output.py ├── examples/ │ ├── good_input.txt │ ├── good_output.json │ └── edge_case.txt └── tests/ └── test_cases.jsonlSKILL.md是主说明文件schema.json定义输入输出参数结构scripts/里放后处理脚本examples/存典型输入输出样例tests/存自动化测试用例。这套结构不是某家平台的标准而是我自己的实践总结用起来顺手最关键。有人会问目录结构重不重要很重要。因为很多 Agent 框架是按目录加载 Skill 的文件名和路径会直接影响是否能被识别。哪怕你的框架不需要这么复杂这种清晰的组织方式也方便你日后升级成多个技能。3.3 编写 SKILL.md 主文件SKILL.md 是整个技能的灵魂。我给大家一个可以直接参考的模板--- name: sales_report_extractor description: 当用户需要从非结构化的销售周报文本中提取指标并转换为 JSON 时使用此技能。适用于包含地区、收入、用户数、环比变化、风险描述等信息的周报整理场景。 --- # 销售周报结构化抽取 ## When to Use - 用户提供一段或多段销售周报、业绩回顾文字希望整理成结构化数据。 - 用户要求提取地区、收入、环比变化、用户规模等信息。 ## When NOT to Use - 输入是图片或 PDF 扫描件需要先转成文本。 - 用户的问题是预测未来业绩而非抽取现状。 - 输入文本中完全没有销售相关字段只是普通聊天。 ## Input - raw_text: string, 必填周报正文。 - week: string, 可选报告所属周如 2025-W12。 ## Steps 1. 阅读 raw_text识别所有销售相关实体。 2. 按 schema.json 中的字段名逐一填充缺失字段填 null。 3. 如果文本明确出现“环比增长/下降 X%”将 X 填入 week_over_week否则填 null。 4. 将结果交给 validate_output.py 做格式校验不合规则修正后返回。 ## Output Format 输出必须是合法 JSON字段如下 { region: 华东, revenue: 1200000, user_count: 8000, week_over_week: 5.0, risks: [华东区渠道库存偏高] }这个模板的关键是“Steps 足够轻”。我不建议写 20 个步骤模型根本执行不过来。4 到 6 个步骤最合适每一步都是不可省略的关键路径。相反在参数和边界部分我会写得更细因为那才是真正约束模型行为的地方。3.4 定义参数 Schema有了描述还要有机器可读的参数定义。我用 JSON Schema因为它是跨平台通用的标准。下面是一个简化版{ name: sales_report_extractor, description: 抽取销售周报关键指标并输出为固定结构 JSON, parameters: { type: object, properties: { raw_text: { type: string, description: 非结构化的销售周报正文必填 }, week: { type: string, description: 报告所属周格式如 2025-W12可选 } }, required: [raw_text] }, output: { type: object, properties: { region: { type: string }, revenue: { type: number }, user_count: { type: integer }, week_over_week: { type: [number, null] }, risks: { type: array, items: { type: string } } }, required: [region, revenue, user_count, week_over_week, risks] } }这里有一个容易踩的坑week_over_week这个字段业务上完全可能缺失所以类型必须允许null。如果 schema 里硬规定必须为 number模型在缺失时就会强行编一个 0 出来这对数据质量是致命的。写 Schema 时一定要想清楚“允许缺失”和“必填”的边界。必填字段越少越好必要字段才设 required。3.5 写一个后处理校验脚本模型输出 JSON 再准也难免有类型错误所以我写了一个确定性脚本来兜底。它的职责很简单读取模型输出解析 JSON检查必填字段是否存在、类型是否正确、数值是否在合理范围内。如果发现不对它不会静默放行而是返回错误信息给模型一次修正机会。import json import sys def validate_output(raw: str) - dict: data json.loads(raw) required [region, revenue, user_count, week_over_week, risks] for field in required: if field not in data: raise ValueError(fmissing field: {field}) if not isinstance(data[revenue], (int, float)): raise ValueError(revenue must be numeric) if data[week_over_week] is not None: if abs(data[week_over_week]) 100: raise ValueError(week_over_week out of range) if not isinstance(data[risks], list): raise ValueError(risks must be list) return data if __name__ __main__: results json.loads(sys.stdin.read()) for r in results: validate_output(json.dumps(r)) print(all ok)为什么要这一步因为语言模型本身就是概率输出再聪明的模型也会偶尔把数字写成字符串、把数组写成 null。有了校验脚本类似错误可以被立刻捕获并触发重试比靠提示词约束可靠得多。我在实践中加校验脚本后 Skill 的失败率降低了一大半强烈推荐每人都做这个步骤。3.6 设计测试用例并跑通测试用例是保证 Skill 改不坏的关键。我在test_cases.jsonl里放几类典型输入每行是一个 JSON 对象原始文本、期望输出、通过条件。比如下面这几个用例场景输入摘要期望关键点标准周报“华东地区本周收入120万环比增长5%用户数8000人…”revenue1200000, week_over_week5.0, risks 包含文本提到的风险缺少环比“华北本周营收80万用户数5000暂无其他问题”week_over_weeknull数字格式混乱“华南收入 1.2M用户数 1 万左右”revenue1200000, user_count10000异常低值“西南收入0元用户数-10”校验失败并触发修正最终不应输出负数用户数跑测试的方法很简单把每个输入交给 Agent 执行 Skill然后用validate_output.py校验输出是否通过。通过率越高说明技能越稳定。每次改动之后全量回归一次就能避免“修好一个 bug 弄坏另一个场景”的问题。4. 常见问题与排查技巧实录4.1 模型死活不肯调用 Skill这是最让人抓狂的问题。你明明定义好了 Skill但用户提问时模型就当它不存在自己胡诌答案。我排查这类问题时第一件事就是看 Skill 的description有没有写清楚触发场景。很多人只写“这个技能可以抽取周报”模型根本不知道“什么时候应该用”。正确写法是把触发条件也放进去“当用户要求从销售周报、业绩回顾、日报等文本中提取结构化指标时使用此技能。”还有一个原因是参数描述太模糊。如果raw_text的描述只写“文本”模型很容易把它理解成闲聊内容。我建议写成“必填参数包含销售数据的非结构化周报正文通常由用户直接粘贴”。描述越具体模型越容易判断。最后可以做一个“最小复现测试”只留这一个 Skill用一个典型用户输入问模型看它会输出什么。如果还是不用就说明 Skill 描述和输入之间没有建立语义关联需要重新措辞。4.2 调用了但输出格式一会儿一个样模型调用 Skill 之后输出的字段名、顺序、类型经常不稳定。我见过同一个输入跑十次三次输出revenue: 1200000两次输出revenue: 120万还有一次干脆把字段名改成sales。这种问题靠提示词约束上限有限最可靠的办法就是后端校验加重试。具体流程是模型第一次输出交给validate_output.py如果校验抛出异常就把异常信息返回给模型要求它根据报错重新生成。最多重试两到三次成功率能接近百分之百。重试时要把错误信息原样给模型看比如“字段 revenue 期望是数值实际收到字符串 120万”模型通常一眼就能明白该怎么改。这里要注意不要让模型在没有新信息的情况下反复重试那样只会浪费 token还可能是同样的错误。必须提供具体的失败原因重试才有意义。4.3 Skill 之间互相抢活当技能库越来越大你会发现两个 Skill 的描述可能都匹配同一个用户输入。比如我同时有“销售周报抽取”和“通用文本信息提取”两个技能用户说“帮我把周报里所有数字提取出来”两个技能听起来都能做。这种冲突的根源是边界没划清。解决思路有三个一是在描述里写明排除条件例如“如果用户要求的是一般性的信息提取请优先使用通用提取技能”二是给每个 Skill 增加优先级字段让框架在冲突时按顺序选择三是在设计阶段尽量让技能之间的能力范围不重叠做不到就去拆场景。我个人的习惯是宁可多拆几个更垂直的 Skill也不要一个 Skill 覆盖所有场景。技能越垂直描述越好写模型的调用准确率越高。4.4 调试 Skill 时的三个实用技巧第一个技巧是打开框架的“调用日志”或“轨迹面板”看模型到底有没有选择这个 Skill、加载了哪些内容。如果没有调用就说明是匹配阶段出了问题重点改 description如果调用了但输出不对那问题就在执行阶段重点看 Steps 和 Schema。第二个技巧是准备一个最小复现集每次只改一个变量。比如我先固定 Skill 描述不变单独改步骤数量跑 20 次看效果再固定步骤不变改参数 Schema。不要一次改好几处否则出了问题根本不知道是哪一步引入的。第三个技巧是保留输入输出快照。每次试跑都记录用户输入、Skill 版本、模型输出、校验结果。这样当效果波动时你能拿着快照去复盘而不是凭记忆猜测哪里变了。4.5 问题排查速查表现象可能原因排查思路解决方案Skill 不被触发description 触发条件不明确检查描述是否包含“当用户…时使用”重写 description加入具体触发场景输出字段不稳定Schema 约束不足检查输出结构定义是否完整增加 schema.json并用脚本强制校验数值类型错误模型输出字符串查看原始输出和校验报错后处理脚本对类型做强制转换必要时重试两个技能争抢能力范围重叠查看各自 when NOT to use在描述中增加排除条件设置优先级Token 消耗过高Skill 说明太冗余统计每次加载的 token 数精简主文件将详情移入 reference5. 从单技能到技能库治理5.1 用 Git 给 Skill 做版本管理Skill 也是一种代码资产而且因为模型的随机性它的迭代复杂度比普通函数更高所以版本管理格外重要。我强烈建议把每个 Skill 当成独立仓库或目录管理用 Git 做版本控制。每次改动都记录变更内容并在 SKILL.md 的头部维护一个version字段和changelog列表。版本更新的节奏也要克制。不要今天改一点、明天改一点然后发现效果变差了却不知道是哪次改动导致的。比较好的做法是每次只围绕一个目标改动改完跑一遍全部测试用例确认通过后打一个 tag。比如从v1.0.0升到v1.1.0备注写“增加了对‘万’单位数字的解析”。有了版本历史你在 Agent 效果波动时才能回退到之前表现好的版本。这一点在多人协作或长期项目中尤其关键。5.2 安全红线自定义 Skill 不要裸奔自定义 Skill 往往要执行脚本、读写文件、调用外部接口自由度越高风险也越大。如果 Skill 不小心把用户输入的文本直接拼进 shell 命令再传给一个没有鉴权的端点那就可能成为安全的缺口。我列几条安全底线脚本内部不把任何未经过滤的用户输入直接拼接进命令行需要执行系统命令时使用subprocess数组参数形式不用shellTrue运行环境走容器或沙箱赋予最小权限涉及外部 API 的密钥统一走环境变量或密钥管理不写死在 Skill 文件里敏感数据输出要做脱敏不在返回结果里附带无关字段。很多 Agent 开发者是从应用层开发转过来的容易把 Skill 当成普通模块忽略运行环境隔离。我的建议是宁可多花一点时间搭沙箱也不要拿生产环境的安全性去赌。5.3 搭建一套 Evaluation 集当你有超过十个 Skill 时你需要一套统一的评测机制来回答“哪个技能更好用”和“这次改动是变好还是变差”。我的做法是维护一个eval/目录里面按 Skill 分文件夹每个文件夹放一份评测集至少包含 10 条真实业务输入和对应的期望输出。评测时跑一遍全量数据统计三个指标调用准确率该用 Skill 时是否用了、输出合法率输出是否通过校验脚本、字段准确率关键字段值是否与期望一致。这三个指标分别对应匹配、执行、语义三层质量。每次改动后重跑这套数据效果变化一目了然。这套评测集一开始会很耗精力但它是技能库变大的前提。没有它你对所有 Skill 的优化都是“靠感觉”出了问题也只能靠运气。我现在每加一个新 Skill第一件事不是写代码而是先写评测集这个习惯帮我在项目后期省了非常多的时间。我自己实际操作下来的最大体会是Agent Skills 和传统代码有着完全不同的“脾气”。你越是把所有规则写得密不透风模型反而越容易翻车你给出清晰的契约定好边界再配上一个确定性脚本兜底它反而稳得像条老狗。所以整个设计的核心其实不是“教模型做事”而是“给模型搭一个不会出错的结构”。最后再分享一个小技巧Skill 的 description 前两句话就是命根子一定要反复打磨到“一个外行看了也知道什么时候该用”的程度。把基础打牢之后你会发现 Agent 的稳定性提升不是一点点而是质的飞跃。
阅读完成 · 觉得有帮助?