1. "什么都能干"是 Agent 最大的谎言:技能化拆解为什么非做不可
我接手过一个典型的失败项目:团队花了三个月,把产品需求、代码规范、部署手册、客户话术全塞进一个超大 System Prompt,配上 40 多个独立工具函数,打算做一个"什么都能干"的售前支持 Agent。结果呢?它确实什么都能聊一点,但什么都做不透彻——问它产品报价,它能给你扯出技术架构;让它写客户跟进邮件,它一半时间在纠结该用哪个工具,一半时间把名单里的字段张冠李戴。
这个锅不该全甩给模型。问题出在我们的设计假设:Agent 是无所不能的执行者,只要给它足够多的指令和工具,它就能处理所有事情。但真实世界里,一个成年人也没法同时精通法务、财务、客服、测试——他是靠分工协作和"手边最常用的几样工具"来工作的。Agent 也一样,它需要的是"技能化"的能力单元,不是说教的"全能咒语"。
Agent Skills(技能包)这个概念最近在社区里被反复讨论,核心思路其实特别朴素:把一类任务所需要的操作说明、脚本、示例、依赖打包成一个独立目录,Agent 在遇到对应场景时按需加载,用完即走。你不需要在系统提示词里常驻所有知识,只需要在提示词里放一份"技能索引"。OpenAI 在 ChatGPT 里做了类似的能力插槽,Anthropic 在 Claude 里也提供了 Skills 的实验形态,开源社区更是一堆自发的 agent-skills 仓库在分享料理、数据分析、PPT 生成之类的技能包。
这篇文章不打算做概念科普。我接下来会分享我过去半年把 Agent 从"单体巨怪"改造成"技能驱动"的完整实践:标准形态长什么样、怎么从零写一个可复用的 Skill、接入框架时哪些坑非踩不可,以及最后我给团队定下的落地规范。如果你也正在被"Agent 要么太笨要么太飘"折磨,这篇应该能给你一套直接能抄的作业。
1.1 单体 Agent 的上下文诅咒
做过 Agent 的都知道,模型上下文窗口再大,也不是用来装业务逻辑的。单个 Agent 如果要在一次对话里同时处理"判断用户意图 + 检索知识库 + 调用业务系统 + 记忆多轮状态",模型必须反复在无关信息里做注意力筛选,而注意力是典型的"池子越小,溅起的水花越窄"。
我自己做过实测:在同样一个代码审查场景里,System Prompt 从 800 个 token 膨胀到 6000 个 token 后,模型遵循"只报告 P0 级别问题"这条指令的准确率下降得相当明显。为什么?因为提示词越长,指令之间互相干扰越厉害,"不要做 X"这类否定式指令尤其容易被淹没。更讽刺的是,我们把每个工具的使用说明写得越详细,Agent 越容易在工具选择上犯选择困难症。
技能化拆解最直接的价值,就是把 6000 token 的"总部大楼"打散成若干 500 token 的"街边门店"。Agent 只需要认识每块招牌(技能名 + 描述),需要时再走进去看具体规则(加载 SKILL.md)。这让 System Prompt 瘦身 80% 以上,也让模型在每一个局部决策点上的上下文都非常干净。
1.2 Skills 和 Tools、MCP、插件到底啥关系
这是我在团队内部被问得最多的问题。先说结论:Tools 是能力的最小原子,MCP 是一种能力封装协议,插件是产品形态,而 Skills 是面向 Agent 的"任务级"知识单元。四者不冲突,甚至常常叠在一起用。
打个比方:Tools 是一颗颗螺丝钉,MCP 是统一了螺丝钉规格的国标文件,插件是装好了的电气设备,而 Skills 则是"怎么完成一台电视机组装"的完整作业指导书。作业指导书里会提到用哪些螺丝钉(Tools),可能基于某种国标(MCP),也可能直接被打包成一个设备(插件)分发。
落到工程上,一个 Skill 内部完全可以调用现有 Tools。比如我的会议纪要 Skill,脚本里既用了文件读写 Tools,也调用了 MCP 里挂的日历服务拉取参会人名单。Skill 的意义不是取代下面任何一层,而是在它们之上提供"什么时候做、按什么顺序做、做成什么样算好"的决策知识。
1.3 一个对比实验:同样的任务,两种姿势
我做一个过内部小实验,任务很简单:从销售通话录音转写文本里提取客户预算、决策链和时间节点,输出一个结构化表格。
方案 A:把提取规则、字段定义、输出 JSON Schema 全部写进 System Prompt,外加一个"调用大模型提取"的 Tool。 方案 B:把规则写进一个sales-intel/SKILL.md,Agent 看到"销售通话摘要"相关请求时按需加载。
结果方案 A 在 50 条测试集上字段级准确率大概 82%,方案 B 到了 93%。更关键的是迭代成本:方案 A 每改一次规则,要重新跑全量回归看有没有影响其他指令;方案 B 只动那个 Skill 自己的目录,跟 Agent 其他行为完全隔离。这个观察直接促成了我们下面的标准化改造。
2. Skill 的标准长相:目录、描述文件与调用协议
现在开源社区里出现了不少 agent-skills 仓库,各家框架(Claude Skills、Cursor 的 skills 目录、OpenClaw 的自定义技能等)细节略有出入,但骨架已经趋于收敛。我这里写的是我综合多个框架和自定义实践后的一套"通用最小规范"。你直接照这个结构搭,在大部分框架里都能比较平滑地迁移。
my-skill/ ├── SKILL.md # 技能的"说明书+入口",Agent 只加载这个文件 ├── scripts/ # 可执行脚本,python/shell/node 都行 │ ├── preprocess.py │ ├── generate.py │ └── __init__.py ├── assets/ # 非脚本素材:模板、词表、示例文件 │ └── meeting_template.md ├── requirements.txt # Python 依赖 ├── package.json # Node 依赖(可选) └── examples/ # 输入输出示例,给模型做 few-shot ├── input_1.txt └── output_1.md你可能会问:为什么非得是"一个目录"?直接写一段 Markdown 让模型读不就行了?答案在于可分发、可版本化、可组合。目录是一个天然的封装边界,git 可以对其单独打 tag,CI 可以单独跑测试,别人可以 fork 改成自己的版本。这是单体文件给不了的工程能力。
2.1 SKILL.md 的描述规范:人读给机器听的接口
SKILL.md 是整个技能包里最容易被低估的文件。很多人把它当成 README 随便写写,但实际它决定了 Agent 在 0.1 秒内决定"我要不要加载这个技能"的唯一依据。
我建议的头部结构用 YAML frontmatter,核心字段如下:
| 字段 | 必填 | 作用与注意点 |
|---|---|---|
name | 是 | 技能唯一标识,kebab-case,别用空格 |
description | 是 | 一句话说明任务域 + 触发条件。要写"当用户提到……时使用",别写"这是一个会议纪要工具"这种没用的话 |
version | 推荐 | 语义化版本号,方便框架做缓存与冲突检测 |
requires | 否 | 依赖的外部能力,比如mcp: calendar或tool: file_write |
scope | 否 | 技能生效范围:conversation(当前对话)或session(整个会话) |
description 是全文最重要的 2-3 行。因为框架通常会把所有技能的 description 拼在一起做检索索引,写得太泛,Agent 会误加载;写得太窄,该用的时候又找不到。我给团队定了个模板:
当且仅当用户请求涉及 XXX(具体任务域),且提供了 YYY(必要输入条件)时,使用此技能。它完成 ZZZ 目标,输出格式为 QQQ。
这个模板能显著降低误触率。我见过太多人写"Handles meeting minutes",结果 Agent 在用户聊到"帮我约个会"时也把这个技能翻出来,白白消耗上下文。
2.2 正文:给模型看的操作规程
看过一些 SKILL.md 的正文写法,最常见的问题是把它当 README:大篇幅讲"本技能为什么存在""架构设计",模型读完也不知道第一步该干嘛。README 是给开发者的,而 SKILL.md 是给模型的"当场操作规程"。两者的读者完全不同,写法必须分开。
我的正文固定五段式,每段尽量控制在 200-400 字:
- Output Format:先规定输出结构。模型在知道"终点长什么样"之后,执行过程会稳定很多。最好给出一个 JSON/Markdown 模板。
- Steps:编号列出操作流程。每个步骤配一行"成功/失败判据",模型才有自检能力。
- Rules / Don't:列出绝对不能做的事,比如"不要编造字段值""不要修改原始文件"。
- Edge Cases:给出已知的边界情况处理方式。这是神秘感最少的部分,写清楚能省大量返工。
- Examples:直接引用
examples/目录下的样例,让模型看一眼"标准答案"。
一个经验:步骤别超过 7 个。超过之后模型遵循率断崖下跌。如果流程真的复杂,把它拆成多个 Skill 串联,或者写成脚本让 Agent 只负责一个"调用"动作。
2.3 脚本入口与依赖声明
为什么 SKILL.md 里时常需要脚本?因为有些操作模型做不好——比如处理 1 万行 CSV,或者做精确的日期计算。模型擅长的是判断、组织、表达,不擅长的是大规模确定性计算。所以成熟的 Skill 是"模型指挥 + 脚本动手"。
脚本入口我固定叫run.py(或run.sh),约定两种调用形态:
# 形态1:直接执行,参数通过环境变量传入 SKILL_INPUT_FILE="./meeting.txt" python scripts/run.py # 形态2:作为工具注册 # 框架自动把 run.py 的 docstring 解析为工具函数签名依赖声明上有一个容易踩的坑:requirements.txt 里别锁死绝对版本,用范围版本。否则不同 Agent 环境里装依赖会互相打架。我习惯写成pandas>=2.0,<3.0这种,既稳定又不至于冲突到死。
2.4 技能加载机制:白名单和懒加载
大部分框架对 Skills 的加载是"检索式"的:Agent 根据用户当前意图以及技能 description 的相关度,动态决定要不要加载某个 SKILL.md 到上下文。这意味着所有技能的 description 集合本身就是个检索系统,你维护的每一个 description 都会参与全局排序。
我踩过的一个真实教训:某个技能 description 里带了大量名词堆砌("Excel、表格、电子表格、spreadsheet、xlsx、csv……"),导致用户只要提到任何一个词,这个技能就被加载,实际却帮不上忙。后来我把 description 改成"仅当用户希望将数据整理为公司规定的周报格式时使用",误加载率立刻降了下来。
如果你用到的框架支持自定义加载策略,强烈建议做一层白名单。比如在公司内部部署时,我可以把 Agent 的 skills 目录设成"仅允许加载来自受信 git origin 的仓库",从源头上防止未知技能被拉进上下文。
3. 亲手开发一个 Skill:从会议纪要整理到可复用成果物
概念说得再多,不如直接走一遍全流程。我拿我们团队最常用的"会议纪要整理"技能做例子,从需求拆解到验证,完整讲一遍我是怎么建的。你完全可以把它替换成自己领域的场景。
3.1 第一步:先给能力画边界
任何技能开发第一件事不是写代码,而是写"能力边界声明"。我用手工整理会议纪要的真实痛点:
- 痛点 1:转写文本乱,说话人重叠,口语词多。
- 痛点 2:多人提到"这个""那个"指代不明,需要结合上下文才能猜测。
- 痛点 3:有的纪要需要给研发看,有的需要给管理层看,详略要求不同。
基于痛点,我把能力边界定为:输入一段纯文本转写稿,输出三种预设风格的纪要(研发版、管理层版、待办版),不负责判断会议重要性,不负责自动邀约下一次会议。边界一旦清楚,后面所有设计和测试都有锚点。
3.2 第二步:搭骨架和 SKILL.md 核心
我建目录meeting-notes/,先写 SKILL.md 的描述部分:
--- name: meeting-notes description: 仅当用户提供会议转写文本并要求整理会议纪要时使用。根据目标受众输出研发版/管理层版/待办版三种风格,输出为 Markdown。 version: 1.2.0 requires: - tool: file_read - tool: file_write scope: conversation ---正文里的 Steps 部分,我最初写到第 9 步,实测效果很一般。这里我把它精简成 6 步,每一步都加了"判据",效果立刻好了不少:
- 通读转写全文,区分"发言内容"和"寒暄/离题内容";判据:删除寒暄后剩余内容仍保持逻辑连贯。
- 按主题聚类段落,识别最多 5 个主要议题;判据:每个议题有独立标题,且原文本证据可引用。
- 提取每个议题下的明确决策(以"我们决定/就定为/同意"等为信号);判据:每个决策都必须能找到原话支撑,找不到就标"待确认"。
- 提取行动项,格式为"负责人 - 事项 - 截止日期";判据:行动项缺任何一列就向用户追问。
- 根据受众风格生成三种纪要版本;判据:研发版保留技术细节和风险,管理层版只保留结论和影响,待办版只保留行动项。
- 自检:扫描全文是否仍有"这个/那个"等无法回溯的指代,若有则删除并标记"[原文指代不清]";判据:输出中不出现裸指代。
这套步骤恰好踩中了模型容易兴奋发挥的地方:第 3 步的"决策提取"和第 6 步的"自检",是防止幻觉的关键闸门。不加这两步,模型很容易脑补出"会上决定采用某某方案"这种并不存在的结论。
3.3 第三步:脚本、示例与测试数据
这个技能的大部分工作是纯文本处理,我用 Python 脚本做了一个辅助函数:把转写文本按说话人切分并统计各议题的发言时长占比,方便纪要里给出"讨论权重"。
# scripts/analyze.py import json, sys from collections import Counter def extract_segments(raw_text): segments = [] current_speaker, current_buf = None, [] for line in raw_text.splitlines(): if ':' in line[:20]: if current_speaker: segments.append({"speaker": current_speaker, "text": " ".join(current_buf)}) current_speaker, current_buf = line.split(':', 1)[0], [line.split(':', 1)[1]] else: current_buf.append(line) if current_speaker: segments.append({"speaker": current_speaker, "text": " ".join(current_buf)}) return segments if __name__ == "__main__": raw = sys.stdin.read() print(json.dumps(extract_segments(raw), ensure_ascii=False, indent=2))示例数据是关键中的关键。我发现只给模型看文字说明,它生成的细节风格漂移很大;但只要给一对"输入-输出"示例,风格立刻锚定。我的examples/目录里存了 5 组不同场景的示例:产品评审会、周会、客户需求沟通会、技术方案评审、跨部门协调会。每组都是一份真实(脱敏后)转写片段 + 三版完整纪要输出。
测试我用了最笨但有效的办法:跑 20 条真实转写,人工检查每条输出的"决策是否可回溯""行动项是否字段齐全"。没跑过这一步之前,我一直以为模型在提取行动项时很擅长——实际让我很意外:它经常漏掉"口头承诺"型行动项,比如"那我回头看看再回复你"这种,人类一听就知道是个待办,模型往往忽略。这种发现只能靠真实样本暴露,靠写 Prompt 是预想不到的。
3.4 第四步:发布与版本管理
技能写完后,我直接推到独立的 git 仓库,打上v1.0.0tag。团队内部的 Agent 框架配置里维护了一个 skills 源列表,框架启动时按 tag 拉取,并锁校验 hash——防止仓库被改动后静默影响线上行为。
版本管理上一个建议:凡是改了 SKILL.md 里 Steps 或 Rules 的,必须升 minor 版本;凡是只改 examples 的,可以只升 patch。这样你在回滚时能快速判断,某个线上行为变化是因为"指令变了"还是"示例变了"。
4. 接入 Agent 框架时的兼容性与安全边界
写好一个 Skill 只是万里长征第一步,真正让人头疼的是把它接入你正在用的 Agent 框架。不同框架对 Skills 的加载策略、上下文注入方式、脚本执行沙箱都不同。我把几个常见框架的差异和我踩过的坑放在一起说。
4.1 框架差异:调用约定的三个维度
我实际接触比较多的几类实现,差异主要体现在三个维度:
| 维度 | 框架 A(对话产品型) | 框架 B(编码助手型) | 框架 C(自动化任务型) |
|---|---|---|---|
| 技能触发 | 由模型根据 description 自主判断 | 用户/skill-name显式调用或模型调用 | 任务编排器按规则匹配 |
| SKILL.md 加载 | 全文注入上下文 | 按需检索注入 | 预先索引,运行时只注入摘要 |
| 脚本执行 | 沙箱容器,网络隔离 | 本地子进程,权限较大 | 分布式执行,支持并发 |
不用纠结具体对应哪个产品,你要做的是在选型时问自己三个问题:
- 我的技能是"用户主动发起的"还是"Agent 自主判断的"?前者需要框架支持显式调用,后者需要 description 写得好。
- 我的技能是否需要访问本地文件系统、网络、或外部服务?沙箱严格的框架可能跑不了你的脚本。
- 我的技能是"重脚本地"还是"轻脚本地"?如果是重脚本,必须确认框架不会把脚本塞进模型上下文里让模型"读代码"。
4.2 我在编码和路径上踩过的坑
第一个坑:编码。某次我在 Windows 上开发了一个中文处理 Skill,本地跑得好好的,一进 Linux 容器就乱码。排查半天发现是框架的脚本执行环境默认用ASCII解码子进程输出,而我的脚本里print中文报表直接抛异常,异常信息本身也是中文……套娃式乱码。后来的解法是脚本里所有输出强制转 UTF-8:
import sys, io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')第二个坑:路径。框架执行脚本时的工作目录是随机的,常驻目录可能是某个临时目录。永远不要用相对路径读取assets/或examples/。正确姿势是用脚本文件自身的位置推导:
from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parents[1] ASSET_PATH = SKILL_ROOT / "assets" / "meeting_template.md"第三个坑:环境变量传递。不同框架往脚本里传输入的方式天差地别,有的用 stdin,有的用临时文件路径,有的用环境变量。为了保证 Skill 可移植,我在 SKILL.md 里明确规定"脚本只从 stdin 读数据,所有配置走环境变量",并且提供了一个run.sh包裹,统一做参数映射:
#!/usr/bin/env bash # 统一入口:让不同框架都通过同一个接口调用 INPUT_FILE="${SKILL_INPUT_FILE:?need SKILL_INPUT_FILE}" OUTPUT_FILE="${SKILL_OUTPUT_FILE:-/dev/stdout}" python scripts/run.py < "$INPUT_FILE" > "$OUTPUT_FILE"4.3 安全边界:不要让你的 Agent 变成任意命令执行器
这是整个接入过程中我认为最不能妥协的部分。Skill 本质是"让 Agent 执行外部代码",而外部代码的来源如果不受控,就等于把整个 Agent 环境拱手让人。
我给团队定了几条铁律:
- 不受信来源的 Skill 一律进沙箱。至少容器隔离,禁止网络访问,禁止挂载宿主敏感目录。
- Skill 的依赖锁文件必须强制校验。拉取技能仓库时同时校验
requirements.txt的 hash,防止依赖被替换。 - Skill 脚本的权限最小化。在 Linux 下单独建一个没有写权限的系统用户执行;这个用户只有技能目录的可读权限和一个专门的临时输出目录写权限。
- SKILL.md 里禁止写"可以通过任意方式执行系统命令"之类的通用授权。要写清楚脚本的边界,比如"仅允许操作
workspace/下的文件"。
有一回我们内部测试,有人写了个"自动清理日志"的 Skill,因为没做权限最小化,差点在 CI 机器上把构建缓存删了。虽然最后及时拦住,但这个教训直接促成了上面第三、四条规则。
5. 设计取舍:Skill 拆分粒度与组合策略
用 Skills 一阵子后你会发现,真正难的不是写单个 Skill,而是决定"什么应该是一个 Skill,什么应该拆成两个,什么根本不该做成 Skill"。这里没有绝对标准,但有一些值得参考的取舍原则。
5.1 粒度的黄金分割:一个 Skill 一件事
我见过一个把所有数据分析需求全塞进一个>