CClaude 中文站
能力扩展

Skills:把方法论变成标准化调用的能力

22 分钟阅读·学习笔记 · 梅奥整理

Tools 是动作,Skills 是方法论。用 SKILL.md 按需注入 SOP,让模型把同一类任务做得稳定一致。

这篇笔记解决什么问题

给 Agent 接上工具之后,它已经能查资料、读写文件、发请求了。但只要你让它干一类"有固定套路"的活儿——整理会议纪要、写发版公告、做代码评审——很快就会发现一个恼人的现象:同一个任务,跑十次能给你十种格式。今天的纪要有行动项表格,明天的没有;这次评审按严重程度分级,下次又变成流水账。

这不是模型笨,而是它压根不知道你心里的"标准答案"长什么样。你们团队约定纪要必须有"结论 + 行动项 + 负责人",评审必须先看错误处理再看性能——这些约定存在于你的脑子里,不在模型的上下文里。

Skills(技能)机制解决的就是这件事:把"做某类事情的标准流程"沉淀成文件,让模型在需要的时候把它加载进来、照着执行。这篇笔记先讲清概念,再拆解它的注入机制,最后动手在一个 Agent 循环里实现一版可运行的技能系统。

核心概念:Tools 是动作,Skills 是方法论

先把两个容易混淆的东西分开。

Tool 是一个原子动作:读文件、查数据库、调一次搜索接口。它由你写好的代码实现,模型只负责决定"调不调、传什么参数",真正干活的是程序。

Skill 是一套做事的方法论,也就是 SOP 或 Workflow:会议纪要该分哪几段、评审代码先看什么后看什么。它的载体不是代码,而是一段写给模型看的指令文档。模型拿到这段文档后,自己按步骤执行——中间可能还会去调用若干工具。

一句话概括:工具决定 Agent 能做什么,技能决定 Agent 怎么把事做对、做稳定

维度ToolSkill
载体代码函数Markdown 文档
入参按需设计的 JSON Schema统一的一个 query 字符串
调用产物执行结果(数据)一段流程指令
真正的执行者程序模型本身
迭代方式改代码、重新部署改文档、立即生效

最后一行值得多看一眼:技能的迭代成本极低。发现纪要少了"待认领事项"这一栏?改一行 Markdown 就完事,不用动任何代码。这让非工程角色(产品、运营)也能参与维护 Agent 的行为。

一个 Skill 就是一个目录

约定俗成的组织方式(由 Anthropic 的 Agent Skills 规范确立,各家基本都沿用了):每个技能是一个独立目录,目录名就是技能 id。

skills/
└── meeting-minutes/
    ├── SKILL.md          # 必备:技能主文件
    ├── assets/           # 可选:输出模板、示例文件
    ├── scripts/          # 可选:技能要用到的辅助脚本
    └── references/       # 可选:详细规范、背景手册

只有 SKILL.md 是必须的。它顶部是一段 YAML frontmatter,正文是方法论本身。以"会议纪要"技能为例:

---
name: meeting-minutes
description: 把零散的会议信息整理成结构化纪要,含结论与行动项。适用于评审会、周会、跨组对齐会。
---

# 会议纪要整理规范

当用户要求整理会议纪要时,按以下流程执行。

## 第一步:确认要素齐全

纪要必须包含四类信息,缺哪个就先向用户提问:

- 会议主题与时间
- 参会人
- 讨论了哪些议题、各自的结论
- 产生了哪些行动项

用户消息里已经给全的,不要重复追问。

## 第二步:按固定结构输出

**会议纪要:{主题}({日期})**

参会人:……

议题与结论
1. {议题}:{结论}

行动项
| 事项 | 负责人 | 截止时间 |

## 第三步:硬性约束

- 结论必须是明确的判断句,不许出现"大家充分交换了意见"这类空话
- 每个行动项必须有负责人,没有的标"待认领"并在结尾提醒
- 除表格外全文不超过 300 字

frontmatter 里的 namedescription 是整个机制的"门面"。后面会看到:模型在决定要不要用这个技能时,唯一能看到的就是这两行description 写得含糊,技能就永远不会被命中;写得过于宽泛,又会被误触发。好的写法是"做什么 + 什么时候用",就像上面示例那样把适用场景点出来。

关键机制:按需注入,而不是常驻系统提示词

看到这里你可能会想:这不就是一段提示词吗?直接塞进系统提示词不就完了?

如果你的 Agent 一辈子只干整理纪要这一件事,确实可以。但生产级 Agent 往往要同时会十几种活儿。把十几套 SOP 全塞进系统提示词,意味着每一轮对话——哪怕用户只是问一句"你好"——模型都要背着几千 token 的无关指令。上下文里无关内容越多,模型的注意力被稀释得越厉害,指令遵循质量反而下降。

