Skip to content

Agent Skills:把团队经验封装成按需加载的 SKILL.md

本文是 AI 应用系统学习系列的 L2 核心篇。前置:生产级 Agent 架构:从 Demo 到上线。 学完可以配合面试题食用:MCP:Model Context Protocol生产级 Agent 架构:从 Demo 到上线

先看一个浪费 token 的现场

你给数据库 Agent 配了十几个工具:查慢日志、看执行计划、杀会话、改配置。用户问一句"订单查询怎么变慢了",模型开始即兴发挥:第一次先 SHOW PROCESSLIST,第二次直接跳去改 innodb_buffer_pool_size,第三次忘查慢日志就下了结论。

同一个问题,三次运行三条路径。每次它都要在上下文里重新推一遍排查顺序,中间步骤错了还得重来,token 一趟烧掉几千。团队里 DBA 老张明明有一套验证过上百次的排查 SOP,但这份经验存在他脑子里和 Confluence 文档里,Agent 每次都从零开始。

问题的根子:步骤性知识没有存进 Agent 能稳定执行的地方。模型参数里没有你们团队的 SOP,上下文里每次临时塞又不稳定。Agent Skills 就是给这类知识一个固定的家:一个目录、一份 SKILL.md、几个脚本。模型先知道"有这么个技能",判断用得上才把正文加载进来,然后照着稳定的流程走。

两种封装:Toolkits 是黑盒,Skills 是白盒

把经验交给 Agent,目前有两条路线,取舍完全相反。

Toolkits:代码封装,黑盒。 把多个底层工具组合成一个高阶工具,逻辑全写死在代码里,对外只露一个 JSON Schema。比如把"查慢日志 + 解析 + 聚合"封装成 analyze_slow_queries(),模型只会看到函数签名和参数说明。好处是确定性强——每次执行的路径一模一样,坏处是每变一次排查思路都要改代码、发版,改造成本高,而且模型完全不知道这个函数内部干了什么,出了问题没法灵活应对。

Agent Skills:自然语言写 SOP,白盒。 把流程写成 Markdown,配上可执行的脚本和参考资料,模型读到的是"先查什么、再查什么、什么情况下别动手"。好处是改流程就是改文档,老手随时能把新踩的坑补进去;模型也能理解每一步的意图,遇到边界情况自己判断。代价是执行路径不如纯代码确定——模型偶尔会跳步,所以关键步骤要配上脚本让它照着调。

一条经验选哪条路线,判断标准就一条:流程固定、参数变化,做成 Toolkit 函数;流程会演化、需要人随时补充判断,写成 Skill。慢 SQL 排查属于后者——日志格式会变、业务场景会变,老手每周都有新发现要补进去。

两者也不互斥。常见的组合是:Skill 的正文里写着"第三步调用 analyze_slow_queries 脚本",把确定性的脏活累活交给代码,流程编排和判断留给模型。

渐进式披露:Skill 可以多,上下文不能炸

如果每个 Skill 的全文都常驻上下文,装 20 个技能就是几万 token 起步,还没干活先花掉一笔钱。Skills 的解法是渐进式披露(progressive disclosure),分两层加载:

  1. 元数据层:只有 name 和 description,每个 Skill 几十 token,全部常驻上下文。模型靠这个判断"当前任务要不要用某个技能"。
  2. 正文层:SKILL.md 的完整内容(流程、注意事项、脚本清单),只在模型判断需要时才读进来。

也就是说,装 50 个 Skill,常驻开销只有 50 条描述;真正干活的那个 Skill 才占完整上下文。加载的决策者是模型自己——它读了描述,觉得这次任务相关,就发起读文件的动作。

mermaid
flowchart LR
    A[任务进入] --> B[上下文里只有<br/>所有 Skill 的 name + description]
    B --> C{模型判断:<br/>本次任务命中哪个 Skill?}
    C -- 命中 --> D[加载该 Skill 的 SKILL.md 正文<br/>+ 按需读 scripts/ references/]
    C -- 不命中 --> E[按普通方式干活]
    D --> F[照 SOP 执行<br/>关键步骤调脚本]

这套机制和 MCP 是互补关系,不是竞争。三句话摆清楚三者的位置:Function Calling 是手——模型调工具的底层动作;MCP 是接口标准——工具和数据源怎么统一接进来;Skills 是操作手册——拿到这些工具之后,按什么顺序做、哪些坑别踩。MCP 服务器解决"能力从哪来",Skill 解决"能力怎么组织成流程"。

动手实操:写一个"慢 SQL 排查"Skill

目录结构

一个 Skill 就是一个目录。约定优于配置:宿主扫描目录,读 frontmatter,其余按需加载。

skills/
└── slow-sql-triage/
    ├── SKILL.md          # 必需:frontmatter + 排查流程
    ├── scripts/
    │   ├── fetch_slow_log.py   # 拉慢日志并按指纹聚合
    │   └── explain_query.py    # 对单条 SQL 跑 EXPLAIN 并给出报告
    └── references/
        └── mysql-8.0-variables.md  # 关键参数的调整建议和默认值

