Agent技能库实战:从描述设计到运行评估的完整方法论
2026/9/23 4:21:17 网站建设 项目流程

做了大半年 Agent 项目,交过不少学费,要说最值的一笔,就是下定决心把"agent-skills"当成一个正经工程来做。一开始我也不觉得技能库算什么大事,代码里塞几个函数、系统提示词里堆几段描述,能跑就行。可等场景一多、调用一乱、模型开始"抽风"不按套路出牌的时候,才意识到技能这块才是 Agent 能不能落地的七寸。这篇就把我从技能库设计、描述编写、运行机制到迭代评估的全过程盘一遍,踩过的坑、总结出的套路都写进去,给同样在搞 Agent 技能体系的朋友做个参考。

1. 技能库的定位与整体设计思路

1.1 Agent技能到底是什么

很多人把技能和工具混为一谈,这其实是第一个认知误区。工具是函数、接口、API,是一个"确定性的执行单元",输入什么参数就返回什么结果,本身没有理解能力。而 Agent 技能,是"模型可理解、可判断、可调用的能力单元",它包含工具的调用方式,更包含"什么时候该调用、怎么传参、结果怎么理解、失败了怎么办"这一整套上下文。

我举个生活化的例子。给 Agent 一个发邮件的函数,它只会机械地往参数里填收件人、主题、正文。可如果给 Agent 一个"发邮件技能",它应该知道:邮件正文太长时需要摘要、收件人不在通讯录时需要先查联系人、发送失败时需要换标题重试、紧急邮件应该前置发送。这些判断逻辑,靠一个函数签名是表达不了的。

所以我在设计 agent-skills 时第一原则就是:技能不是给工程师看的接口文档,而是给模型看的行为说明书。同样一个查天气函数,描述成"根据城市查询天气"和描述成"当用户询问未来几天是否需要带伞或出行计划时,用此技能获取目的地天气,重点关注降水、温度、风力等维度",后者的调用准确率显然会高得多。

1.2 为什么要把技能独立成库

项目早期我是把所有技能描述直接写进系统提示词的,维护了大概两个月就疼得不行。最直接的问题是耦合:改一个技能描述的措辞,整个提示词都要重新评测;加一个新技能,要担心会不会挤占其他技能的触发空间;线上效果出了问题,很难定位是技能本身的问题还是提示词里其他内容在干扰。

抽成独立的技能库之后,我最大的体会是边界变清晰了。每个技能有自己独立的版本、自己的触发明细、自己的评测集。改动一个技能不需要重新过一遍全量提示词,只需要验证这个技能的场景集合就行。而且技能库天然就是配置即代码,可以走 Git 评审、版本回溯、灰度发布,这在多人协作时格外重要。

另外一点可能不容易在初期注意到:独立技能库对模型幻觉有抑制作用。当所有技能描述混在大段系统提示词里时,模型对边界的感知是模糊的,容易"发明"出不存在的参数或能力。而按固定 schema 组织的技能库,描述和信息密度是被约束过的,模型反而更容易理解"哪些能做、哪些不能做"。

1.3 技能分类与组织方式

技能库具体怎么组织,没有统一标准,我经过几轮调整后固定下来一套自己用着最顺手的分类方式。按触发方式分,有主动技能和被动技能:主动技能是模型根据用户需求自主决定调用的,比如查天气、设提醒;被动技能是用户明确要求执行的,比如"帮我发条消息给张三"。这两类技能的描述侧重点很不一样,主动技能必须写清楚触发条件,被动技能则要注意参数校验和确认逻辑。

按能力类型分,我的库里有四类:

  • 工具型技能:操作外部系统,查数据库、调 API、发消息、操作文件,最传统的一类。
  • 读写型技能:从知识库、文档中读取信息或写入笔记,侧重上下文理解。
  • 认知型技能:不调外部工具,而是完成推理、总结、翻译、拆解任务,本质是"模型自身能力的封装"。
  • 协作型技能:把任务分派给其他 Agent 或角色,用于多 Agent 场景。

