1. 从一次 Agent 失控说起:SKILL.md 到底是什么
如果你正在做 AI Agent 项目,大概率遇到过这种场景:在系统提示里写了几千字的规则,Agent 该跑偏还是跑偏;接了一堆 MCP 工具,问它能不能用,它回你一句“我没有可用的工具”;同一个任务换个说法,执行流程就完全不一样。问题不在于模型不够强,而在于你把“知识、流程、工具”全塞进了一个上下文里,模型分不清什么时候该用哪一段。
SKILL.md 就是来解决这个问题的。它本质上是一个用自然语言写的“技能说明书”,放在一个独立文件夹里,配合脚本和参考文件,构成一个可被 Agent 按需加载的能力单元。你可以把它理解成给 Agent 准备的一本操作手册:平时只记住书名和一句话简介,真正需要做这件事的时候才翻开细看,需要动手时才去调用里面的工具脚本。
这套机制最早由 Anthropic 在 Claude 的能力扩展体系中提出,现在已经被大多数主流 Agent 开发框架接受为一种标准扩展方式。它的核心价值有三个:第一是可复用,写一次可以在不同会话、不同项目里反复触发;第二是可控,行为边界写在文档里,减少模型自由发挥带来的幻觉;第三是可组合,多个 Skill 可以串成一条完整工作流。
适合读这篇文章的人很明确:你已经在用 Claude Code、Cline、Cursor 或者自建的 Agent 框架,想让 Agent 稳定执行某类特定任务,比如“按固定格式生成周报”“把日志按规则归类”“调用内部 API 做数据校验”,但又不希望每次都把完整指令塞进系统提示。接下来我会从文件结构、加载机制、MCP 协作方式一路讲到可复制的模板和本地验证步骤,你跟着做就能跑通第一个自定义 Skill。
2. SKILL.md 文件结构与三级加载机制详解
2.1 一个 Skill 文件夹里到底放什么
一个标准的 Skill 以文件夹形式存在,名字通常就是技能标识,比如weekly-report。文件夹内部至少包含一个SKILL.md,其余是可选的脚本和资源。典型结构如下:
weekly-report/ ├── SKILL.md # 必需:元数据 + 说明文档 ├── scripts/ │ └── fetch_data.py # 可选:可执行脚本 ├── references/ │ └── format.md # 可选:参考文档、模板 └── assets/ └── template.xlsx # 可选:静态资源SKILL.md本身分两段:开头是 YAML front matter,写元数据;下面是 Markdown 正文,写详细说明。元数据部分至少要有name和description,这两个字段决定了 Agent 能不能在第一时间“想起”这个技能。
--- name: weekly-report description: 当用户需要生成周报、汇总本周工作、按固定模板输出进度时使用。输入为原始工作记录,输出为 Markdown 格式周报。 ---description的写法直接决定触发准确率。不要写“一个用于处理周报的技能”这种废话,要写清楚“什么时候用、输入是什么、输出是什么”。Agent 在意图匹配阶段只看这段元数据,它就像名片上的职务和专长,写模糊了别人就不知道找你干什么。
2.2 三级渐进式加载:Token 效率的关键
Skill 最巧妙的设计是三级加载,它在“让 Agent 知道有这个能力”和“不把上下文撑爆”之间找到了平衡。
Level 1 元数据始终加载。Agent 启动时会把所有已安装 Skill 的name和description读进上下文。这部分非常轻,一个技能大约 100 Token 左右,所以你装几十个技能也不会明显挤占窗口。这一步解决的是“Agent 知道有哪些技能可用”。
Level 2 说明文档触发时加载。只有当用户请求和某个 Skill 的 description 匹配上,Agent 才会去读SKILL.md的正文。正文建议控制在 5000 Token 以内,写清楚步骤、注意事项、输入输出格式。这一步解决的是“Agent 知道这个技能具体怎么做”。
Level 3 资源与代码按需加载。脚本、参考文档、模板这些不会自动进上下文。Agent 在执行过程中需要时,才通过 bash 读取或运行。脚本代码本身不进入上下文窗口,只把执行结果拿回来。这一步解决的是“Agent 能拿到大量辅助信息但不占上下文”。
把这三层串起来,完整调用链路是:用户提出请求 → Agent 扫一遍所有 Skill 的元数据做意图匹配 → 命中后读取对应 SKILL.md 正文 → 按正文指引决定是否调用脚本或读取参考文件 → 执行并返回结果。整个过程里,只有命中的那个技能会消耗较多 Token,其余技能只占元数据那一点点空间。
2.3 Skill 和 Command、MCP、Rules 的边界
很多人会把这几个概念混在一起,我用一张表说清楚。
| 概念 | 触发方式 | 核心作用 | 典型场景 |
|---|---|---|---|
| Command | 用户主动输入 | 执行固定动作 | /commit、/review |
| MCP | 模型按需调用 | 提供外部工具能力 | 查数据库、调 API |
| Rules | 启动时全量加载 | 项目级固定约束 | 代码风格、目录规范 |
| Skill | 模型自动匹配 | 封装流程与知识 | 周报生成、日志归类 |
关键区别在于:Command 是你按的按钮,MCP 是工具箱里的工具,Rules 是贴在墙上的规章,而 Skill 是一本菜谱——它告诉你做这道菜需要哪些工具、按什么步骤、注意什么火候。MCP 提供“能做什么”,Skill 提供“什么时候做、怎么做”。两者不是替代关系,而是协作关系,下一节会具体讲怎么配合。
3. 可复制配置:SKILL.md 模板与 MCP 协作配置
3.1 一份可直接用的 SKILL.md 模板
下面这份模板你可以直接复制,改掉 name、description 和正文步骤就能用。我把它设计成“日志归类”场景,因为这类任务流程明确、边界清晰,最适合作为第一个 Skill 练手。
--- name: log-classifier description: 当用户提供原始日志文本、要求按错误级别归类、提取关键异常或生成日志摘要时使用。输入为纯文本日志,输出为 Markdown 表格。 --- # 日志归类技能 ## 角色 你是一名资深 SRE,擅长从杂乱日志中快速定位异常模式。 ## 输入 用户提供的原始日志文本,可能包含多行、多种级别。 ## 执行步骤 1. 逐行扫描日志,识别级别标记:ERROR、WARN、INFO、DEBUG。 2. 对 ERROR 和 WARN 行,提取时间戳、模块名、异常关键词。 3. 按级别分组,统计每类数量。 4. 输出 Markdown 表格,列为:级别、数量、典型样例。 ## 输出格式 固定输出如下结构,不要添加额外解释: | 级别 | 数量 | 典型样例 | |------|------|----------| | ERROR | 3 | ... | | WARN | 5 | ... | ## 红线 - 不要编造日志中不存在的行。 - 不要对异常原因做推测,只做归类。 - 如果日志为空,直接返回“无有效日志”。这份模板体现了几个原则:单一职责(只做归类)、给了明确输出格式(Few-Shot 思路)、立了红线(防止幻觉)、定义了输入输出接口。你写自己的 Skill 时,把这四点对齐,触发准确率和执行稳定性都会明显提升。
3.2 MCP 协作配置:让 Skill 能调用外部工具
Skill 本身不提供工具能力,它负责“指挥”,真正干活的是 MCP。假设你已经有一个查询内部工单系统的 MCP Server,现在要让上面的日志归类 Skill 在遇到未知异常时去查工单,需要在 Agent 的配置里同时声明 MCP Server 和 Skill 目录。
以 Claude Code 风格的配置为例,settings.json里这样写:
{ "mcpServers": { "ticket-system": { "command": "npx", "args": ["-y", "@your-org/ticket-mcp"], "env": { "TICKET_API_BASE": "https://taotoken.net/api", "TICKET_API_KEY": "your-key-here" } } }, "skills": { "directories": ["./skills"] } }这里有三件套必须对齐:Base URL、Key、Model ID。如果你用的是兼容 Anthropic 协议的自建 Agent,Base URL 填https://taotoken.net/api,Key 在控制台生成,Model ID 按你实际调用的模型填。三者缺一,请求就会在鉴权或路由阶段失败。
配置完成后,Skill 的正文里可以这样引用 MCP 工具:
## 可用工具 - ticket-system.query:根据异常关键词查询历史工单。 当 ERROR 行中出现未在 references/known-errors.md 中记录的异常时, 调用此工具查询是否有相似工单,并把工单号附在样例后面。注意,Skill 正文里只写“什么时候调用、传什么参数”,不写工具的具体实现。工具实现归 MCP Server 管,职责分离才能让两边都稳定。
3.3 目录放置与加载顺序
Skill 目录的放置位置取决于你的 Agent 框架。常见约定是项目根目录下的skills/文件夹,每个子文件夹一个技能。Agent 启动时会扫描这个目录,读取每个SKILL.md的 front matter。如果你有多个技能目录,在配置里按优先级排列,靠前的先被扫描。
加载顺序上,元数据永远最先加载,正文在触发时加载,脚本在调用时加载。这意味着你可以在skills/下放几十个技能而不用担心启动变慢或上下文被占满。真正需要控制的只有每个技能 description 的精准度,因为那是唯一始终在上下文里的部分。
4. 本地验证:跑通第一个自定义 Skill
4.1 准备一个最小可验证环境
先建目录和文件:
mkdir -p skills/log-classifier cd skills/log-classifier touch SKILL.md把 3.1 节的模板内容写进SKILL.md。然后准备一份测试日志:
cat > test.log <<'EOF' 2024-01-15 10:23:11 ERROR [order-service] Connection timeout to payment-gateway 2024-01-15 10:23:12 INFO [order-service] Retry attempt 1 2024-01-15 10:23:15 WARN [inventory-service] Stock level below threshold for SKU-8821 2024-01-15 10:23:18 ERROR [order-service] Connection timeout to payment-gateway 2024-01-15 10:23:20 INFO [order-service] Order 10023 created EOF4.2 触发 Skill 并观察加载行为
在你的 Agent 对话里输入:“帮我把 test.log 里的日志按级别归类,输出表格。”如果 description 写得准确,Agent 应该自动匹配到log-classifier,读取正文,然后按模板输出。
验证成功的标志有三个:第一,Agent 没有反问你“用什么格式”,说明正文里的输出格式生效了;第二,输出是 Markdown 表格,列名和模板一致;第三,ERROR 和 WARN 的样例是从日志里摘的,没有编造。
如果 Agent 没有触发 Skill,先检查 description 是否包含用户可能说的关键词。比如用户说“归类日志”,你的 description 里只有“日志摘要”,就可能匹配不上。把常见说法都覆盖进去,但不要堆砌无关词。
4.3 用 API 直接验证 Skill 加载链路
如果你想脱离对话界面,直接用 API 验证,可以发一个带 Skill 元数据的请求。下面是一个 curl 示例,Base URL 用https://taotoken.net/api:
curl -X POST https://taotoken.net/api/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: $TAOTOKEN_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 1024, "system": "你有一个可用技能:log-classifier,描述为:当用户提供原始日志文本、要求按错误级别归类时使用。", "messages": [ {"role": "user", "content": "把这段日志归类:ERROR timeout; WARN low stock; ERROR timeout"} ] }'这个请求把 Skill 的元数据放进 system,模拟 Level 1 加载。如果模型返回表格结构,说明元数据被正确识别。再进一步,你可以把 SKILL.md 正文也放进 system,模拟 Level 2,观察输出是否更稳定。
4.4 验证 MCP 工具调用是否打通
在 Skill 正文里加上工具引用后,用一条会触发工具调用的输入测试。比如日志里出现一个known-errors.md里没有的异常关键词,看 Agent 是否会去调ticket-system.query。如果调用成功,返回结果里会带工单号;如果失败,通常会看到工具名报错或超时。
这一步的关键是确认三件套都对:Base URL 指向https://taotoken.net/api,Key 有效,Model ID 和你的账号权限匹配。任何一项不对,工具调用都会在鉴权阶段被拦下。
5. 常见报错排查:401、local proxy failed 与 choices 解析失败
5.1 401 Unauthorized:Key 或 Base URL 不匹配
这是最常见的报错。表现是请求直接返回 401,日志里写invalid api key或authentication failed。原因通常有三个:Key 复制时带了空格或换行;Base URL 写成了带路径的完整地址而不是根地址;Key 对应的账号没有开通对应模型权限。
排查顺序:先用echo $TAOTOKEN_API_KEY | wc -c确认长度合理,没有多余字符;再确认 Base URL 是https://taotoken.net/api,不要自己拼/v1/messages到配置里,SDK 通常会自己加;最后去控制台确认 Key 状态和模型权限。三件套里 Base URL、Key、Model ID 必须来自同一个账号体系,混用就会 401。
5.2 local proxy failed:本地代理配置冲突
这个报错通常出现在你本机设置了 HTTP 代理,但 Agent 或 SDK 不走代理,或者代理地址失效。表现是连接被拒绝或超时,日志里写local proxy failed或connect ECONNREFUSED。
处理方式:检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向一个已经关闭的本地端口。如果是,清掉这两个变量再试。如果你确实需要走网络中间层,确保地址和端口正确,并且该中间层允许访问taotoken.net。注意,这里说的是正常的网络配置,不要使用任何非正规的网络工具。
5.3 reading choices 解析失败:响应结构不符合预期
这个报错说明 SDK 在解析响应时找不到choices字段。常见原因是 Base URL 指向了一个兼容 OpenAI 协议的端点,但你用的 SDK 是按 Anthropic 协议解析的,或者反过来。两种协议的响应结构不同,混用就会解析失败。
解决方法是确认你的 SDK 和 Base URL 协议一致。如果你用的是 Anthropic SDK,Base URL 用https://taotoken.net/api,请求路径和响应字段都按 Anthropic 格式来。如果你用的是 OpenAI SDK,需要确认端点是否支持 OpenAI 格式。不要在一个请求里混用两套协议。
5.4 OAuth 相关报错:令牌过期或权限不足
如果你用的是 OAuth 方式接入,可能会遇到token expired或insufficient scope。前者是令牌过期,重新走一次授权流程即可;后者是授权范围不包含你要调用的模型或工具,需要在授权时勾选对应权限。
排查时先看报错里的 scope 字段,确认你申请的权限和实际调用的是否一致。如果 Skill 里引用了 MCP 工具,而 OAuth 令牌没有该工具的权限,也会在工具调用阶段报权限错误。这时候要么重新授权,要么在 MCP Server 侧调整权限配置。
5.5 Skill 不触发:description 匹配失败
这不是报错,但比报错更让人困惑。Agent 正常回复,只是没用你的 Skill。原因几乎总是 description 写得太泛或太窄。太泛比如“处理数据”,Agent 不知道什么时候该用;太窄比如只写了“归类 ERROR 日志”,用户说“整理日志”就匹配不上。
改法是把用户可能说的动词和名词都覆盖进去,同时保持一句话说清输入输出。你可以把 description 当成搜索关键词来写:用户会用什么词来描述这个任务,你就把这些词放进去。改完重启 Agent 让元数据重新加载。
6. 把 Skill 接入你的 Agent 工作流
走到这里,你已经有了一个能触发的 Skill、一份可复制的模板、一套 MCP 协作配置,以及一份排错清单。接下来要做的不是继续加功能,而是把它放进真实工作流里跑一周,观察哪些输入触发了、哪些没触发、哪些输出需要调整。
我自己的习惯是给每个 Skill 建一个bad-cases.md,把没触发或输出不对的例子记下来,每周把共性问题补进 description 或正文红线。Skill 不是一次写完就完事的,它更像一个需要迭代的产品,触发准确率和输出稳定性都是调出来的。
如果你还没有可用的 API Key,可以去控制台生成一个,然后按第 4 节的 curl 示例先验证链路通不通。链路通了,再把 Skill 目录挂到你的 Agent 配置里。接入文档里有不同框架的配置示例,照着改 Base URL 和目录路径就行。需要长期跑编码或 Agent 任务的,可以看下 Coding Plan,它更适合高频调用场景。先把第一个 Skill 跑通,后面加技能就是复制文件夹改内容的事了。