SKILL.md 全文

markdown
---
name: slow-sql-triage
description: >
  排查 MySQL 慢查询。当用户反馈"查询变慢 / 接口超时 / 数据库
  CPU 高",或需要分析慢日志、执行计划时使用。不适用于:Redis 热
  key 排查、应用侧线程池问题、表结构设计评审。
---

# 慢 SQL 排查 SOP

## 第 0 步:确认范围
先问清楚(或从告警里拿到):哪个库、什么时间开始、是单条 SQL
还是整体变慢。整体变慢优先查连接数和锁等待,不要急着看单条 SQL。

## 第 1 步:定位 Top SQL
运行 `python3 scripts/fetch_slow_log.py --since 2h`,按查询指纹
聚合,输出 Top 5。禁止跳过这步直接改参数。

## 第 2 步:分析执行计划
对 Top SQL 逐条运行 `python3 scripts/explain_query.py --sql "<SQL>"`
重点看:type 是否为 ALL、rows 是否比返回行数大两个数量级、
Extra 里有没有 Using filesort / Using temporary。

## 第 3 步:给出结论
结论必须包含三部分:根因(索引缺失 / 统计信息过期 / 参数配置 /
数据量增长)、证据(EXPLAIN 输出的关键字段)、建议动作。
禁止在没有 EXPLAIN 证据时建议改 innodb_buffer_pool_size。

## 红线
- 生产库禁止直接 KILL 超过 10 秒的会话,先报给值班 DBA
- 涉及改参数的建议,一律先引用 references/mysql-8.0-variables.md
  里的说明,标注影响范围

description 有三行,前两行写什么时候用,最后一行明确写什么时候不用。这不是废话——描述含糊的 Skill 会被误触发,用户问"Redis 慢"它也跳出来,比没有这个技能更糟。

宿主侧最小加载器

下面这个 Python 片段演示"元数据常驻 → 按需加载正文"的最小实现,核心逻辑生产宿主(如 Claude Code、OpenClaw)都类似:

python
import yaml
from pathlib import Path

class SkillLoader:
    """两级加载:目录扫描只留元数据,正文按需读盘。"""

    def __init__(self, skills_dir: str):
        self.skills_dir = Path(skills_dir)
        self._meta = {}   # name -> {description, path}
        for skill_md in self.skills_dir.glob("*/SKILL.md"):
            text = skill_md.read_text(encoding="utf-8")
            fm = text.split("---")[1]           # 取 frontmatter 段
            data = yaml.safe_load(fm)
            # 常驻内存的只有 name/description,几十 token
            self._meta[data["name"]] = {
                "description": data["description"],
                "path": skill_md.parent,
            }

    def catalog_for_system_prompt(self) -> str:
        """拼进 system prompt 的技能清单:每个技能两行。"""
        lines = ["可用技能:"]
        for name, m in self._meta.items():
            lines.append(f"- {name}: {m['description']}")
        lines.append("判断当前任务命中某个技能时,用 read_file 读取对应 SKILL.md 再行动。")
        return "\n".join(lines)

    def load_body(self, name: str) -> str:
        """模型判定相关后才调用:加载完整正文。"""
        return (self._meta[name]["path"] / "SKILL.md").read_text(encoding="utf-8")

# 宿主启动时:catalog 全文进 system prompt(50 个技能也就 2-3K token)
# 模型运行中:自主决定 load_body("slow-sql-triage"),正文才进上下文

宿主不用管脚本怎么执行——SKILL.md 里已经写了"第 1 步运行什么命令",模型的 Function Calling 会去调,MCP 或本地 shell 负责真正执行。

常见误区与小结

  • description 只写"是什么"不写"什么时候用"。触发靠模型读描述做判断,没有使用场景的描述等于没装。
  • 不写"什么时候别用"。误触发比不触发更糟——用户问 Redis 它跑去查 MySQL 慢日志,一次错误执行的成本远超省下的规划 token。
  • 把 Skill 当 Toolkits 用。流程里全是模型自由发挥的散文,没有配脚本。确定性要求高的步骤应该固化成脚本,让模型照着调。
  • 装第三方 Skill 不看内容。Skill 里可以夹带任意脚本,装它等于给 Agent 装软件。脚本要可读,来源要审计,供应链和 npm 包一个道理。
  • Skill 常驻全文进上下文。这就退化回了普通 prompt,渐进式披露的全部意义就没了。

小结:Skills 在整条 Agent 学习路径里的位置是"经验工程"——Agent 架构解决单次任务怎么跑稳,Skill 解决团队经验怎么复用。判断一个团队 Agent 成熟度,看它 skills/ 目录的 git log 比看他 PPT 有用。

下一篇讲 Harness Engineering:模型之外的那圈"挽具"代码——重试、校验、上下文组装,为什么说它比提示词更决定线上效果。

参考

  • Anthropic: Agent Skills 文档与工程博客(Introducing Agent Skills,2025-10)
  • Anthropic: Equipping agents for the real world with Agent Skills
  • 本项目实验代码:agent-lab/skills-demo/

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1