目录结构上我建议这样分:

agent-skills/ ├── skills/ │ ├── email/ │ │ ├── skill.yaml │ │ ├── examples.md │ │ └── tests/ │ ├── database_query/ │ │ ├── skill.yaml │ │ ├── examples.md │ │ └── tests/ │ └── meeting_summary/ │ ├── skill.yaml │ ├── examples.md │ └── tests/ ├── eval/ │ └── cases.csv └── registry.json

每个技能一个目录,skill.yaml 放元信息和描述,examples.md 放示例对话,tests 目录放该技能的离线评测用例。这套组织方式在技能数超过 20 个以后,优势会非常明显。

2. 技能描述的结构设计与实操要点

2.1 技能描述为什么是成败关键

我见过不少团队,工具函数写得漂漂亮亮,技能描述就一两句话糊弄过去,然后抱怨"模型就是不调用我们的工具"。这里要说句得罪人的话:多数时候不是模型不行,是技能描述写得不行。

模型调用技能的过程,本质上是一个"阅读理解+匹配"的过程。模型读到用户需求,在有限的上下文窗口里寻找"哪个技能最匹配当前场景",这其实是在做信息检索。如果你的技能描述信息密度低、触发条件模糊、例子匮乏,模型在短暂的计算过程中根本检索不到这个技能的存在,更别说正确调用了。

我们做过一次对比实验:同一个查订单技能的描述,从一句话扩展成完整的结构化描述后,模拟用户咨询场景下的技能触发率从 43% 提升到了 78%。这个提升没有改一行代码,只改了文本。技能描述的价值我后来总结成一句话:描述写得好不好,决定了模型是"看得见"这个技能,还是"看不见"

2.2 一份可用的技能描述该包含哪些字段

我在 agent-skills 里用 YAML 来写技能描述,字段是经过多轮迭代稳定下来的。以下是我认为最核心的七个字段:

字段作用填写要点
name技能唯一标识使用英文短横线命名,如database-query,不要用中文和空格
description一句话说明技能能力用动词开头,说清楚"做什么",不要含糊
when_to_use触发条件最关键的字段,明确列出什么场景必须调用、什么场景禁止调用
input_schema输入参数定义定义参数名、类型、必填与否、含义,尽量用枚举约束取值
output_format输出结构定义返回数据的结构和格式,方便 Agent 理解结果
error_handling失败处理策略说明调用失败后应如何降级、重试或向用户解释
examples示例2~3 组用户输入到技能调用的完整示例,正例反例都要有

拿"查询数据库"这个技能举例,描述大致长这样:

name: database-query description: 查询业务数据库中的结构化数据,支持订单、用户、商品三类表 when_to_use: > 当用户询问订单状态、用户信息、商品库存等结构化数据时使用。 当用户只是泛泛询问"数据情况"而没明确具体对象时,先追问澄清,不要调用。 input_schema: table: type: string enum: [orders, users, products] required: true description: 要查询的数据表 conditions: type: object required: false description: 筛选条件,例如 {"status": "pending"} output_format: JSON 数组,每条记录包含表的所有非空字段 error_handling: > 查询失败时,首先检查 SQL 语法和表名拼写; 如果是因为缺少筛选条件导致数据量过大,补充条件后重试; 仍失败则如实告知用户"暂时无法查询,请稍后再试",不要编造数据。 examples: - input: "帮我看看订单 A10086 现在什么状态" thinking: "用户想查订单状态,属于 orders 表,条件是订单号" call: database-query(table="orders", conditions={"order_id": "A10086"})

2.3 编写技能描述的三个忌讳