Skills 的设计是分两级加载:

  1. 常驻的只有元信息。Agent 启动时扫描技能目录,把每个技能的 name + description 汇总给模型(每条也就一两行,开销可以忽略)。
  2. 正文按需进入上下文。模型判断当前任务匹配某个技能时,才发起一次"调用",此时 SKILL.md 全文才被送进对话。用不到的技能,模型全程感知不到它的存在。

这就是它和系统提示词的本质区别:系统提示词是永远在场的背景音,技能是需要时才翻开的操作手册

工程实现:把技能伪装成工具

那"调用一个技能"在 API 层面是怎么发生的?答案很取巧:复用 function calling 通道,把技能包装成一个假工具

每个技能都注册成一个函数定义,塞进请求的 tools 数组。三个设计要点:

  • 函数名加 skill__ 前缀(真工具加 tool__ 前缀),执行时靠前缀分发;
  • 所有技能共用同一套入参:只有一个 query 字符串。因为技能的"执行"逻辑是统一的——不管传什么,都是把 SKILL.md 原文返回去——所以不需要为每个技能设计参数;
  • description 直接取自 frontmatter,这是模型做路由决策的唯一依据。

一个技能被注册后长这样:

{
  "type": "function",
  "function": {
    "name": "skill__meeting-minutes",
    "description": "把零散的会议信息整理成结构化纪要,含结论与行动项。适用于评审会、周会、跨组对齐会。",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "用户希望用这个技能完成的原始任务"
        }
      },
      "required": ["query"]
    }
  }
}

而所谓"执行技能",就是读出对应的 SKILL.md,把全文作为 tool result 塞回消息列表。模型在下一轮看到这段方法论,就会照着做。整个生命周期是:

启动扫描目录 → name/description 注册为假工具
       ↓
模型判断任务匹配 → 发起 skill__xxx 调用
       ↓
程序读 SKILL.md → 全文作为 tool result 返回
       ↓
模型按方法论逐步执行(期间可继续调真工具)

技能注册器

下面用 Python 实现这一层。先写扫描与读取(frontmatter 用正则手工解析,没必要为两个字段引入 YAML 依赖):

# skill_registry.py
import re
from pathlib import Path

SKILL_ROOT = Path("skills")

_META_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)


def _read_meta(md_text: str) -> dict:
    """提取 SKILL.md 顶部 frontmatter 里的键值对。"""
    meta = {}
    m = _META_RE.match(md_text)
    if not m:
        return meta
    for raw in m.group(1).splitlines():
        if ":" not in raw:
            continue
        key, _, val = raw.partition(":")
        meta[key.strip()] = val.strip().strip("'\"")
    return meta


def scan_skills() -> list[dict]:
    """遍历技能根目录,只收集元信息,不加载正文。"""
    found = []
    if not SKILL_ROOT.exists():
        return found
    for folder in sorted(SKILL_ROOT.iterdir()):
        entry = folder / "SKILL.md"
        if not (folder.is_dir() and entry.exists()):
            continue
        meta = _read_meta(entry.read_text(encoding="utf-8"))
        found.append({
            "id": folder.name,
            "name": meta.get("name", folder.name),
            "description": meta.get("description", ""),
        })
    return found


def load_skill_body(skill_id: str) -> str:
    """读取完整 SKILL.md,作为 tool result 交还给模型。"""
    entry = SKILL_ROOT / skill_id / "SKILL.md"
    if not entry.exists():
        return f"[skill error] 找不到技能:{skill_id}"
    return entry.read_text(encoding="utf-8")

合并注册与前缀分发

把真工具和技能拼成同一个 tools 数组,执行时按前缀分流。假设真工具以 {name: {"description": ..., "schema": ..., "run": 可调用}} 的形式维护在一个字典里:

# agent_core.py
import json
from openai import OpenAI

from skill_registry import scan_skills, load_skill_body

# 任意 OpenAI 兼容服务均可:DeepSeek / Kimi / GLM / 本地 vLLM……
client = OpenAI(
    base_url="https://your-provider.example.com/v1",
    api_key="YOUR_API_KEY",
)
MODEL = "your-model-name"

SKILL_QUERY_SCHEMA = {
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "description": "用户希望用这个技能完成的原始任务",
        }
    },
    "required": ["query"],
}


def build_tool_specs(native_tools: dict) -> list[dict]:
    """真工具 + 技能,统一构造成 function calling 的 tools 数组。"""
    specs = []
    for name, tool in native_tools.items():
        specs.append({
            "type": "function",
            "function": {
                "name": f"tool__{name}",
                "description": tool["description"],
                "parameters": tool["schema"],
            },
        })
    for sk in scan_skills():
        specs.append({
            "type": "function",
            "function": {
                "name": f"skill__{sk['id']}",
                "description": sk["description"] or sk["name"],
                "parameters": SKILL_QUERY_SCHEMA,
            },
        })
    return specs


