1. 从“Agent 玩成 Prompt 堆砌大赛”说起
先交代一下背景。我之前做过不少 LLM 应用,最早的版本特别简单,把所有能力、规则、人物设定、工具说明全部塞进一个 8000 字的 system prompt 里,靠模型自己去“理解”。一开始效果还行,但随着功能变多,问题一个个冒出来:上下文被无关内容挤占,关键指令反而被稀释;改一个业务规则要动整个 prompt,改完这轮正常、下轮莫名失效;Token 成本更是肉眼可见地涨。
后来我换了个思路,不再试图让模型“天生就知道一切”,而是把 Agent 的能力拆成一个一个独立的 skill,每个 skill 管一件事:查天气一个、算运费一个、写周报一个。模型只负责判断“现在该调用哪个技能”,具体怎么执行,交给技能内部完成。这套做法我整理成了一个小项目,就叫 agent-skills。
说白了,agent-skills 不是什么神秘框架,它是一套把 Agent 能力工程化的方法论加落地代码:定义技能、注册技能、触发技能、编排技能。只要你的应用是基于 LLM 的 Agent,不管是做客服、做数据分析、做内容生成,这套思路都能帮你把混乱的 Prompt 逻辑收敛成可维护、可测试、可观测的模块。下面我按实际搭建的顺序,把我踩过的坑、验证过的参数、最终跑通的方案都写出来,东西不复杂,但细节不少,希望能给准备动手的人省点时间。
2. 技能库的三个核心抽象:意图描述、参数契约、执行体
我在最开始想得很简单,以为 skill 就是一个函数,模型调用函数就完事了。真做下去才发现,如果只给模型一堆函数签名,效果很差,模型经常不知道该选哪个,或者选对了但参数传错。后来我把每个 skill 拆成三部分,才算真正稳定下来。
2.1 为什么要拆成三件套
先说结论:Agent 的技能调用本质上是一次“意图识别 + 参数绑定的决策过程”。你只给模型一个函数名和参数列表,模型是在“盲选”;你给它一份结构化的技能说明,它才能“有依据地选”。
这三件套分别是:
- 意图描述(intent):用自然语言说明这个技能是干什么的、什么时候该用它、什么时候不该用它
- 参数契约(schema):定义调用这个技能需要的字段、类型、约束条件
- 执行体(executor):真正干活的代码,可以是本地函数、外部 API 封装、甚至是另一个子 Agent
我在 agent-skills 里用一个 YAML 文件来声明技能,结构大概是这样的:
skills: - name: order_query description: 查询订单状态和物流进度 intent: | 当用户询问订单是否发货、物流到哪了、预计什么时候到达时,使用此技能。 当用户只是闲聊或询问商品推荐时,不要使用此技能。 parameters: type: object properties: order_id: type: string description: 订单编号,通常是一串数字 required: - order_id executor: functions.order_query这个声明文件是给“人”看的,也是给“模型决策层”看的。我在接入模型之前,会把这段 YAML 转换成模型厂商兼容的 function calling 格式,转换逻辑很简单,就是把 description、parameters 原样透传,但 intent 字段会放到系统提示词里的技能索引区,因为模型看长文本说明的效果通常比只看函数签名更好。
2.2 意图描述怎么写才有效
这部分我调整过很多次,最开始的写法是“本技能用于查询订单信息”,太笼统,模型经常误用。后来参照搜索引擎的查询扩写思路,把意图写成“正例 + 反例 + 边界条件”三段式。
正例一定要带动词和对象,比如“查询订单状态”“修改收货地址”“生成周报”。反例要写清“不要做什么”,比如“当用户只是表达不满但没有明确要求退款时,不要调用退款技能”。边界条件要说明数据范围,比如“只支持查询最近三个月的订单”。
一个写得好的意图描述,能把技能误触发率从百分之二三十降到百分之五以下。这个数字不是拍脑袋,我在后面加了一组对照测试,同样的测试集,宽泛描述的误触发率明显高很多。所以写 intent 时千万别偷懒,正例反例边界都写全,模型才能少犯错。
2.3 参数契约的取舍
参数契约是我踩坑最多的地方。一开始我照着 OpenAPI 规范写,每个字段都加 constraints,还设了各种 enum。结果模型调用失败率很高,经常因为传了一个不在枚举里的值而报错。后来我把规则收敛成三条:
- 必填字段尽可能少,能只留一个 order_id 就绝不多要一个 user_id
- 枚举值只在业务硬性要求时才设置
- 给 description 写明取值示例,模型参考示例填参的准确率比我预想的高很多
另外有个小技巧:如果某个参数可以从上下文推断,就不要让模型猜。比如用户在一段多轮对话里已经说过“我是会员”,后续的查询技能就不需要再要求传会员等级,直接在编排层把上下文信息注入进去。
2.4 执行体:最不被重视但最容易出问题的地方
执行体不建议直接写太重的业务逻辑。我的习惯是,执行体只做三件事:拼接口请求参数、调外部服务、把结果整理成适合 LLM 阅读的摘要。这里要注意的是:给模型返回的内容不要是原始 JSON 堆砌,最好先做字段筛选,只保留模型回答用户问题时真正有用的信息。
比如查天气的接口返回一百多个字段,你全丢给模型,它虽然能读懂,但会浪费 token 而且容易抓住次要信息。我在执行体里会预处理成一句话摘要:当前温度 23 度,多云,东南风 2 级,未来两小时无明显降水。这一步直接决定了后续生成回复的质量。
3. 触发链路设计:让 Agent 在合适的时机选中合适的技能
技能定义好了,接下来要解决“什么时候触发哪个技能”的问题。这是 agent-skills 里最核心的消息通路,也是我最想分享的部分,因为这里面的参数调节直接决定整个系统的可用性。
3.1 三种触发方式的路线对比
我试过三种触发路线,各有优劣。第一种是硬编码规则路由,比如用户消息里包含“订单”两个字就查订单表,实现简单但泛化差,换个说法就漏。第二种是纯语义检索,把所有技能的 intent 描述向量化,用户消息实时算 embedding,取相似度最高的技能。第三种是混合式,先用语义检索召回 Top-3,再用一个轻量分类 Prompt 从候选里做最终决策。
我最终用的是混合式。原因很简单:纯语义检索会偶发“高相似度但事实不对”的情况,比如用户说“我不想查订单了”,语义上跟订单查询技能高度相关,但意图是取消查询。加一层轻量分类 Prompt 之后,它会先判断用户当前是否真有查询意图,再决定是否调用,误判率降了一个数量级。
3.2 语义检索的嵌入模型与向量库选型
嵌入模型我用过通用型文本向量模型,也用过针对短文本优化的开源模型。实测下来,技能匹配场景大多是短查询对短描述,用视中文效果波动的通用模型反而容易跑偏,最后换成了一款针对搜索优化的嵌入模型,匹配准确率提升明显。
向量库方面,中小规模技能库(几十到几百个技能)用成熟的开源向量数据库完全够用,不需要上大规模分布式检索服务。检索时最重要的参数是两个:召回数量 top_k 和相似度阈值。我最终设置在技能数量五十个左右时,top_k=3、相似度阈值 0.35 效果比较均衡。
下面是我在测试集中记录的一组表现对比,能够看出阈值设太高虽然精确率高但很多该触发的情况被拦下了,设太低则乱七八糟的技能都会被召回来:
| 相似度阈值 | 精确率 | 召回率 | 实测体感 |
|---|---|---|---|
| 0.20 | 42% | 95% | 大量无关技能混入,决策层负担重 |
| 0.30 | 71% | 88% | 有少量噪声,但能靠决策层兜住 |
| 0.35 | 86% | 79% | 均衡,推荐起始点 |
| 0.45 | 94% | 51% | 漏触发明显,用户常见说法被拦 |
表格里这个“实测体感”只有你真跑了才会懂:阈值低的时候,问题不是模型傻,而是语音干扰太多;阈值高的时候,问题变成用户换个句式技能就罢工了。所以我不建议你照抄我的 0.35,而是拿自己的测试集跑一遍,看清自己的技能描述分布再调。
3.3 决策层的兜底校验逻辑
召回层之后,决策层不能只是简单“选相似度最高”,我加了三道校验:
- 当前用户消息是否承接了上文的否定或撤销意图
- 技能意图描述里是否包含与当前消息冲突的边界条件
- 必填参数是否齐全,不齐时是否已经在上下文中能补齐
只有三道校验全部通过才真正执行技能。这样做会多一点额外 token,但带来的稳定性提升非常明显。特别是第一道,它解决的“不想做某事”问题,是单一 Embedding 无法判断的,加一道规则级校验最高效,千万别省。
3.4 触发失败时的降级策略
最后是降级。技能匹配不上时不能直接回“我无法处理”,我准备了三个梯度:第一梯度是反问澄清,比如用户问“那我的东西到哪了”,没有提到具体订单,就反问订单号;第二梯度是通用技能兜底,比如默认的闲聊响应;第三梯度是人工转接。这三个梯度按顺序走,用户体验比僵硬报错好得多。
4. 多技能编排:从单技能调用到一串任务的调度
单技能的触发跑通之后,真正的复杂度在多个技能之间的编排。我在 agent-skills 里实现了几种编排模式,实际使用频率最高的是动态编排和串行流水线,各有适用场景。
4.1 串行编排与并行编排的适用边界
串行编排适合任务步骤明确、前一步输出是后一步输入的场景。比如“查天气后生成穿衣建议”,必须先查完天气才能写建议,天然串行。并行编排适合多个独立查询互不依赖的场景,比如用户问“北京和上海明天都什么温度”,两个技能调用互不影响,并行执行可以节省整体耗时。
我实现串行时给每个节点加了输入输出字段映射,前一个技能的输出摘要会被写入后续技能的上下文前缀。实现并行时的技巧是控制并发后的合并节奏,所有分支返回后再统一汇总给上层决策。并行如果某个分支超时,要能单独降级而不影响其他分支结果。
4.2 上下文在技能之间如何流转
这是做多技能编排时最容易乱的地方。我在 agent-skills 里的做法是引入一个会话级 Memory Buffer,但 Buffer 不是无脑全存,而是分三层:
- 全局层:存用户身份、历史摘要、长期偏好
- 技能层:存每个技能上次调用的结果摘要
- 临时层:存当前这个完整请求内部的中间产物
技能执行时,只能读取全局层和自己对应的技能层;临时层只在前端编排链路里存在,技能结束后按需合并进技能层,而不是丢进全局层。这个隔离设计我是在一次故障之后才想明白的:最初所有技能共享一个大 context,结果查天气的技能把上一轮“用户想退款”的临时信息也读进来了,生成的引导话术直接跑偏。
4.3 多个技能都想接管同一个请求时怎么办
技能数量到几十个之后,一定会出现“一个请求触发两个技能都很合理”的情况。比如用户说“帮我推荐一部电影,顺便告诉我附近哪家影院最近”,电影推荐和影院查询两个技能都被命中。
我的解决思路是给技能加上优先级和互斥组声明,在决策层做冲突裁决。互斥组的逻辑比较简单:同一批技能共享一个互斥标志,同时命中时按业务预设的优先级取最高者;优先级相同时再走一个轻量决策 Prompt。优先级这件事建议从写技能声明时就规划,不然后期加技能容易顾此失彼。
4.4 Token 成本与延迟的平衡
技能编排跑起来之后第二个现实问题是成本。每个技能调用都要消耗输入 token,如果编排层级深,一个请求的 token 消耗可能是单技能的好几倍。我做了两个措施:
- 中间结果摘要化:技能返回的完整数据只保留 200 字以内的摘要进入下一环节,完整数据存本地日志,需要时再查
- 全局 Token 预算:在每个请求入口设一个阈值,比如 8000 token,超过时高级编排自动退化成“只做一次主技能调用”
这两个措施相结合,整体成本大约下降了 40%,而效果几乎没有可见性损失,因为模型只要拿到摘要就能生成合理回答,真正需要完整数据的场景大多数在本地就能解决。
5. 技能系统的工程底座:持久化、可观测与回归验证
技能多了之后,最怕的就是“这次改了一个技能的 description,另一个技能的触发率掉了”。不把观测和回归做起来,你根本无法定位这种隐性回归。这一部分是我认为 agent-skills 从玩具变成工具的分水岭。
5.1 技能调用日志应该记哪些字段
我用的日志结构和传统的接口请求日志不太一样,重点关心的是链路本身。每条技能调用记录包含这样几个字段:
- session_id:会话唯一标识
- request_id:用户当前这一条消息的唯一标识,一次消息可能触发多次技能调用
- skill_name:实际调用的技能名
- matched_score:召回层给的相似度分数
- decision_source:是语义命中、规则命中还是降级兜底
- llm_round:决策层第几轮生成
- latency_ms:技能执行耗时
- input_tokens / output_tokens:消耗 token 数
- raw_input / result_summary:原文和返回摘要
每个字段都能回答一个具体问题,缺哪个都会在排查时抓瞎。比如 matched_score 字段能帮你发现某技能误触发是因为召回层分数虚高,还是决策层没拦住;latency_ms 能暴露执行体里某次外部 API 超时的规律。
日志我建议直接落到本地日志服务,别只存在内存里,否则复盘时没数据可用。会话维度再用一张表做聚合,reduce 出每个技能调用成功率和平均耗时。
5.2 可观测性:一次跨技能请求的全链路追踪
当一次请求触发了“查订单 → 查物流 → 生成进度说明”三个技能时,我需要能一次性看到整条链路。我的做法是引入 trace_id,一次用户消息生成一个 trace_id,所有技能调用记录都挂在这个 trace_id 下面,同时记录 parent_skill_id 表达父子关系。
排查问题时,我直接按 trace_id 拉出整条链路的时间线,一眼就能看出哪一段慢、哪一段失败。有一次线上问题就是物流查询技能偶发超时,单看技能日志只看到 latency 波动很大,拉出链路才发现是并发场景下多个请求同时打同一个外部接口,触发限流。这类问题不靠链路视图根本看不出来。
5.3 技能级回归测试集
我建了一个精简但有效的回归测试集,大概一百多条测试用例,每个用例包含:用户输入、期望触发的技能名、期望不触发的技能名。每次修改技能声明或者调了检索参数之后,跑一遍测试集,对比通过率。
这个测试集的价值在于,它能比较客观地告诉你“这次改动有没有影响其他技能”。我遇到过最典型的情况是,给退款技能加了新的意图描述之后,发票技能在测试集里的误触发率到了 8%。如果没有回归测试,这个问题在线上可能要好几周才能被发现。
测试集的编写也有技巧,不要只写正例,要大量写反例,特别是那些“看着该触发但其实不该触发”的句子。反例比例我建议至少占三成,这样才能真正起到约束作用。
6. 实战中的踩坑记录:这些问题不调试你是看不出来的
最后集中写几个调试过程中让人抓狂的问题。这些问题都很有共性,如果你也在做类似的事,大概率会遇到其中一个。
6.1 意图描述过细带来的检索塌陷
我一开始觉得意图描述写得越细越好,于是把正例写了一大堆,结果发现召回率反而下降了。原因是描述太长之后,向量被各种细节拉偏,简短的用户查询反而匹配不上。后来我改成“核心语义 + 边界反例”两边分开,正例只保留最典型的三种句式,反而效果更好。
6.2 参数契约太严格导致技能“看得见摸不着”
这个问题出现在一个“导出报表”技能上。我要求必填参数包括报表类型、时间范围、格式、是否抄送邮箱,结果模型经常漏传或者传错格式。后来我把必填项砍到只有时间范围一个,其他参数全部改成可选,调用成功率立竿见影。多数场景下,模型能少猜一个字段就少一次出错机会。
6.3 技能数量膨胀后的检索衰减
技能从十来个增长到五十多个后,我明显感觉匹配变差了,误触发和漏触发同时增多。五十多行 intent 描述挤在一起,彼此语义距离变近,确实容易互相污染。处理方法有两个:一是把技能做成分组命名空间,比如订单类和售后类分开描述前缀;二是把相似意图描述合并成一个大技能,内部再做二级分发。效果都很明显,五六个组命名空间下去,测试集通过率又回来了。
6.4 Agent 自主编排与固定流程之间的平衡
最后一个想提醒的是:不是所有流程都该让 Agent 自主决定。我曾经尝试让模型自己决定技能编排顺序,结果它偶尔会跳过必要的前置步骤,比如直接调用“生成售后工单”而没有先调用“查订单归属”。后来我改成关键业务流程用显式 DAG 固定路径,只有非关键分支允许动态决策。一下子既保留了灵活性,又卡住了业务底线。
这套平衡原则基本没什么例外:核心链路越短越固定,用户体感越一致;只有旁路分支才适合交给 Agent 发散。
我在 agent-skills 上反复调试最大的体会是:Agent 能力工程化没有银弹,但把意图、参数、执行、触发、编排、观测这些环节逐个理顺之后,系统的稳定性会一天比一天好。你如果也在做类似的事,建议先从最小可用的三个技能跑通全链路,再逐步加量,过程中的日志和测试集从一开始就建好。后面加技能只会越来越顺,不会越来越乱。