第一个忌讳是触发条件写得太模糊。"当用户需要查询时使用"这种描述等于没写,因为模型判断"用户是否需要查询"的成本很高,很容易漏触发或乱触发。正确做法是列出具体的意图关键词和行为模式:"当用户提到订单号、物流单号、报修编号时,优先考虑调用此技能"。

第二个忌讳是参数说明不完整。特别是枚举值,不写全的话模型就可能发明出不在范围内的取值。我见过模型自己生成一个"user_type=VIP"去查用户表,结果 SQL 直接报错。好的做法是把可枚举的取值全部列出来,并标注默认值和含义解释。

第三个忌讳是没有反面示例。模型学习调用时机,很大程度上依赖少量样本,如果你只给正面示例,模型可能会过度触发。比如上面 database-query 的例子,我特意写了"当用户只是泛泛询问数据情况时,先追问澄清,不要调用",这个反例在测试中显著降低了误触发率。

3. 技能调用的运行机制与工具选型

3.1 技能注册与加载流程

技能描述文件是静态的,要让模型真正用起来,必须对接运行时。我现在的加载链路是:启动时扫描技能目录 -> 解析每份 skill.yaml -> 通过 registry.json 做依赖注册 -> 把技能的 description 和 input_schema 注入到模型上下文。核心逻辑不复杂,关键在"哪些内容需要注入"。

最早我是把所有技能描述全量注入到系统提示词里,技能一多上下文就爆。后来改成按场景分组加载:先根据用户会话的会话历史做一次粗粒度意图判断,然后只加载匹配场景的那几个技能描述。用这种方式,上下文占用能减少 60% 以上,模型对技能的记忆精度也有提升。

伪代码大概长这样:

def load_skills_for_conversation(conversation_intent: str) -> list[Skill]: skills = read_all_skills("agent-skills/skills") # 根据会话意图做粗粒度过滤,只加载相关技能 matched = [s for s in skills if s.is_relevant(conversation_intent)] # 如果过滤后为空,回退到默认技能组 return matched or [s for s in skills if s.is_default]

这个过滤不追求完美,只要别把明显无关的技能塞进去就行。真正的技能选择还要靠模型在加载进来的候选集里自己判断。

3.2 技能调用链路的观测

技能库上线之后,必须解决"看不见"的问题。我在基本跑通的阶段就发现,技能调用链路是个黑盒,模型调没调技能、传参对不对、返回结果有没有被正确使用,全都不知道,出了问题只能靠猜。后来我搭建了一套轻量级观测方案,给每次技能调用都记录结构化日志。

我定义的日志字段包括:时间戳、会话 ID、技能名、输入参数(脱敏后)、返回码、耗时、模型最终对结果的引用情况。其中"模型最终对结果的引用情况"是后来看线上问题才补上的,这个字段解决了一个特别诡异的现象:技能成功调用了,返回结果也很正确,但模型在回复用户时完全没用到这个结果。

这条观测链路的直接收益是,我能算出每个技能的真实调用成功率。注意这里指的是"从用户诉求到用户满意"的成功率,不是工具执行成功率。工具执行成功但用户问题没解决的,恰恰是技能库最需要优化的环节。

3.3 我把技能放在哪里管理

这是项目管理层面的选型。技能描述本质是配置,但它和代码一样需要版本管理、评审和回溯,所以我全程用 Git 管理。每个技能目录下的 YAML 文件和示例、测试用例一起提交到同一个代码仓库,任何改动都走 MR 评审。

这里要补充一个很多人忽略的点:技能文件一定要做 Schema 校验。YAML 语法写错了不会报错,只有加载到运行时才发现,如果线上没有提前校验,一个小小的缩进错误就可能让整个技能加载失败。我在 CI 流程里加了一步skilllint校验,检查 YAML 格式、必填字段是否完整、枚举值是否合法。这个工具不复杂,但收益极高,等于给技能库上了个编译检查。

