1. 从“调工具”到“具备技能”:Agent 可靠性的关键转折
我最早做 Agent 项目的时候,踩过一个特别典型的坑:模型明明调用了正确的工具,参数也没传错,但结果就是不对。有一次我让 Agent 去统计某个目录下所有的 Python 文件数量,它调了list_files,然后自己心算了一个答案回来——错的。问题出在哪?不是模型不行,也不是工具不行,而是我给了它一堆“工具”,却没有给它“技能”。
这个区别非常关键。工具是死的,它只提供一个动作入口,比如“读取文件”“发送请求”“执行命令”。而技能是活的,它包含完整的执行路径、边界条件、预期输出,甚至包含这个动作在什么场景下该用、什么场景下不该用。agent-skills 做的事情,就是把后者规范化、结构化、系统化,让 Agent 真正“会做事”,而不是“能调接口”。
我第一次意识到需要这么一套东西,是在一个自动化数据采集的项目里。Agent 需要自己去查资料、解析网页、整理成结构化数据。如果只是把 requests、BeautifulSoup 这类工具丢给它,模型每次调用都要重新“现想”怎么用,不仅 token 消耗大,而且很多边缘情况根本处理不好。后来我把整个流程腌制成一个技能,它有明确的输入 schema、标准化的执行步骤、预置的错误处理出口,Agent 每次只需要传三个参数,正确率直线上升,从原来的六七成直接干到九成以上。
如果有人问 agent-skills 到底解决什么问题,我的回答很简单:它解决的是 Agent 的“无头苍蝇”问题。没有技能库的 Agent,每次任务都像第一次进厨房的新手,连刀怎么握都要想半天;有了技能库,它就是一个熟手厨师,看到食材就知道该走哪个流程。这篇文章适合正在做 Agent 应用、或者准备把 LLM 接入实际业务流程的工程师,我会把整套东西怎么设计、怎么写、怎么用、怎么踩坑,从头到尾说一遍。
2. 技能与工具的本质区别:为什么 Agent 会“选中”但“做错”
要理解 agent-skills 的价值,得先理解一个核心问题:为什么现在的模型调用工具的时候,经常会出现“动作对了但结果错了”的情况。这不是偶然失误,而是架构层面的缺陷。
2.1 工具描述是“名词解释”,技能描述是“使用手册”
大部分工具调用框架里,工具通过一个 JSON Schema 来描述自身——名字、功能描述、参数类型、必填项。模型读出这段描述,决定要不要调用。这个机制的问题在于,给模型的指令颗粒度太粗。
拿“发送 HTTP 请求”这个工具来举例。如果工具描述只写“向指定 URL 发送请求,支持 GET/POST/PUT/DELETE”,模型确实知道它能发请求,但不知道:什么时候该用 GET、请求超时该等多久、返回 403 是重试还是放弃、遇到重定向要不要跟进。这些都是实际操作中必然遇到的分支判断,而模型每次调用都要临时推理一遍。
技能不一样,技能把这段操作里 80% 的判断逻辑预先封装好了。Agent 只需要表达意图,技能负责执行细节。判断什么时候该用这个技能、参数怎么填、返回结果怎么解释,都是技能设计者事先想清楚的事情。
2.2 把“尝试”的负担从模型身上移走
模型调用工具的另一个大问题是“反复试错”。第一次调用失败,它会换一种参数重新尝试,再失败,再尝试……在简单场景下这个还能容忍,但在真实业务里,试错成本很高。一个请求重试 5 次,每次都是完整的一轮 LLM 推理,这笔 token 开销和延迟是很多项目无法接受的。
技能体系解决这个问题的方式是:把容错逻辑固化成代码。请求失败了一次,技能内部自动换重试策略;页面结构变了,技能内部自动走备用解析方案。模型根本不需要“看到”这个失败过程,因为它只对接技能的输出。
2.3 可测试性带来的稳定保障
工具调用的行为像黑盒,模型这次是以一种方式用工具,下次可能是另一种方式。你没法为“模型随机发挥”写测试用例。技能就不一样,既然技能是固化的代码路径,那就可以做单元测试、集成测试、回归测试。
组件有一个很直白的矛盾:模型技术栈在飞速迭代,但你的业务逻辑应该稳定。技能这个设计天然地画了一条分界线——模型负责理解和决策,技能负责执行和交付。决策可以换模型,执行不能随便换,因为执行是要保证稳定的。这条分界线划清楚之后,Agent 项目的工程质量才真正有保障。
3. 技能的边界与架构设计:原子性拆分、元数据、依赖管理
设计一套 agent-skills,最大的难点不是写技能代码本身,而是怎么把大任务拆成一个一个边界清晰的原子技能。拆分得不好,技能之间互相重叠、互相依赖,Agent 反而会被技能列表搞晕。
3.1 原子性:一个技能只做一件事
我见过有人把“数据分析”做成一个技能,里面有 20 个参数,涵盖了从读文件到画图表的全部功能。这种“全能技能”实际上是变回了一堆工具的大杂烩,模型使用起来依然迷茫。
原子技能的正确拆分标准很简单:一个技能应该对应一个不可再分的业务操作。可以按下面四个特征来评估一个技能是否原子:
- 输入参数少于 8 个,超过 8 个说明职责不单一
- 只有一个主要返回结果,不会一次返回 5 种不同类型的数据
- 有明确的失败模式和重试策略,失败不会连带其他技能挂掉
- 技能的描述能在 50 字以内说清楚“什么场景下用它”
我在实际项目中会把技能分成三类:基础技能、组合技能、业务技能。基础技能是“打开文件”“发起请求”这种最底层操作,组合技能是把几个基础技能串成一个流程,业务技能则是绑定具体业务逻辑的高级操作。层级清晰之后,Agent 的选择空间并不乱:日常任务用业务技能,有特殊需求时可以自主组合低层技能。
3.2 元数据设计:让模型“一看就懂”的功夫
技能描述是模型选择技能时的唯一天线,描述写得差,再实现得好的技能也是摆设。我的经验是技能描述不能只写“这个技能做什么”,还必须写“什么时候用它、什么时候别用它、跟相邻技能有什么区别”。
一个成熟的技能描述模板是这样:
技能名称:web_page_extractor 技能用途:抓取指定 URL 的正文内容,过滤导航、广告等无关信息 适用场景:需要获取网页正文、文章内容或页面主要信息时 不适用场景:需要完整的原始 HTML、需要执行页面 JavaScript 后才渲染的页面 相邻区分:与 url_downloader 的区别是,本技能返回清洗后的文本内容,url_downloader 返回原始文件尤其是“相邻区分”这一项,很多技能库都会忽略。但模型最容易困惑的恰恰是两个相似技能该选哪个。你把这层窗户纸捅破了,模型就不会犹豫。
3.3 依赖关系的显式声明
技能之间天然存在依赖关系,高级技能往往要调用低级技能。这个依赖关系必须显式声明,否则 Agent 在使用技能时会出现两个问题:一是模型不知道某个技能需要前置环境,直接用导致失败;二是组合调用时层级混乱,模型既要管流程又要管细节,负担很大。
我在每个技能的元数据里都加了requires字段,声明它依赖哪些前置技能或环境条件。如果条件不满足,Agent 应该先去执行前置技能,或者直接提示用户环境未就绪,而不是徒劳地调用。
4. 构建一套 agent-skills 的实操记录:从需求分析到测试完成
光说不练没意义,这一节分享一下我在一个真实项目里构建技能库的完整过程。项目背景是开发一个内部知识库问答机器人,Agent 需要检索内部文档、提取关键信息、生成答案。
4.1 需求盘点:画出“业务流程脑图”
第一步不是写代码,而是把 Agent 要干的活完整列出来,然后逐层拆解。拿知识库问答机器人来说,拆解之后大概是:
- 接收用户的自然语言问题
- 识别问题的类型(事实型、流程型、操作型)
- 根据类型选择检索策略(全文检索、标题检索、语义检索)
- 从检索结果中提取相关段落
- 将段落组织为回答草稿
- 对回答进行格式化和引用标注
这六步里,哪些是模型天然擅长的,哪些是必须固化的?类型识别和回答组织可以交给模型,但检索策略、段落提取这些环节有明确的规则可循,适合做成技能。
4.2 技能清单与优先级
排优先级有个原则:别追求一次性把技能库做全,先把出现频率最高、失败代价最大的技能做出来,跑通后再扩展。
第一版技能清单我定了五个优先级最高的:
| 优先级 | 技能名称 | 功能描述 | 配套工具 |
|---|---|---|---|
| P0 | doc_retriever | 根据关键词/语义检索内部文档库 | 向量数据库、ES 全文索引 |
| P0 | content_extractor | 从检索到的文档中提取指定主题的关键段落 | PDF 解析器、HTML 解析器 |
| P1 | citation_formatter | 为回答内容自动生成引用标注 | 自研格式化函数 |
| P1 | query_expander | 对用户自然语言问题做同义扩展 | LLM 二次调用 |
| P2 | doc_summarizer | 对长文档生成层级摘要 | LLM 调用 + 分段策略 |
P0 的技能必须在第一天就稳定,因为它们直接影响核心流程。P1 是体验增强,P2 放到后期迭代。
4.3 技能实现的结构规范
每个技能我都要求统一结构,方便后续维护和模型理解。一个完整的技能定义文件长这样:
{ "name": "doc_retriever", "description": "在内部文档库中检索与查询最相关的文档片段,支持语义检索与关键词检索混合模式", "when_to_use": "用户提问涉及内部规范、流程文件、产品文档等场景", "when_not_to_use": "用户询问的是常识性问题或代码调试问题", "similar_skills": ["web_searcher", "codebase_searcher"], "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "检索查询内容"}, "top_k": {"type": "integer", "description": "返回文档片段数量", "default": 5}, "search_mode": {"type": "string", "enum": ["hybrid", "semantic", "keyword"], "default": "hybrid"} }, "required": ["query"] }, "output_schema": { "type": "object", "properties": { "fragments": {"type": "array", "items": {"type": "object"}}, "total_found": {"type": "integer"} } }, "execution_plan": [ "1. 解析查询内容,提取核心关键词", "2. 使用语义检索获取向量相似度最高的候选集", "3. 使用关键词检索获取命中候选集", "4. 合并候选集并去重,按综合相关性排序", "5. 截断超过窗口长度的内容,返回前 top_k 个结果" ], "error_handling": { "retry_strategy": "首次查询失败后,降低语义检索阈值重试一次", "fallback": "检索结果为空时,返回提示信息并建议用户更换关键词" }, "requires": ["vector_db_client", "es_client"] }这个结构其实就是一个“技能的技能说明”——不只是给代码调用方看的,更是给 Agent 的模型看的。执行计划这一项很多人会忽略,觉得模型反正是要自己推理的,你写不写它都会做。其实不是,模型在读取技能元数据时,如果你把执行步骤写清楚了,它的决策负担会大幅降低,同时也降低了它自由发挥跑偏的概率。
4.4 技能测试:比工具调用多一层验证
技能测试不能只看“调通了没有”,还要看“在不同输入下表现是否稳定”。我会为每个技能编写三组测试用例:正常场景、边界场景、异常场景。
正常场景,输入标准参数,验证返回结果符合预期。边界场景,比如空字符串查询、超长文本、不存在的文件路径。异常场景,比如下游服务超时、返回数据格式异常。
这三组用例跑完之后,再加一组和模型搭配的端到端测试。因为技能最终是给模型用的,模型对技能的描述理解得对不对、参数填得准不准,都要通过实测来验证。端到端测试里经常发现的问题是:模型的参数格式与技能预期的 schema 对不上。这种问题只有实际跑一遍才能暴露出来,静态检查根本找不到。
5. 将技能库接入 Agent 主循环:路由、选择与错误传播
技能库建好只是第一步,真正决定成败的是怎么把它接入 Agent 的主循环。这一节分享我在接入过程中的踩坑和经验。
5.1 挂载方式的选择
接入技能库有三种常见方式:
- 方式一:把技能当作普通工具挂载,所有技能平铺在工具列表里。简单直接,但技能数量一多,模型选择时注意力会被稀释。
- 方式二:引入一个“技能路由器”,模型先看到分类列表,选完类目再选具体技能。路径变长,但是每层可选项少,准确率会提升。
- 方式三:动态挂载,根据任务上下文提前用检索方式召回相关的 3~5 个技能,只把这几个技能暴露给模型。这个方式我目前最推荐。
方式三本质上是用一个技能检索器来决定本次对话给模型看哪些技能。用户输入问题后,系统先用 embedding 做一次相似度检索,把候选技能列表缩小到 5 个以内,再连同用户的原始问题一起传给模型,让它在这些技能中做选择。
路由的粒度可以调节,技能少的项目用方式一就行,超出 20 个技能就开始考虑方式三。这个临界值没有严格标准,但 20 个之后模型在长列表里选对技能的概率会显著下降。
5.2 技能选择的“拒答路径”
技能选择问题有一个经常被忽略的细节:如果在路由阶段检索不到相关技能,Agent 应该怎么办?很多实现会强迫模型随便选一个,结果就是答非所问。
我的做法是引入一个 reject 出口。技能候选列表为空,或者相关性分数低于阈值时,Agent 会直接告知用户“当前能力范围内无法处理该请求”,并附带建议。这个设计一开始做会觉得是在“浪费机会”,但实际使用中避免了很多不该发生的幻觉输出。
5.3 错误传播的边界
技能内部处理了大部分异常,但免不了有异常需要向上层传播。传播时需要记住一个原则:传给模型的是结构化错误码,而不是裸异常信息。
裸异常信息经常包含堆栈追踪、内部路径、SQL 语句等细节,模型翻译成用户能看懂的话时会添油加醋。结构化错误码就好得多,比如ERROR_404_DOC_NOT_FOUND对应“未找到请求的文档”,模型只需要把这个原因翻译给用户即可,不需要了解底层的技术细节。
6. 技能召回与存储优化:技能库大了之后怎么办
技能数量从十几个涨到七八十个的时候,一个新的问题浮出水面:Agent 怎么在这么多技能里找到正确的那一个?这就像图书馆里的书多了之后,怎么快速找到需要的那本一样,需要索引和分类体系。
6.1 技能描述的检索友好化
技能描述不仅影响模型阅读,还影响检索。因为技能选择的核心机制是 embedding 相似度匹配,描述的措辞直接决定了匹配效果。
一个技巧是给技能配置多条“唤起语”。例如一个负责 PDF 转文本的技能,描述里除了写“PDF 解析”,还可以写上“读取 PDF 文件”“提取 PDF 内容”“把 PDF 变成文本”这些用户可能使用的表述。模型不会在乎你写得啰嗦,它只在乎能不能搜得到。
6.2 聚类与层级管理
技能数量多了以后,平铺的效果一定不好。我开始按业务域对技能做聚类,比如“文档处理域”“网络请求域”“数据处理域”“系统交互域”,每个域有一个域描述。用户在提问时,系统先匹配域,再在域内匹配具体技能。这个两段式匹配跟前面说的方式二类似,但在技能量大时是必需品。
每个域的技术支持也要维护一个使用统计,某个域的技能完用率长期低于阈值,就要反思是技能本身设计问题还是描述问题,是否应该下线或合并。
6.3 反馈闭环:让技能库越用越顺
技能库不是上线之后就冻结的,它需要根据 Agent 的表现持续迭代。我在项目中建立了一个最简单有效的反馈机制:给每次技能调用附加两个指标——成功还是失败、模型是否需要矫正后手动重试。
周会后统计一次,发现某类技能失败率偏高,就优先排查。比如有一次我发现content_extractor的失败率一周内从 5% 涨到 25%,排查后发现是上游文档系统的页面结构改了。这类问题如果没反馈闭环,可能要等到用户投诉才知道。
技能库的优化方向很多时候不是加新技能,而是调整已有技能的边界和容错。特别是当你发现模型反复选择一个技能但执行结果不理想时,问题大概率不在模型而在技能本身的设计。
7. 实测中的意外情况与我的最终建议
项目上线之后,我记录了三个月里踩过的所有坑,再回头看,最值得提醒后来者的其实不是技术上的难题,而是一些很容易被低估的细节。
7.1 两个最容易被忽略的坑
第一个坑是技能描述里的“不适用场景”写得太含糊或者漏写。模型把技能用错场景的情况,一半以上就是因为描述里没写清楚边界。写“不适用场景”不是走过场,它切实地帮模型节省了在错误方向上试探的时间。
第二个坑是技能的执行计划写得过于笼统。执行计划千万别只写“调用外部 API 获取数据”这种废话,要把关键决策点写清楚,比如超时时间、重试策略、什么情况下中断执行。模型在调用技能时确实会读取这段信息,它知道得越具体,自主发挥的余地就越小,稳定性就越高。
7.2 给小团队的建议
如果你的团队只有一两个人,不用一口气建一个几十技能的库。先认真梳理核心业务链路,挑两三个最高频、最有重复价值的技能建好,跑通之后再慢慢扩展。技能库是活的东西,随着业务和模型能力的演进,它需要不断维护和重构。
最后一点,千万别把技能做成“一次性脚手架”。技能和模型的边界要划清楚:模型是会变的,今天用 GPT-4,明天可能换国产模型,技能体系如果绑定在特定模型的能力假设上,换模型时就要全部重写。做技能的时候一定要默认模型是个“笨但听话的执行者”,把逻辑都放在技能里,而不是期待模型能自己兜底。