做 AI Agent 开发这一年多,被问得最多的一个问题不是“Agent 怎么搭”,而是“Skill 到底怎么写才算好用”。这问题看着简单,实际坑特别多。GitHub 上 Agent 框架选型五花八门,但真正拉开体验差距的,往往不是模型本身,而是挂在 Agent 身上的那些 Skill 写得好不好。这篇我把自己的理解和踩坑经验完整梳理一遍,从概念到实操,尽量一次讲透。
这篇内容适合谁?正在做 Agent 应用、想给助手加自定义能力、或者刚接触 Skill 开发不知道从哪下手的开发者。读完你至少能明白三件事:Skill 的边界在哪、描述文件怎么写模型才不迷糊、核心代码要注意哪些坑。
1. 先搞清楚:Skill 在 Agent 体系里到底扮演什么角色
1.1 Skill 和 Agent 的关系,别搞反了
先说结论:Agent 是执行者,Skill 是能力包,两者是“调用”和“被调用”的关系,不是平级关系。
我用个比较好懂的类比。Agent 就像一个刚入职的新员工,脑子聪明(大模型),学习能力强,但没有任何岗位经验。Skill 就是给这个新员工的操作手册和工具包——告诉他“遇到什么情况,翻开哪一页,按什么步骤操作”。没有 Skill 的 Agent 也能干活,但只能干一些通用的事,比如闲聊、套模板、简单推理。一旦涉及专业场景,比如“解析这份财报并提取关键指标”“把这段音频转成带时间戳的文字稿”,没有对应 Skill 的 Agent 就会开始自由发挥,结果不可控。
这里经常有人把概念搞混,把 Skill 当成一个 mini Agent,在 Skill 内部写一堆决策逻辑。我的建议是:Skill 内部尽量少做“决策”,多做“执行”。决策是 Agent 的事,Skill 只需要把“怎么做”做到极致。你在一个 Skill 里塞的智能判断越多,模型的调用准确率就越低,出问题的时候也越难排查。
1.2 Skill、Memory、MCP 三者怎么分工
现在社区里最火的一组概念就是 agent skill memory mcp,很多人搞不清这几个东西的关系,甚至觉得它们是同类竞品。其实它们是三个完全不同的层次:
- Skill 管“怎么做”:是一段可复用的能力描述加实现代码,解决的是“特定任务怎么完成”的问题。
- Memory 管“记住什么”:是 Agent 的长期记忆和短期上下文,解决的是“这个用户上次聊到哪、偏好是什么”的问题。
- MCP 管“怎么连”:是标准化的工具接入协议,解决的是“Agent 怎么调用外部系统、数据源”的问题。
举个例子你就明白了。假设你要做一个“会议纪要助手”。Memory 负责记住每个参会人的部门、项目背景、上次会议遗留事项;MCP 负责连接日历系统、会议录制系统,把原始材料拉下来;Skill 负责把录音转文字、按议程归纳、生成待办清单。三者各管一段,缺一不可。
如果你把该放 Memory 的东西写进 Skill,比如在 Skill 描述里写“用户上次提到过……”,那就是把动态信息硬编码进了静态能力里,每次还得改文件,迟早翻车。同理,能通过 MCP 标准协议接入的外部服务,也没必要单独包一层 Skill——那等于把标准接口又私有化了一遍,维护成本翻倍。记住这个判断顺序:能靠 Memory 解决的用 Memory,能靠 MCP 连接的用 MCP,最后的专业处理逻辑才交给 Skill。
2. 好用的 Skill 是设计出来的,不是写出来的
2.1 先用一句话定义 Skill 的边界
我评估一个 Skill 好不好用,第一个标准就是:能不能用一句话说清楚它干什么。如果一句话说不清,那这个 Skill 的边界就有问题。
举个反面例子。有人写了个 Skill 叫“数据处理”,描述里写“处理各种数据文件,包括但不限于 CSV、Excel、JSON、XML、日志文件,支持清洗、转换、统计、可视化”。这种 Skill 模型根本不知道怎么用——它面对一个具体任务时,要从一大堆“可能”里猜该走哪条路径,猜错的概率极高。
正确做法是拆。清洗 CSV 是一个 Skill,Excel 转 JSON 是另一个 Skill,日志统计又是一个 Skill。每个 Skill 只干一件事,描述里把“输入是什么、输出是什么、什么情况下用”写死。模型在调用时就不需要做复杂推理,直接匹配就行。
一句话原则:一个 Skill 解决一类问题,宁可多写几个,不要堆成一个。我在实际项目里见过太多“万能 Skill”,最后全都变成了“摆设 Skill”——模型不敢调用,调用了也不知道会发生什么。
2.2 描述文件是写给模型看的,不是写给人看的
Skill 的目录里通常有个描述文件(比如 SKILL.md),这是整个 Skill 的灵魂。很多人把这个文件当成 README 来写,写一堆“本 Skill 基于 Python 3.10 开发,支持批量处理”这种开发者视角的话。但真相是:这个文件的第一读者是模型,不是人。
模型是通过你的描述来决定“什么时候调用、怎么调用”的。所以描述文件至少要写清楚这几件事:
- 触发条件:什么场景下应该使用这个 Skill,最好给两到三个具体例子。
- 禁用条件:什么场景下绝不要用。这一点很多人忽略,但恰恰是防止模型乱调用的关键。
- 输入格式:每个参数是什么含义、允许的取值、格式要求。
- 输出格式:返回什么结构,字段怎么定义。
- 注意事项:比如依赖外部 API、需要网络、耗时较长等。
我给你看一个反例。某 Skill 描述里写着“本工具用于处理日志文件,可以实现日志的统计和分析”。模型拿到这个描述,面对“帮我看看这个日志里有多少报错”这种任务,它会犹豫:是调 Skill 还是自己写正则?因为描述里没说明输入路径怎么给、输出格式是什么。模型一犹豫,就可能做出错误选择。这个问题的根源在于:你写的描述是“给自己看的项目说明”,不是“给模型的调用指南”。
2.3 输入输出设计要给模型留好“抓手”
模型调用 Skill 本质上是一个“填参—调用—拿结果”的过程。参数设计得好不好,直接影响成功率。
我的经验是三个原则。第一,参数要扁平。尽量用平铺的独立参数,不要搞深层嵌套的 JSON 对象。模型在构造参数时是逐字段生成的,层级越深,出错率越高。第二,输出要稳定。最好定义成结构化的 JSON 或 Markdown,让模型能直接消费。第三,错误要语义化。Skill 内部报错时,返回给模型的不应该是堆栈,而是一句人话加一个建议动作。
举个参数设计的例子。你做一个“网页正文提取”的 Skill,参数设计成{ "url": "...", "output_format": "markdown", "max_length": 3000 }就比设计成{ "request": { "target": { "url": "..." }, "options": { "format": "markdown", "max_length": 3000 } } }稳妥得多。后者在模型眼里就是四个字:容易出错。你每多包一层嵌套,就是在给模型制造多一分填错的可能性。
3. 实操:从零写一个可复用的 Agent Skill
3.1 目录骨架怎么搭
不同 Agent 框架对 Skill 的目录结构要求不完全一样,但核心思想是一致的:一个 Skill 必须自包含——要么不依赖外部资源,要么把依赖明确声明出来。我常用的最小骨架是这样:
meeting-minutes/ ├── SKILL.md ├── scripts/ │ ├── transcribe.py │ ├── summarize.py │ └── requirements.txt ├── assets/ │ └── prompt_templates/ └── tests/ ├── sample_input.json ├── sample_output.json └── test_skill.py解释一下每个部分的作用。SKILL.md 是给模型看的说明书,必须放在最显眼的位置。scripts 目录放实际执行的代码,requirements.txt 声明 Python 依赖。assets 目录放辅助资源,比如给模型用的提示词模板、预置数据。tests 目录放测试数据和测试脚本,这部分很多人嫌麻烦不写,但我强烈建议至少放一组样例输入输出——这不仅能让你自己调试方便,还能在接入新框架时快速验证。
我自己用下来的体会是,目录结构这关最忌讳“为了规范而规范”。如果 Skill 本身只需要一个脚本,那就只放一个脚本,不要硬拆出一堆空目录。骨架是给人看的,也是给框架识别用的,保持简洁比什么都重要。
3.2 SKILL.md 完整示例与逐段拆解
下面这个示例我实际项目里用过,你可以直接抄来改。它做的是“会议纪要生成”:
--- name: meeting-minutes description: 生成结构化会议纪要。当用户提供了会议录音文件或会议文字记录,并希望得到带决策项、待办事项的会议纪要时使用。 version: 1.0.0 ---注意 frontmatter 里的 description,我特意写了“当用户提供了……并希望得到……”这种带条件和目的的话术,而不是“本工具用于生成会议纪要”。前者给了模型清晰的触发判断依据,后者没有。这是 SKILL.md 里最容易忽视但最关键的细节。
正文部分要写清楚用法:
## 使用方法 输入以下任一形式: - 本地录音文件路径,例如 /data/meetings/20250601.mp3 - 会议文字记录(纯文本,需用双引号包裹) - 同时提供录音文件和已有文字稿,文字稿优先 输出: 返回 Markdown 格式的会议纪要,包含以下字段: - 会议主题 - 参会人(如无法识别则标注“未知”) - 讨论要点(按时间顺序,最多 5 条) - 决策项(格式:编号 + 描述 + 负责人,负责人未知时留空) - 待办事项(格式:编号 + 任务 + 负责人 + 截止日期,日期未知时标注“待定”) ## 使用约束 - 仅处理中文和英文的会议内容 - 如果输入文件超过 100MB,拒绝处理并提示用户压缩 - 如果录音中有效人声不足 30 秒,返回“录音过短,无法生成纪要” - 不要主动修改输入文件,不要在纪要中补充原文没有的信息这段描述里,“使用约束”部分特别重要。我见过太多 Skill 没有约束段,模型就会在边界情况下乱来。你写清楚“不要主动修改输入文件”“不要补充原文没有的信息”,模型就会收敛很多。还有一点,输出里的字段格式我写得很具体,比如“编号 + 描述 + 负责人”,这种颗粒度正好是模型能直接照做的程度。
3.3 核心代码实现有哪些隐藏要点
代码本身不复杂,但有几个点必须处理好。第一是幂等性,同一个输入无论调用多少次,输出应该一致。第二是超时控制,Skill 可能会被模型反复调用,如果某个操作特别慢,你得有超时机制,不能卡死整个 Agent。第三是依赖隔离,尽量把 Skill 的依赖声明清楚,不要和主项目的依赖混在一起。
我用一个最简单的 Python 片段说明“给模型返回语义化错误”的写法:
import os def run(file_path: str = "", text: str = "") -> dict: try: if not file_path and not text: return { "error": "缺少输入:请提供录音文件路径或会议文字记录", "suggestion": "请提供 file_path 或 text 参数", } if file_path: if os.path.getsize(file_path) > 100 * 1024 * 1024: return { "error": "文件超过 100MB", "suggestion": "请压缩后重试", } # 核心逻辑... minutes_markdown = "# 会议纪要\n\n## 会议主题\n..." return {"ok": True, "result": minutes_markdown} except Exception as e: # 这里不要把堆栈抛给模型,转成人话 return { "error": f"处理失败:{e}", "suggestion": "检查输入文件格式是否正确", }看到区别了吗?每个错误分支都返回了error加suggestion。模型拿到这个结果后,可以自己判断下一步怎么补救,甚至直接根据 suggestion 主动向用户询问缺失信息。如果你直接抛一个 Python 堆栈,模型大概率会一脸懵,然后开始瞎编。
这里再补充一个容易被忽略的点:Skill 的入口函数最好统一签名。无论内部逻辑多复杂,对外只暴露一个run函数,入参是平铺的字符串参数,出参是字典。这样不管接到哪个 Agent 框架里,适配成本都极低。你可以在项目里定一个通用模板,所有新 Skill 都套这个模板生成,维护起来会轻松很多。
3.4 调试 Skill 的“可观测性”做法
调试 Skill 最痛苦的点在于:你看不到模型是怎么理解你的描述文件的。同一个 Skill,换个模型可能调用逻辑就变了。我的做法是给 Skill 加一个 dry-run 模式。
具体操作是,在 Skill 入口处加一个环境变量或参数控制,当开启 dry-run 时,不真正执行核心逻辑,只输出模型传入的参数、以及你对这些参数的解析结果。这样你就能快速验证两个问题:模型有没有传对参数?你的参数解析逻辑有没有 bug?我经常发现,模型把max_length传成了字符串“三千”,这种问题在 dry-run 模式下一眼就能暴露。
另外强烈建议在实际运行日志里记录每次调用的触发理由。你可以在 SKILL.md 里要求模型在调用时附带一句“调用原因”,比如“用户提供了录音文件路径,符合触发条件”。这看起来多此一举,但实际调试时极其有用——你能立刻知道模型是“正确地调用了”还是“乱调用”。我自己排查线上问题时有相当一部分,就是靠日志里那句调用原因快速定位到是描述文件写得不清楚,而不是代码有 bug。
4. 常见问题与排查技巧实录
4.1 高频翻车点速查表
我把项目里见过的高频问题整理成了一张表,你可以直接对照排查:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 模型该调用 Skill 时不调用 | 描述文件触发条件写得太抽象 | 在 description 里加具体场景例子 |
| 模型不该调用时乱调用 | 缺少禁用条件(negative prompt) | 增加“使用约束”段落,明确什么情况不要用 |
| 参数传错或传不全 | 参数层级太深、字段命名有歧义 | 扁平化参数,字段名用通俗词汇,补充参数示例 |
| 输出格式不稳定 | 代码没有强制结构化输出 | 代码层固定 JSON 结构,不要依赖模型整理 |
| Skill 跑得很慢拖垮 Agent | 缺少超时和重试机制 | 加超时控制,把耗时操作拆成异步或分步 |
| 换一个模型后行为变化大 | 描述文件依赖了特定模型的表达习惯 | 用更通用的自然语言重写描述,减少“潜台词” |
这里我重点说一下第一行。模型不调用 Skill,很多时候不是模型笨,而是你的描述文件太“哲学”。你写“本工具用于数据处理”,模型不知道你的数据处理和它自己用代码处理有什么区别。你写“当用户提供 CSV 文件且要求筛选、统计、可视化时使用,输出 JSON 格式图表配置”,模型就知道该调用了。触发条件一定要具体到“什么输入 + 什么意图”,缺一不可。
4.2 我自己踩过的三个坑
第一个坑:把多个动作塞进一个 Skill。早期我做了一个“综合办公助手”Skill,既能查天气,又能算 Excel,还能写周报。结果模型每次调用都在猜我要干什么,猜错率极高。后来拆成了三个独立 Skill,准确率直接上来了。这个教训让我彻底认同了“单一职责”原则——在 Agent Skill 的开发里,这个原则比在普通软件工程里还要重要,因为你的调用方是一个会“猜”的模型,它猜的难度直接决定了成功率。
第二个坑:描述文件里写太多“可以”。比如“本 Skill 可以用 Python 处理数据,也可以生成图表,还可以导出报告”。模型面对这种描述,会把“可以”理解成“可选”,导致它经常只做其中一部分就交差。后来我把每个能力拆成独立的调用接口,在描述里明确“每次调用只执行一个函数,不自动串联多个函数”,问题才解决。
第三个坑:输出格式没在代码层兜底。我之前有个 Skill 让模型自己组织输出格式,结果一次线上运行,同一个任务拿到了三种不同结构的返回结果,下游解析代码直接被搞崩。后来所有 Skill 的输出我都改成代码层强制的 JSON 结构,模型只提供数据字段,不再让模型自由发挥格式。从那以后输出稳定性提升了一个量级。这事的教训就是:能交给代码保证的东西,绝不要交给模型“自觉”。
4.3 几个提高 Skill 成功率的细节
最后分享几个我实测对提升成功率特别有用的细节。一是 SKILL.md 里加一个“快速示例”段落,用一个具体的输入输出对演示完整调用过程,模型看过例子之后,照着做的概率远高于只读抽象描述。人看说明书也是一样,给个成品样例比讲十句规则都管用。
二是在参数说明里给每个字段加上必填/选填和取值范围,比如“max_length:可选,默认 3000,范围 500-10000”,模型传参时就不容易越界。我在调试时见过模型给max_length传了-1,如果你没有取值范围约束,这种错误参数会一路传进代码里,产生莫名其妙的结果。
三是建议在 Skill 里对“部分成功”的情况做明确返回。比如会议纪要生成时,录音转了文字但参会人没识别出来,这时候返回结果里标注“参会人识别失败,已置为未知”,而不是直接报错。模型知道哪些信息缺失后,会主动向用户确认,体验比“硬报错”好很多。这个思路本质上就是我前面说的语义化错误的延伸——让模型始终知道自己“拿到了什么、缺了什么、下一步能做什么”。
写 Skill 这件事,做到最后还是回到两个关键原则:让模型少猜,让代码多扛。描述文件把触发条件、输入输出、边界约束写清楚,模型就不需要瞎猜;代码层把结构化输出、错误处理、超时控制做好,系统就不会因为模型的一点点理解偏差而崩溃。把这个思路贯彻下去,你写出来的 Skill 大概率不会难用。
我个人实际操作中的体会是,Skill 开发是一个反复迭代的过程,别指望第一版就完美。先用最简单的骨架跑通一个场景,再根据日志里模型的调用行为不断打磨描述文件,最后再补全边界处理和测试。这种节奏看起来慢,但每一步都在积累对“模型怎么理解你的代码”的直觉,写到第三个第四个 Skill 的时候,你会明显感觉到顺手很多。