版本管理上我用 semantic version,每个技能独立版本,不搞全局版本。比如email技能的版本号是 1.3.0,指的是"发邮件技能"这个技能自身迭代到 1.3.0,跟其他技能无关。这种做法在技能数量多、迭代频繁的时候特别有用,可以精确定位线上效果变化是哪个技能改动引起的。

4. 技能评估与迭代:如何在真实场景中持续改进

4.1 离线评估集

技能库改对了还是改错了,不能靠感觉,要有一份可量化的评测集。我维护了一套离线评估集,专门用来在每次技能描述变更后跑回归。每个技能目录下的 tests 里,有 10 到 30 条精心构造的用例,覆盖典型场景、边界场景和容易误触发的反例。

每条用例包含四个要素:用户输入、期望命中的技能、期望传入的关键参数、期望的行为(调用技能 or 不调用技能)。拿"发邮件"技能举例,用例可能是这样:

用户输入期望技能期望参数期望行为
帮我把会议纪要发给李明email-sendrecipient=李明, subject=会议纪要调用
提醒我明天上午十点开会reminder-addtime=10:00, event=开会不调用 email-send
给我邮箱里找一下上个月的设计稿email-searchkeyword=设计稿, timeframe=上月调用 email-search

离线评估集跑过一轮,我就会发现一些特别反直觉的问题。比如模型的调用行为并不稳定,同一个输入跑两三次可能结果是不同的。正因如此,每条用例我至少跑三次,取多数结果来判定。评估通过之后,才允许把技能变更合并到主线。

4.2 线上监控指标

离线评估覆盖不了所有线上场景,所以线上指标监控必须跟上。我重点盯三个指标:技能触发准确率、参数正确率、任务完成率。

技能触发准确率的计算是:正确触发次数 / 技能总触发次数,所谓"正确触发"是指模型确实在当前场景下该调这个技能才调了。误触发和漏触发都要扣分,分别对应"不该调的时候调了"和"该调的时候没调"。

参数正确率主要看模型为技能传入的参数值是否合法、是否贴合用户意图。这个指标很能反映问题。比如查天气技能,模型把城市名"苏州"识别成了"徐州",参数传入正确率就是零,这种错误离线评估有时候很难发现,只有在线上真实对话中才会暴露。

任务完成率是最终指标,看用户的原始诉求有没有被真正解决。我会让用户在对话结束后点一个"已解决/未解决",再和日志里的技能调用记录做关联。三组指标一起看,才能定位问题到底出在"没调用技能"、"调错了技能"还是"调对了技能但结果没用上"。

4.3 技能库的版本管理与灰度发布

技能描述的改动看起来只是文本变化,但影响的却是线上所有用户的实际体验,所以它必须经过灰度发布。我在技能库里做了这样一套机制:每个技能支持同时存在多个版本,线上路由可以根据用户 ID、会话 ID 按比例分配不同版本。

举例来说,email-send这个技能,我要把触发条件从"包含@或邮箱后缀时触发"改成"提到发送、转发、抄送邮件时也触发",这个改动不能直接全量推给所有用户。我先让 5% 的流量落到新版本上,观察 24 小时内的触发准确率和任务完成率,如果指标不低于旧版本,再逐步扩大到 30%、100%。

灰度发布的最大价值是给了你后悔的余地。有一次我改了一个技能描述,离线评估全过,可灰度一放,触发准确率直接从 82% 掉到 65%。原因是新描述里加了太多具体场景关键词,让模型"更勇敢"了,开始在一些模糊场景下抢着调用。这种情况如果没有灰度,就直接酿成线上事故了。

5. 常见问题与排查技巧实录

5.1 模型死活不调用某个技能怎么办

