ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用技能库重构Agent工具调用:SKILL.md实战与准确率提升指南

用技能库重构Agent工具调用:SKILL.md实战与准确率提升指南 1. 先说我为什么从挂工具转成搭技能库1.1 一次翻车现场模型把天气工具用去查订单大概在半年前我第一次正儿八经做客服 Agent当时的思路很直接给大模型挂上十几个 API 工具让它自己选着调。听起来没问题实际一上线就崩。用户问我的订单什么时候到模型调了查天气的接口用户问你们退货政策是啥模型调了订单查询接口然后一本正经地编了一段退货政策出来。我一开始也骂模型蠢后来把调用日志翻出来一条条看发现问题不在模型在我。当时的工具描述就一行字比如Get weather、Query order参数也写得极其随意——字段叫什么、什么格式、必填不填全靠模型猜。模型在一个充满歧义的工具菜单里做选择选错其实是大概率事件选对才是运气好。那一刻我意识到工具调用这件事真正缺的不是 API是给每个能力写一份说得清什么时候用、怎么用、什么时候别用的说明书。1.2 技能库的本质把能力列表升级成使用契约后来我参考了社区里 Agent Skills 的思路把整个工具层重构了一遍做成了一套叫 agent-skills 的体系。核心变化有三点。第一每个能力独立成一个技能目录目录里有说明书SKILL.md和可执行脚本不再是一堆散装的 JSON Schema 丢给模型。第二说明书里不只写这技能是干嘛的还明确写什么时候用什么时候绝不能用具体执行步骤是什么。这相当于和模型签了一份使用契约把歧义摁死。第三所有技能统一注册、统一加载路由时先生成结构化调用意图再执行脚本。执行结果回传后模型基于真实结果继续回答而不是自己脑补。这套重构上线之后同一个场景下工具选择准确率从大概 71% 提到了 94%最直观的变化是模型不再拿查天气的去查订单了。这篇文章我想把整个 agent-skills 的搭建过程、设计思路和踩过的坑完整写出来。如果你正在做 Agent 开发被工具调用不稳定、模型乱选工具、参数瞎传这些问题折磨过这篇文章大概率能帮你少走两周弯路。1.3 适合谁看不适合谁看先做个读者定位。这套方法适合下面几类人正在用 OpenAI Function Calling、Claude Tool Use 或类似机制做 Agent 的开发者手头工具数量超过 5 个、已经开始出现模型选错工具的团队想给自己的 Agent 接入私有 API 或内部系统但不知道该怎么组织这部分能力的人不太适合的情况也有如果你的 Agent 只调一个工具或者所有调用都是写死在代码里的流程编排那技能库这套东西暂时帮不到你。等你的 Agent 需要自主决定调什么的时候再回来翻这篇不迟。2. SKILL.md 怎么写模型才不会误解你的意图2.1 一个技能的完整骨架一套技能本质上就是一个目录加一份说明书。目录结构我用了最朴素的约定skills/ search_docs/ SKILL.md search_docs.py query_sql/ SKILL.md query_sql.py calc/ SKILL.md calc.py每个技能目录下都有一份 SKILL.md这是整个体系里最重要的文件。它的 YAML frontmatter 长这样--- name: search_docs description: 在本地知识库中检索资料返回与问题最相关的文档片段 when_to_use: 用户询问产品使用、操作手册、公司制度、FAQ 等静态知识 when_not_to_use: 用户询问实时数据天气、股价、新闻或要求进行数学计算 version: 1.2.0 ---frontmatter 下面才是正文正文用枚举步骤把执行流程写清楚# 技能本地文档检索 1. 从用户问题中提取 2-5 个关键词去掉停用词 2. 调用 search_docs.py传入 query 和 limit 参数 3. 如果结果为空改写关键词后重试一次 4. 返回结果前用一句话概括每个命中文档的核心结论这个结构的好处是既给模型读了何时用的语义描述也给了它怎么执行的操作序列。模型读完后不是瞎调工具而是按你的步骤一步步走。2.2 正向描述人人会写负向约束才是分水岭很多教程都会教你写 description但 90% 的人写出来的 description 只有半句话比如用于文档搜索。这种写法有一个致命问题模型对文档的理解太宽了。用户说帮我看看这个 PDF 里写了啥模型可能觉得这也是文档搜索于是把 search_docs 调起来。但实际上那个 PDF 是用户上传的本地知识库里根本没有。我后来在技能描述里加了一个之前完全没意识到重要性的字段——when_not_to_use。效果非常明显。拿 search_docs 举例。最初版本只有在本地知识库中检索文档模型命中准确率只有七成。加了负向约束之后错误调用率直接降了一半。因为负向约束给了模型一个排除法的依据如果任务不属于这些场景就不应该选它。所以我的建议是每个技能的 description 里至少用一句话说明什么时候不要用它。这一句话比你在正向描述里多写三行废话有用得多。2.3 描述的关键词排布决定匹配质量还有一个容易被忽略的细节技能的 description 不是给你看的是给模型的语义匹配看的。模型做工具选择时会把用户意图和技能描述做语义相似度计算所以描述里的关键词排布直接决定了匹配质量。我踩过一个挺典型的坑。当时做了两个技能一个是query_order查订单状态一个是query_sql查数据库。前者的描述写的是查询订单状态、物流进展、售后进度后者的描述写的是执行 SQL 查询数据库表格数据。表面看没什么问题但实际上线后发现用户说帮我查一下数据库里有多少订单模型经常会选 query_order因为它看到了订单两个字。这些技能描述之间出现语义重叠时模型就像站在岔路口很容易选错。后来我在 query_sql 的描述里加了本技能直接读取业务数据库用于数据分析与统计不返回单个订单详情在 query_order 的描述里加了只查具体订单不做聚合统计。两个边界一划清楚选择准确率马上就上来了。写技能描述时的几条经验描述里必须点出技能的输入场景而不是只说功能名字如果两个技能服务同一类用户请求必须把差异点显式写进描述关键词要放在描述开头因为截断或注意力衰减可能让后半句失效3. 技能库的运行架构注册、路由、执行3.1 加载器把目录变成模型能看的 tools 列表光有说明书还不够得让代码能把说明书读进来转成模型能识别的工具 Schema。我写了个能力加载器每次 Agent 启动时扫描整个 skills 目录from pathlib import Path from typing import Any SKILLS_ROOT Path(./skills) def load_skills(root: Path) - list[dict[str, Any]]: skills [] for skill_dir in root.iterdir(): md_path skill_dir / SKILL.md if not md_path.exists(): continue meta parse_frontmatter(md_path) skills.append({ name: meta[name], description: build_description(meta), parameters: json.loads((skill_dir / schema.json).read_text()), runnable: skill_dir / f{meta[name]}.py, version: meta.get(version, 0.0.1), }) return skills关键在 build_description。我会把 frontmatter 里的 description、when_to_use、when_not_to_use 拼接成一个完整的描述文本。这样模型看到的 description 就不再是一句话而是一段包含正反向约束的信息def build_description(meta: dict) - str: desc f{meta[description]}。适用场景{meta[when_to_use]}。 if meta.get(when_not_to_use): desc f不适用场景{meta[when_not_to_use]}。 return desc每个技能的参数定义单独放在 schema.json 里加载器会把它转成模型的 function 参数结构。这个做法的好处是技能的新增和下线完全不需要改主程序代码往 skills 目录里丢一个新文件夹就完事了。3.2 两步路由先选技能再执行技能这是 agent-skills 里最关键的一个设计。很多 Agent 的做法是直接把所有工具函数一股脑传给模型让它在一个大列表里挑。工具少的时候没问题工具一多模型的选择质量就断崖式下跌。我的做法是拆成两步。第一步让模型在技能列表里做选择这一步只输出技能名和参数不执行任何代码。如果模型觉得所有技能都不合适允许它不选——直接回复用户而不是硬调一个不相关的技能。第二步根据模型给出的技能名在注册表里定位到对应脚本用子进程或独立容器执行然后把真实结果回传给模型由模型基于结果组织最终回答。代码层面大概是这个意思# 第一步模型选技能 response model.call( messagesconversation, tools[skill[tool_schema] for skill in skills], ) if not response.tool_calls: return reply_without_tool(response) selected response.tool_calls[0] skill_map {s[name]: s for s in skills} skill skill_map.get(selected.name) if skill is None: return 抱歉该操作当前不可用 # 第二步执行技能脚本 result subprocess.run( [sys.executable, skill[runnable], *args_to_cli(selected.arguments)], capture_outputTrue, timeout30, ) # 第三步真实结果回传 final_reply model.call([ *conversation, {role: tool, content: result.stdout}, ])为什么拆成两步比一次性调用稳定因为选哪个工具和用工具之后怎么回答是两个不同难度的决策。前者是一个分类问题后者是一个生成问题。混在一起模型的注意力会被长上下文稀释拆开之后每一个环节的上下文都很短、很聚焦出错的概率自然就降下来了。3.3 技能执行的边界权限、超时与无害化技能一旦涉及真实操作比如写文件、发消息、改数据就必须考虑安全边界。这个部分不能偷懒否则一个参数错误就可能造成不可逆的影响。我的做法分三层第一层参数白名单校验。模型传进来的参数不能直接信必须用 schema 校验一遍类型不对就强转范围越界就拒绝。第二层超时和资源限制。所有技能统一用 subprocess 执行设置 timeout防止某个技能因为死循环或请求外部服务卡死不退。第三层操作类技能加预执行确认。凡是带副作用的技能比如发送邮件修改数据库在执行前多问模型一轮你确定要执行吗参数都准确吗虽然多一次调用但能拦掉大量幻觉参数导致的误操作。4. 排查实录模型连续三天选错技能的那次经历4.1 现象所有错误都集中在两个相似技能上技能库上线第二周我发现一个诡异的现象。日志里 search_docs 的调用次数高得离谱但里面有一半的调用用户实际问的是算一下统计一下这类需求。也就是说模型把检索文档的技能用来干数学计算的活了。最开始我怀疑是不是模型版本更新导致能力回退但我把出错的调用日志单独拉出来看发现所有错误调用都指向同一个输入模式用户问题里含有查一下看看帮我找找这类模糊动词。比如用户说帮我查一下 3 月份销售额是多少模型就把 search_docs 调起来了。而当时明明有一个 query_sql 技能专门干这个事。4.2 排查链路从怀疑模型到定位到描述向量重叠我做了三步排查。第一步检查上下文。把出错时喂给模型的完整 messages 序列调出来看确认上下文没有截断技能描述都被完整送进去了。排除上下文长度问题。第二步直接把同一个 prompt 在不同模型上测了一遍。在 GPT 和 Claude 上都复现了类似错误——这说明问题不在某一个模型而在技能描述本身。第三步给技能描述做了个相似度计算。我用 embedding 把两个技能的 description 向量化然后算余弦相似度。结果让我有点意外search_docs 和 query_sql 的描述相似度高达 0.61而它们本应该是两个完全不相干的能力。为什么相似度这么高因为两边都出现了查询、数据、内容、文档这些高频词。模型在做语义匹配时会倾向于把用户问句映射到相似度最高的描述上而用户问查一下销售额其语义重心其实更靠近查询数据于是模型就挑中了描述里同样有查询、数据的 search_docs。4.3 修复不只是改文案还要改路由逻辑定位到根因后我做两处修改。第一处重写两个技能的描述。核心思路是让它们的语义空间尽量正交。search_docs 的描述里所有数据字样全部改成文档片段知识条目query_sql 的描述里则强化了结构化表格聚合统计销售/订单明细等专属关键词。同时两个技能的 when_not_to_use 里互相点名对方search_docs 的不适用场景加上不用于数值统计、表格分析query_sql 的不适用场景加上不用于检索自然语言文档或回答政策类问题第二处路由逻辑升级。我不再让模型从一个平铺的技能列表里直接选而是把技能按领域分组先做一次粗粒度路由再到组内做细粒度选择。这一步相当于给模型加了一个先分类再匹配的过程选择准确率提升非常明显。修复后我重新做了一轮回归测试随机抽取 500 条历史对话模拟真实用户请求。技能选择准确率从 71% 提升到了 94%而且之前最严重的用搜索技能做计算的错误在测试集中归零。这次排查给我最大的教训是当模型频繁选错工具时第一反应不要怪模型先去看技能描述之间的相似度。描述写得含糊模型必然含糊。5. 参数 Schema 和上下文预算规模一上去新坑就来了5.1 Schema 写得太严格模型反而不会填技能数量变多之后参数问题开始冒头。最开始我图省事每个技能只有一个参数全部用字符串类型。后来技能复杂了开始出现必填字段、枚举值、嵌套对象问题就来了。一个典型的案例search_docs 的 limit 参数我一开始设成必填范围限制在 1-20。结果模型经常漏填一漏填整个技能调用就报错Agent 就得重试。后来我给 limit 加了默认值 5并且把必填改为非必填漏填率一下就降下来了。另一个坑是枚举值。我在 query_sql 技能里设了一个 table_name 参数枚举了三个可查的表格。结果用户问了个不在枚举里的表模型没有选择不去查而是从枚举里硬挑了一个最接近的——然后返回了一堆错误数据。我的解决办法是 schema 里加了 additionalProperties: false禁止模型发明参数。另外枚举值不是拦截所有不在范围内的请求而是在参数校验不通过时让 Agent 主动反问问用户而不是闭眼硬试。5.2 技能列表膨胀上下文塞不下了技能到 20 个的时候新的问题出现了所有技能描述拼起来占了上下文一大半模型开始出现注意力稀释——它能看到所有技能但哪个都记不牢。当时我在日志里看到一个典型错误用户问今天天气怎么样模型在一堆技能列表里选中了一个人气最高的技能因为描述最长、信息最多而不是真正匹配天气的技能。这说明当技能超过一定数量后模型的选择逻辑会退化成选描述最显眼的。我试过压缩描述、删除冗余字段都没用因为根因是候选集太大。最终解法是引入技能分组路由SKILL_GROUPS { knowledge: [search_docs, faq_lookup, wiki_query], data: [query_sql, export_csv, chart_draw], external: [weather, geo_code, translate], operation: [send_email, create_ticket, update_order], }执行时先让模型判断用户请求属于哪个分组再只把这个分组内的技能描述传给模型做选择。这样一来模型每一步面对的候选集从 20 个降到了 4-5 个选择准确率重新回到了 90% 以上。关于技能数量我最后得出的经验是单个 Agent 实例里平铺技能不要超过 12 个超过就得分组否则能力列表本身就是噪音。5.3 模型幻觉参数的拦截校验、重试、放弃三层兜底模型生成参数时偶尔会幻觉出一些根本不存在的字段或者把数字类型传成字符串。这些问题如果不在入口拦住会一路带进业务系统轻则报错重则脏数据。我的技能执行入口做了三层兜底第一层Schema 强校验。所有参数过 pydantic 模型类型不对自动强转字段缺失用默认值补齐多余字段直接丢弃。第二层校验失败后自动重试一次。把校验错误信息回传给模型让它重新生成参数。这一步能挽回大概六成左右的错误参数。第三层重试仍然失败就放弃调用明确告知用户当前无法完成该操作绝不带着错误的参数硬上。这三层兜底跑了一个月之后参数相关的错误率从 8% 降到了 2% 以下。别小看这 6 个百分点的提升在真实业务里这就意味着每天少几十次错误调用。6. 技能库的长期运营版本、度量和淘汰6.1 技能必须做版本管理不然线上问题你定位不了技能库上线稳定之后我踩了一个挺常见的坑技能脚本更新了但 Agent 框架加载的还是旧版本线上出了问题对着新代码排查了半天最后发现跑的压根不是这个版本。后来我做了三件事彻底解决这个问题。第一每个技能目录全部纳入 git 管理并且独立记录版本号发布时跟着主程序一起做 tag。第二Agent 框架每次加载技能时把技能版本号写入日志。这样哪次调用用了哪个版本的技能全部可以回溯。第三灰度发布。技能脚本改了之后不直接全量上线而是先在一小部分会话里加载新版本跑一天看错误率没问题再全量切。这套流程跑通之后技能改起来放心多了。因为没有版本管理之前一次改动上线出问题你连是哪个版本导致的都说不清。6.2 用几个指标判断技能有没有在认真工作技能库运营一段时间后我建立了一套简单的度量体系。不多就三个核心指标。第一个是调用次数。这个最直观一个技能一周内被调用多少次一眼就能看出它是热门能力还是摆设。连续一个月一次没被调用的技能基本可以怀疑它存在的价值。第二个是选择准确率。计算方式是所有调用日志里人工标记为正确技能的次数除以总调用次数。理想情况应该在 90% 以上。如果一个技能经常被选但经常选错说明描述可能有问题或者它和另一个技能语义重叠了。第三个是修正率。模型第一次调用就完美执行的占比这个指标衡量的是参数 Schema 写得好不好。如果某个技能修正率特别低老是让模型重试参数说明它的参数定义对模型不够友好需要简化。这三个指标我每周跑一次表格拉出来自己看作为技能库优化的依据。6.3 该合并就合并该下线就下线技能库不是越大越好。我有一次为了覆盖更多场景一股脑加了十几个新技能结果其中一个技能上线 45 天一次都没被调用过。不仅占上下文预算还在语义匹配时对其他技能造成干扰。后来我建立了一个比较简单的淘汰规则技能连续 30 天零调用或者和已有技能描述相似度超过 0.5就进入评审。评审后要么合并、要么下线、要么重写描述。另外技能合并也要动脑筋。不是把两个脚本塞到一个技能里就行而是要重新分析这两个能力的用户意图是否一致。比如查天气和查空气质量可以合并成一个查天气与环境但如果把查天气和查订单合并那模型反而会晕。合并的判断标准是用户会用一句类似的话来同时触发这两个能力才值得合并。技能库运营到现在我的体感是它更像一个产品不是一份代码。技能的增删改查应该跟着真实用户需求走而不是跟着我能做什么走。定期清理、度量、迭代才能让技能库保持健康。最后分享一个我个人的经验如果你第一次接触 agent-skills 这套方法不要一上来就铺二十个技能。先拿 5 个高频的、边界清晰的技能跑通全流程把描述规范、路由机制和安全边界都调顺了再慢慢往里加。技能库这个系统规模小的时候怎么搭都行规模一大前面偷的懒都会变成后面踩的坑。
返回列表