def dispatch(func_name: str, args: dict, native_tools: dict) -> str:
    """按前缀分发:skill__ 走文档加载,tool__ 走真实执行。"""
    if func_name.startswith("skill__"):
        return load_skill_body(func_name.removeprefix("skill__"))
    if func_name.startswith("tool__"):
        real = func_name.removeprefix("tool__")
        if real not in native_tools:
            return f"[dispatch error] 未注册的工具:{real}"
        return native_tools[real]["run"](**args)
    return f"[dispatch error] 无法识别的调用:{func_name}"

接入 Agent 循环

def run_turn(messages: list[dict], native_tools: dict) -> str:
    specs = build_tool_specs(native_tools)
    while True:
        resp = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=specs,
        )
        msg = resp.choices[0].message
        if not msg.tool_calls:          # 没有调用请求,说明是最终回答
            return msg.content
        messages.append(msg)
        for call in msg.tool_calls:
            output = dispatch(
                call.function.name,
                json.loads(call.function.arguments or "{}"),
                native_tools,
            )
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": output,
            })

注意 while True 这个循环形态很关键:技能返回的只是方法论,不是最终答案。模型拿到 SKILL.md 之后往往还要继续跑好几轮——追问用户、调真工具、最后组装输出。循环必须允许这个过程自然发生。

跑起来验证:对 Agent 说"帮我整理一下刚才架构评审会的纪要,定了三件事……",日志里会先出现一次 skill__meeting-minutes 调用,随后模型按模板产出结构一致的纪要。如果只说"帮我整理个纪要"而不给任何细节,模型会先追问主题、参会人和结论——因为 SKILL.md 第一步就要求它这么做。

技能的两个进阶玩法

引用外部文件,保持正文精简。 复杂技能不必把所有内容塞进一个 Markdown。比如一个"竞品速览"技能,SKILL.md 里只写一句"输出格式参照 assets/brief-template.md"——模型看到这句话,会自己调用文件读取工具把模板拿进来。方法论和素材就此解耦:流程稳定不变,模板可以随时替换。

指挥模型调工具。 SKILL.md 里完全可以写"先用 tool__web_search 检索目标公司近三个月的动态,再用 tool__read_file 读取本地已有的调研笔记,最后按模板汇总"。这时技能就从"格式规范"升级成了"多步作业的编排脚本",而编排的执行者是模型自己。

这两个玩法叠加起来,一个技能目录就能承载相当复杂的工作流,而系统提示词始终保持干净。

实践要点与常见坑

  • description 是唯一的路由信号。 技能不被命中,九成是 description 的问题。别写"处理会议相关任务"这种模糊描述,要写"把零散会议信息整理成含行动项的结构化纪要"。
  • 步骤写成可判断的条件,而不是含糊的建议。 "如果用户已提供参会人则跳过提问"是可执行的;"酌情收集信息"是不可执行的。SKILL.md 越像一份能被新员工照做的 SOP,模型执行得越稳。
  • 别把技能正文搬回系统提示词"图省事"。 这等于亲手拆掉按需注入机制,退化回注意力被稀释的老问题。
  • query 参数可以不消费,但别删。 统一入参是为了让所有技能共享一套执行路径;即使实现里暂时用不上 query,保留它也能让调用日志里留下"模型当时想干什么"的记录,排查路由错误时非常有用。
  • 技能数量多了以后,同样要控制元信息总量。 几十个技能的 name/description 也会累积成可观的 token。定期合并职责重叠的技能、下线没人用的技能,和管代码依赖是一个道理。

Skills 本质上是结构化、可版本管理的提示词。它把"这类事我们怎么做"从口口相传变成了仓库里的文件——可以 review、可以 diff、可以回滚。这一层补上之后,Agent 的扩展就有了两个正交的维度:工具扩展它的能力边界,技能扩展它的做事章法。本系列后面还会讲到 MCP,解决的则是第三个维度:如何低成本接入别人已经写好的外部工具。

动手清单

  1. 照本文的注册器实现,建一个 bug-report 技能:规定"复现步骤 / 期望行为 / 实际行为 / 环境信息"四段结构。同一段口头描述分别在有无技能的情况下各跑五次,对比输出结构的稳定性。
  2. meeting-minutes 技能加双模式:速记版(100 字内)和归档版(500 字内),在 SKILL.md 里写清两者的区别与选择规则,观察模型是否会主动让用户确认用哪种。
  3. 把纪要的输出模板抽到 assets/minutes-template.md,SKILL.md 里只留一句引用,验证模型会不会自己调文件工具把模板读进来。
  4. 写一个需要编排工具的技能(例如"每周技术雷达":先搜索再汇总成固定表格),体会 SKILL.md 如何驱动多轮工具调用。
  5. 做一个反向实验:把某个技能的 description 改成一句含糊的"处理相关任务",统计命中率的变化——亲身感受 description 作为路由信号的分量。

*本文为学习笔记,知识框架参考自叶小钗「生产级 Agent」系列课程;文字与代码示例为本站原创整理。*

#Skills#SKILL.md#按需注入