这是新手最常遇到也最崩溃的问题。排查顺序我总结成了五步,按这个顺序走基本不跑偏:

  1. 先确认技能描述有没有被正确加载。很多人改完 description 但忘了重新部署或缓存没刷新,模型用的还是旧版描述。
  2. 再确认触发条件和用户输入是否匹配。拿一条线上真实对话输入,手动喂给模型,看模型的推理过程里有没有提到这个技能。
  3. 检查描述的信息密度。如果 description 和 when_to_use 加起来不到 100 个字,大概率不足以让模型在检索阶段注意到它。
  4. 加例子。许多技能加了 2 个典型正例和 1 个反例之后,触发率会有非常明显的提升。
  5. 最后再怀疑模型本身的问题,换用推理能力更好的模型测试一次。

我见过一个很典型的案例:一个查询订单的技能持续漏触发,排查发现技能描述里的字段名是order_id,但用户习惯说的是"订单号",日志里模型在推理时反复出现"用户提到订单号,但技能需要 order_id 参数,不确定是否匹配"。解决方案是在描述里加上"用户可能表达为订单号、单号、order id、A 开头的字符串"这样的别名提示。

5.2 技能调用成功后结果却被模型无视

这个问题的特征很清晰:日志显示技能返回了正常结果,状态码 200,耗时也在合理范围,但用户在对话框里得到的回复完全没用到这些数据。我最初以为是模型随机性问题,后来统计了一下,这个现象在 15% 左右的调用中会出现。

排查后根因有两点。一是技能返回结果太长,塞进了模型的上下文,但模型在处理后续生成时对这部分内容的注意力权重会下降,尤其是中间位置的信息最容易丢失。解决方法是输出格式要精简,按"结论先行、数据后置"的结构组织,让模型能一眼抓到核心。

二是结果和用户问题没有做显式关联。模型在生成回答时,需要能明确说出"用户问题 -> 技能调用 -> 技能结果"这条逻辑链。我在技能返回结果里加了一个字段answer_summary,里面写好一段可以直接引用的话术,模型拿到后一般会直接复述或微调,这个改动让结果引用率提升了不少。

5.3 技能数量膨胀后互相干扰

技能库超过 30 个之后,新的问题来了:技能之间开始"抢活"。模型在判断该用哪个技能时,可能被描述里相似的关键词带偏。比如email-searchemail-send,一个是搜索邮件、一个是发送邮件,描述都包含"邮件"、"收件人"等词,模型偶尔会在用户要求"找邮件"时调成发邮件技能。

我做了三件事来解决这个问题。第一,给技能加conflicts_with字段,显式声明哪些技能之间容易混淆,在生成候选集时做一次去重。第二,在 when_to_use 里做更严格的"互斥说明":"当用户要查找、检索邮件时,绝对不要使用 email-send 技能"。第三,增加一层技能路由层,在候选技能超过 5 个时,先由规则引擎做一次粗筛选,把明显不可能的技能过滤掉,减轻模型选择负担。

5.4 技能描述改了但效果变差怎么回滚

有一套稳定的回滚机制很重要。我的做法是把每个技能的每次描述变更都加到技能的 CHANGELOG 里,记录清楚改了什么、为什么改、受影响的用例有哪些。一旦线上效果出现回退,可以快速定位到具体的变更,并一键切回上一个版本。

有一次我把一个技能描述从 YAML 里非常精简的写法扩展成大段文字,加入了很多"帮助模型理解业务背景"的内容。结果离线评测显示触发率上升了,但线上任务完成率却掉了一截。分析之后发现问题出在过长的描述反而让模型的注意力分散,淹没了对关键触发条件的识别。这个案例让我确立了新的原则:技能描述的信息不是越多越好,而是越精准越好,每个词都要承担"帮助模型做出正确调用决策"的功能,与这个目标无关的内容,能删就删。

最后再分享一个我实际使用中的习惯:技能库做出来后,要让团队里每个人都用自然语言描述自己的操作过程,再把它翻译成技能定义。因为技能的本质是"把人类会做的事,用模型能理解的方式表达出来"——这个翻译过程做得越细致,Agent 就越像一个真正会办事的人,而不是一个只会调接口的机器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询