过去几个月我一直在折腾agent-skills这件事——不是简单给 Agent 塞几个函数,而是真正把“技能”当成一套独立体系来设计和维护。如果你正在做 AI Agent 类应用,或者打算给大模型配一套可以复用的能力层,这篇内容应该能帮你少踩几个坑。
先说清楚这东西是什么。agent-skills可以理解成 Agent 的“技能库”,它不是一堆散装工具函数,而是一套有组织、有规范、可被发现、可被调用的能力集合。它的核心价值是让大模型不用靠提示词硬写逻辑,而是像人类打开工具箱一样,按需拿起对应的工具完成具体任务。适合的人群很明确:做 Agent 应用开发的工程师、做 AI 产品原型验证的技术负责人、以及想把多智能体协作落到实处的团队。
我下面讲的,是从零搭一个技能体系时真正会碰到的设计问题、落地细节和调试经验,都是实操过才总结出来的东西。
1. 技能体系整体思路:为什么 Agent 需要“技能”而不是“工具”
1.1 从工具到技能:一次抽象层级的跃迁
很多人第一次接触agent-skills时会问:这不就是给 Agent 封装一批 API 吗?函数调用、工具调用,OpenAI 早就支持了。这话对了一半。工具调用确实解决“让模型能调外部函数”的问题,但技能体系解决的是另一个层级的问题:怎么把一堆工具组织成一套可治理、可复用、可解释的能力系统。
我打一个生活化的比方。工具调用像是你给一个新手厨师递了一把菜刀,他会用,但不知道该在什么菜式上用、切完怎么保养、什么时候该换刀。而技能体系是给他一本菜谱,上面写着“这道菜用刀方式”“处理食材的步骤”“成品标准”。技能不仅是“能做什么”,还包含了“什么时候做”“怎么做更好”“做完了怎么验证”。
所以agent-skills的核心抽象,是从“函数”升级到了“能力单元”。一个技能单元,不只是代码实现,还包含描述文档、输入输出规范、调用条件、错误处理方式,甚至包含使用这个技能的注意事项。模型在选择技能时,读的不是函数签名,而是一份它“看得懂”的说明书。
1.2 技能系统要解决的三类问题
我拆解需求后,把技能体系的目标收敛成三类问题。
第一类是复用性问题。同一个能力,比如“网页内容抓取”“数据库查询”“文件格式转换”,可能被多个 Agent、多个流程反复使用。如果每次都在提示词里重新描述一遍,或者每个 Agent 各自实现一套,维护成本会指数级上升。技能库把公共能力收拢,一处实现,处处可用。
第二类是可组合性问题。简单技能可以拼成复杂技能。例如“收集竞品信息”这个高级技能,内部要组合“搜索”“抓取网页”“提取正文”“结构化输出”这几个基础技能。技能体系必须有清晰的输入输出契约,才能支持这种嵌套和编排。
第三类是可控性问题。这是最容易忽略的。给 Agent 太多自由发挥的空间,它就会给你“自由发挥”出事故。技能体系通过显式的参数定义、权限边界、描述约束和错误返回结构,让模型的行为范围被限定在可控域内。没有技能的裸 Agent,像是一个没有操作手册的实习生;有技能体系的 Agent,才像是经过培训后可以独立上岗的员工。
1.3 设计取舍:为什么我选择“目录即技能库”
在设计技能体系的存储结构时,我对比了几种方案:数据库存储、纯代码注册、目录文件约定。最终选择了“目录即技能库”的约定式方案。
原因有三点。第一,可观测性好。目录结构天然就是可视的,一个skills/文件夹里有哪些技能,一目了然,团队成员可以像读 README 一样了解整个能力地图。第二,易于版本控制。技能本质上是代码加文档的组合,Git 天然适合做这类内容的版本管理,每次改动技能逻辑、调整描述文本,都能留下完整的 diff 历史。第三,部署成本低。技能库不依赖额外服务,Agent 启动时扫描目录即可加载,减小运维负担。
当然,这个方案也有代价。技能多了以后,目录层级变深,查找效率会下降;技能之间的依赖描述不够显式,可能产生隐式耦合。所以我又引入了命名空间和索引文件来解决这两个副作用,后面会详细讲。
2. 核心细节:技能文件结构与描述规范
2.1 SKILL.md 是一份“给 AI 看的产品说明书”
技能描述是整个体系中最重要的单点文件。它不是写给人看的文档,而是写给模型看的产品说明书。模型通过它来决定:这个技能是干什么的、什么时候该调用、参数怎么填、返回结果是什么意思。
我沿用的规范是每个技能目录下放一个SKILL.md,文件头部用 YAML 定义元信息,正文用 Markdown 描述使用方式。典型的元信息长这样:
name: web_search version: 1.2.0 description: 搜索互联网获取最新信息,返回搜索结果列表。当需要了解实时信息、最新新闻、具体事实时使用。 permission: network timeout: 30这里的description字段几乎决定技能被选中的概率和准确率。写得太宽泛,模型会在不需要的时候调用;写得太狭窄,模型又找不到它。我试验下来的经验是:描述里要包含“能力边界”和“适用场景触发词”,比如“当用户提到新闻、最新、实时、搜索时,优先考虑”。
正文部分要讲清楚三件事:技能执行流程、参数说明和示例、返回值与错误码。把这些写全了,模型才能正确使用技能,而不是靠猜。
2.2 参数与返回值设计的六个原则
参数设计直接决定模型能不能正确调用技能。模型是按照参数名和类型描述来填参的,如果参数定义不清晰,就会出现模型传错参数、漏传必填项、类型不对等问题。我把参数设计的经验收敛成六条原则,每次定义新技能时就拿这六条过一遍。
第一条,参数名要让模型“望文生义”。不要用a、b、data这种缩写,改用query、file_path、max_results这种自解释命名。模型不是编译器,不会去读你的源码注释,参数名就是它的第一信息来源。
第二条,给每个参数补充语义描述。类型定义只说“string”对模型帮助不大,要说明这个字符串应该包含什么。例如:
{ "query": { "type": "string", "description": "搜索关键词,可以是一个短语,建议不超过20个字" }, "max_results": { "type": "integer", "description": "返回结果数量上限,默认5,最大20" } }第三条,坚持“必填参数最少化”。能设置默认值的都设置默认值,减少模型决策负担。模型只有在必须做选择的时候才做选择,否则会因犹豫而产生错误调用。
第四条,返回值尽量结构化。不要返回一段自由文本让模型自己解析,而是返回 JSON 对象,字段名清晰,枚举值固定。这能显著降低模型后续处理的不确定性。
第五条,错误返回与正常返回使用同一结构。错误时把status置为error,并附error_code和message。模型对固定结构的学习成本远低于处理各种异常格式的成本。
第六条,每个技能都要有明确的“不做清单”。在描述里写明“本技能不处理XX,不访问XX”,相当于给模型划出安全边界。模型在边界模糊时倾向于调用技能,边界清晰时反而会更谨慎。
2.3 技能命名与隔离策略
技能命名看似小事,实际对调用效果影响很大。我早期踩过的一个坑是:所有技能都平铺在 skills 根目录下,结果模型在选择时经常混淆两个相似技能。后来我引入了命名空间隔离,按业务域划分子目录。
skills/ search/ web_search/ news_search/ content/ extract_article/ generate_summary/ data/ sql_query/ csv_convert/命名空间的好处有两个。第一,从结构上强制按业务域组织,避免技能无限堆积在一个平面空间里,难以管理。第二,可以在描述引用时加上命名空间前缀,例如“search.web_search”,让模型对不同领域的技能形成更强的区分意识。
隔离策略还包括权限隔离。技能文件里声明permission字段,例如network、file_read、file_write、db_readonly等。Agent 运行时按权限清单校验,防止一个技能越权访问敏感资源。这个设计在团队协作场景下尤其重要——不同角色生成的技能,互不越界。
3. 实操落地:从零搭建一个可运行的技能库
3.1 最小技能库目录结构
下面我从零演示一遍搭建流程。先建一个最小可运行的技能库,包含一个技能:date_time,作用是获取当前日期和时间。这个技能实现简单但没有技术含量,用来跑通整个流程再合适不过。
agent-skills/ skills/ date_time/ SKILL.md implementation.py index.yaml README.mdimplementation.py是最简实现,提供查询日期时间的能力:
from datetime import datetime from typing import Dict def run() -> Dict: now = datetime.now() return { "status": "success", "date": now.strftime("%Y-%m-%d"), "time": now.strftime("%H:%M:%S"), "timezone": "local" }技能实现本身没什么可讲,核心在SKILL.md里。布局是否合理、描述是否准确,决定了模型能不能正确调用这个技能。
3.2 技能发现机制:让 Agent 真正找到技能
技能库放到目录里,Agent 要能发现它。这一步,很多初学者会犯一个共同错误:把技能描述直接全量塞进系统提示词里。技能少的时候可以,技能一多,上下文就爆炸,模型会丢失注意力。
正确的做法是分层加载。Agent 启动时先扫描索引文件index.yaml,生成一个轻量技能注册表,每个技能只保留技能名和一句话摘要,占用的 token 极少。
skills: - name: date_time description: 获取当前日期时间,当需要知道当前时间、日期时使用。 path: skills/date_time - name: web_search description: 搜索互联网获取最新信息,当需要实时信息时使用。 path: skills/web_search真正运行时,模型先根据注册表判断该调用哪个技能,再按路径加载那个技能对应的SKILL.md全文,拿到参数细节。这种两段式发现机制,避免了把全部技能细节一次性塞给模型,既省钱又省注意力。
我测试下来的经验是:注册表里每个技能摘要控制在 60 字以内,技能库 100 个技能时,索引占用约 2K token,完全可接受。加载单个技能完整描述后,上下文增加约 300~500 token,对模型性能影响很小。
3.3 运行时调度:提示词模板与技能执行
技能发现只是第一步,真正的难点是让模型在正确的时机把技能调用出来。我使用一个固定的提示词模板来约束调用行为,模板里只做必要说明,不给模型自由发挥的空间。
你是一个拥有技能库的智能助手。你可以调用以下技能: {{skills_index}} 当遇到需要技能才能完成的问题时,按步骤执行: 1. 从技能索引中选择最合适的技能名 2. 将结果以 JSON 格式输出,格式如下: { "skill": "date_time", "arguments": {} } 调用结果返回给你后,结合结果回答用户问题。这个模板的关键点是:只告诉模型可以按格式调用技能,不把技能实现细节暴露出来。细节暴露越多,模型越容易在提示词里“表演”执行过程,而不真正触发调用。
执行时,Agent 运行时收到模型的 JSON 输出后,将参数交给implementation.py执行,再把返回值作为新的上下文片段传回给模型。这一步我踩过一个大坑:技能返回结果如果太长,会挤占模型注意力。解决方案是加一个摘要预处理环节,技能返回超过一定长度时先做摘要再回传模型。常见长度阈值是 3000 字符,超过则自动摘要。
3.4 让 Agent 学会“组装”技能的编排层
当技能数量上来后,单一技能调用就不够用了。真实业务里有大量任务需要多个技能协作完成,例如“整理今天的热点新闻并生成摘要”,底层要调用“搜索新闻”“抓取正文”“文本摘要”三个技能。如果让模型自己逐个调度,中间任何一步理解偏差都可能导致链条断裂。
我的做法是引入一个轻量编排层,用显式的工作流语言或者简单的 JSON 步骤数组来定义技能组合。编排层不依赖模型自由发挥,而是把任务拆成固定步骤,每一步调用约定技能,上一步输出作为下一步输入。
{ "workflow": "news_digest", "steps": [ { "skill": "search.news_search", "args": {"query": "今日热点", "limit": 5} }, { "skill": "content.extract_article", "args": {"url": "{{steps[0].results[0].url}}"} }, { "skill": "content.generate_summary", "args": {"text": "{{steps[1].content}}"} } ] }这种编排方式既保留了技能的独立性,又让组合变得可控。我建议做 Agent 产品时,基础技能尽量原子化,粒度控制在“一次输入、一次输出、一个明确目标”,这样编排层的可组合性才强。
4. 常见问题与排查实录
4.1 技能“找不到”和“不想用”
我最初调试时遇到最频繁的问题,是模型明明有技能可用,却不用,或者选了错的技能。排查思路是:先看技能的description与用户问题的匹配度。
案例:当时我写了一个查询天气的技能,描述是“获取指定城市的天气数据”。模型遇到“北京今天出门要不要带伞”这种问题时,总是调不到天气技能。后来我改了描述,写成“当用户询问天气、降雨、温度、穿衣建议、出行准备时使用,参数为城市名称”,调用准确率立刻上来了。
这里有个底层逻辑:模型选择技能依靠语义相关度,描述里的每个关键词都是一条语义索引。索引覆盖不到,技能就等于“不存在”。所以描述一定要用口语化、多样化的表达来包裹,让模型从不同角度都“想得起”这个技能。
4.2 参数幻觉与技能误调用
参数幻觉指的是模型凭空捏造参数值。典型表现是:技能要求传入city,模型在用户没提城市时自作主张填了“北京”。
这类问题我在日志里反复观察过,本质原因是模型不确定某些信息是否可以从上下文推断,于是默认填一个“最可能”的值。解决思路是给参数加大括号约束——在参数定义里显式标注“不能由模型推测的内容,缺失时必须询问用户”。
另外,运行时要做参数合法性校验,这不能省。我在技能执行前加了一层 schema 校验,字段必填性、类型、枚举值、长度限制都卡一遍,不通过就返回明确的错误码,不让错误输入继续传播下去。
4.3 技能互相干扰与依赖地狱
技能多起来后,会出现两个技能描述相似导致模型选错,或者技能 A 依赖技能 B,但 B 没被加载。前者是描述冲突问题,后者是依赖管理问题。
描述冲突我通过两个手段解决。第一,引入命名空间前缀;第二,在技能描述里加“排除语句”——写上“本技能不处理XX,请求XX请使用其他技能”。这让模型在选择时有了更清晰的判别依据。
依赖管理上,每个技能的SKILL.md元信息里加一个depends字段,列出依赖的其他技能。运行时加载技能时自动递归加载依赖,避免模型只拿到单个技能、缺依赖无法执行的情况。
4.4 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调用任何技能 | 技能摘要未进入索引 | 检查index.yaml是否包含该技能 |
| 模型总是选错技能 | description描述与用户问题语义不匹配 | 扩充描述触发词,加排除语句 |
| 模型调用成功但结果错误 | 参数定义不清晰或缺少校验 | 检查参数 description、补 schema 校验 |
| 技能返回结果过长影响回答质量 | 未做返回长度控制 | 加摘要预处理或者 truncate |
| 技能 A 依赖 B 但 B 报错 | B 未注册或未加载 | 用depends字段递归加载依赖 |
| 相同请求结果不稳定 | 模型随机性导致选择漂移 | 降低温度参数,或将选择逻辑改成规则匹配 |
这些问题是逐步暴露出来的,我每解决一个就沉淀成一条经验。技能体系不是写完就完事,它更像一个生物群落,需要持续维护和修剪。
5. 避坑技巧与扩展方向
5.1 给技能写“自测用例”
技能不只是给模型用的,也是给开发者用的。每次写完一个技能,我都会写一个独立的自测脚本,直接调用技能实现并打印输出,绕开模型环节。
这一步看似简单,但价值极高。很多技能问题其实是技能实现自身的问题,如果每次都通过对话来排查,既慢又不稳定。直接调用实现函数,可以用固定输入验证输出格式,确认无异常后再回到 Agent 环境里测试调用。实测下来,这个流程能过滤掉百分之六七十的“假 Agent 问题”。
自测脚本我推荐放在技能目录下,命名test.py,用标准库里的unittest或者简单的断言即可,不用引额外框架。
5.2 技能版本与灰度演进
技能库上线后,必然面临迭代。我踩过一个教训:某次更新技能逻辑后,所有依赖该技能的工作流出现连锁报错。问题就出在没有版本管理。
我的方案是给技能目录做版本快照,更新逻辑前先复制一份带版本名的旧目录,再修改新版本。例如skills/web_search/v1.2.0/这种层级,由目录结构表达版本状态。同时读一遍SKILL.md里的version字段,升级时更新语义版本号,保证工作流引用能锁定版本。
灰度发布时也不用退回旧版。新建一个技能目录引用新版本,在老工作流里继续用旧版,新流程用新版,并行观察几轮再切换。
5.3 技能评测与多维质量把关
评价一个技能好不好,不能只看它能不能被调用,要看调用后的产出质量。我建立了一套轻量评测流程:为每个技能准备一组典型测试问题,跑固定次数,统计三件事——调用命中率、参数正确率、结果可用率。
这三率和技能描述质量高度相关。命中率低,多半是描述触发词不够;参数正确率低,多半是参数描述和默认值设置不合理;结果可用率低,多半是实现逻辑或输出格式有问题。
在团队协作场景里,这套评测尤其重要。因为不同人写的技能风格参差,有了统一指标,评审和验收才有据可依。
def evaluate(skill_name, test_cases, agent_runner): hit = 0 param_ok = 0 result_ok = 0 for case in test_cases: response = agent_runner.run(case["question"]) if response.get("skill") == skill_name: hit += 1 if response.get("arguments") == case["expected_args"]: param_ok += 1 if case.get("expected_result") in str(response.get("result", "")): result_ok += 1 total = len(test_cases) return { "hit_rate": hit / total, "param_rate": param_ok / total, "result_rate": result_ok / total }我实测过,对一个中等复杂度的技能,人工调参描述文本带来的命中率提升通常能达到两成左右,这是一个非常值得投入的优化方向。
5.4 多 Agent 协作下的技能共享
最后说一个我近期在摸索的方向:多 Agent 场景下的技能共享。
单个 Agent 用技能库是单机玩法,多 Agent 协作时,技能库就变成了一个需要治理的公共服务。多个 Agent 共享同一套技能,意味着技能可能被并发调用,要处理资源争抢和权限控制;也意味着某个 Agent 对技能的错误使用,可能污染共享状态。
我的建议是在架构上把技能库做成独立服务,Agent 通过接口调用技能而不是直接 load 本地代码。这样技能执行可以加锁、加配额、加审计日志,谁调用了什么技能、传了什么参数、结果是什么,全部留痕。出了问题能追溯,迭代时有依据。
这一层推演下去还会涉及非常多的工程细节,但原则是清晰的:技能数量少时可以靠约定,技能数量多了必须靠治理。
最后再分享一个我实际调试中的体会:技能体系设计得再好,也不如把每个技能的描述当成一个“产品说明”去认真打磨。模型看不懂它,它就不存在;模型看懂了但被误导,它比不存在还危险。所以每次写完技能,回到日志里看几轮真实调用记录,调整描述措辞和参数边界,比去研究更复杂的提示词工程要有效得多。