做Agent项目时间久了,你会发现一个规律:真正让智能体在业务里立住的,往往不是某个提示词写得有多精巧,而是它背后有没有一套可以复用、校验、编排的能力模块。这个模块层,我习惯叫它agent-skills,也就是智能体的技能库。它不是RAG,也不是插件市场,而是介于两者之间的能力抽象层——把"模型能做什么"收敛成"系统能稳定执行什么"。这篇文章我会从技能的定义、注册、路由、执行、编排到常见翻车场景,完整拆一遍,适合正在做Agent应用、想从"提示词碰运气"转向"工程化构建智能体能力"的开发者参考。
1. 技能与提示词的本质区别:为什么通用提示词撑不起复杂任务
先说说我为什么开始研究技能库这件事。早期做某个客服任务系统时,我一度把所有业务逻辑都塞进一个大而全的提示词里,让模型自己判断该做什么、按什么顺序做。结果在Demo阶段表现还行,一上真实数据就露馅:模型会在中途自创步骤、把参数填错、甚至把"查订单"做成了"查物流"。后来我意识到,问题不在于模型不够聪明,而在于我给了它太多自由、太少约束。
1.1 技能是一份契约,提示词是一段散文
提示词本质上是一段给模型的上下文备注,语气再强硬也只是"建议"。模型可以遵守,也可以打折扣,尤其当上下文里同时存在多段指令、历史消息、示例时,优先级冲突非常常见。而技能不一样,它是把一类任务封装成可描述、可校验、可测试的执行单元。你可以把它理解为函数,而不是注释。
具体来说,一个技能至少包含三样东西:描述(什么时候该用它)、参数契约(输入长什么样)、执行体(真正落地去做什么)。模型在技能体系里的职责只剩下两件事:判断该调用哪个技能、把用户的输入填充成技能要求的参数。其他事情全部由代码接管。这样一来,模型犯错的空间被大幅压缩,系统整体稳定性反而上去了。
1.2 一次定义、多处复用的工程红利
提示词还有一个隐形负担:一旦项目里有多个Agent、多个入口,你往往需要复制粘贴同一大段指令,改一处需求就得全局同步,几乎不可能维护。技能库天然是"一处定义,多处引用"。
我在某个多智能体项目里,把"查订单状态""计算配送时间""生成售后表单"这些能力统一沉淀成技能,然后让客服Agent、工单助手Agent、质检Agent共用同一套技能注册表。后来业务调整,配送时间计算逻辑变了,我只改了技能执行体,所有Agent同时生效,不用逐份改提示词。这种复用带来的维护收益,做三个月以上项目的人体会会非常深。
1.3 技能粒度的选择心法
技能粒度是新手最容易纠结的地方。我的经验是:技能的粗细不以"功能名好不好听"为准,而以"能否被稳定触发并明确验证产出"为准。
比如"给用户发送一条短信"是一个合格技能,它输入手机号和正文,出参是发送结果,边界清晰。"处理用户售后请求"就不是一个合格技能,因为它内部包含查单、判责、补偿、通知等多个动作,放在一起会让路由和执行都变得不可控。更合理的做法是拆成"查订单信息""计算退款金额""发起退款"等多个原子技能,再由上层编排去串。粒度判断有一个简单标准:如果一个技能内部有超过两个前置条件或超过三种结果分支,就该考虑拆分。
2. 技能描述的范式设计:从意图识别到参数契约
技能描述是路由器的眼睛。用户说了一句话,系统靠技能描述去判断该调用谁。很多项目的技能库做不好,问题就出在描述写得太随意,有的像功能说明书,有的像营销文案,导致路由准确率忽高忽低。
2.1 描述文本的三层结构
我给技能描述定义了一个固定写作范式,每个字段都不能省:触发场景、能力边界、输出承诺。
触发场景说明"当用户表现出什么意图时,你应该使用这个技能",要写具体,最好带上典型说法。能力边界说明"这个技能不能做什么、不保证什么",用来抑制模型乱用它。输出承诺说明"调用后你将得到什么结构的结果",用来避免Agent拿着技能结果不知道怎么向用户交代。
下面是我常用的技能定义格式:
{ "name": "query_order_status", "description": "当用户询问订单的当前状态、物流进度、签收情况时使用。该技能不包含退款和售后处理,如需退款请调用售后技能。返回订单当前状态、最新物流节点及预计送达时间。", "parameters": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "order_id": {"type": "string", "description": "用户提供的订单编号,必填,格式为纯数字串"}, "include_logistics": {"type": "boolean", "description": "是否需要返回完整物流轨迹,默认false,只返回最新节点"} }, "required": ["order_id"] } }2.2 参数契约:模型填参的唯一依据
参数契约这块,我的建议是直接上JSON Schema,别用自由格式的文本说明。原因很简单:JSON Schema 有类型校验、有必填约束、有描述字段,既能约束模型的输出格式,又能在执行前做程序化校验,飞掉那些模型乱填的内容。
参数描述要当作给模型看的字段说明书来写,写清楚"这个参数从哪里来、什么格式、缺了会怎样"。比如订单号,有的用户说"帮我查一下单号123456的订单",模型能提取出来;但有的用户说"我昨天买的东西到哪了",根本没有订单号,这时参数契约就要求模型回问用户,而不是自作主张填一个空值。一句话总结:参数描述写得越具体,模型填参的准确率越高。
2.3 描述质量直接影响路由准确率
我曾经做过一轮对照:同一批测试请求,技能描述用含糊的功能概括和用结构化三层描述,路由准确率从七成左右提升到九成以上。这个差异很明显,因为语义匹配在找"意思相近"而不是"字面相同"的东西。
描述里还有一个小细节:适当写反例。比如"当用户询问退款时不要使用本技能,应调用refund_order",能显著降低误触发率。模型在语义空间里对"这条技能和当前请求不匹配"的判断,很多时候不如你直接告诉它来的稳。
3. 技能注册与动态路由:让Agent在合适时机选出正确技能
有了技能定义,接下来要解决的是"怎么把用户的请求路由到正确的技能上"。这是技能库运行时的核心矛盾:注册表里躺着几十个技能,哪一个是当前请求的归宿?我在实际项目里用过好几种路由方案,最后沉淀下来的是一套"语义相似度打分 + 规则过滤 + 阈值兜底"的混合策略。
3.1 注册中心:技能的元信息仓库
注册中心不只是一个静态列表,它至少需要维护这些字段:技能名称、版本号、描述文本、参数Schema、启用状态、调用统计、可用的嵌入向量缓存。
版本号这点尤其容易被忽略。技能描述一旦改动,语义向量就要重新计算,如果没有版本管理,线上Agent可能加载到旧向量、旧Schema,造成"明明改了描述但路由表现没变化"的假象。我在某项目里碰上过这个问题,排查到最后发现是嵌入缓存没刷新,所以我现在处理技能改动时坚持走"改版本号-重算向量-灰度切流量"三步。
3.2 路由打分:语义相似度与规则过滤的组合
路由器的基本流程是:先把用户当前请求做嵌入向量化,再和所有已启用技能的描述向量计算余弦相似度,得到候选排序,然后叠加规则做精排。
规则过滤虽然简单,但很有效。比如技能A要求参数是纯数字的订单号,用户的请求里根本没有数字,那即使语义相似度再高,也应当被过滤掉。再比如技能B在后台配置了"仅在工作时间可用",那就直接排除。语义解决"像不像",规则解决"能不能",两者配合才能减少误触发。
我用一个简单伪代码描述路由核心逻辑:
def route(intent_text, skills, threshold=0.62): candidates = [] for skill in skills: if not skill.enabled: continue score = cosine_similarity(embed(intent_text), skill.semantic_vector) if not rule_filter(intent_text, skill): continue candidates.append((skill, score)) candidates.sort(key=lambda x: x[1], reverse=True) if not candidates or candidates[0][1] < threshold: return None return candidates[0][0]3.3 回退机制:没有稳赢的技能的时,宁可不选
路由最怕的不是选错,而是"明知道不靠谱还硬选"。阈值设太高,Agent会频繁拒答或让用户补充信息;阈值设太低,路由会在多个相似技能间摇摆。我的习惯是把初始阈值设在0.6至0.65之间,再根据线上样本调。
当所有候选技能得分都低于阈值时,正确做法是触发回退策略:向用户发起澄清提问,或者转给通用对话模型处理。千万不要硬选一个低分技能去执行,否则用户的真实意图会被带偏。下面这张表是回退类型与处理方式的对照,我调试时经常参考:
| 回退场景 | 典型表现 | 处理方式 |
|---|---|---|
| 候选分数普遍偏低 | 用户意图确实模糊,或涉及未覆盖领域 | 触发澄清追问,补充最多两次后转人工 |
| 语义匹配高但规则过滤全灭 | 缺少关键参数、技能不可用 | 返回参数缺失类型,让Agent主动索要 |
| 多个技能分数接近 | 语义空间重叠严重,描述区分度不足 | 推送人工评审,优先优化技能描述而非加规则 |
4. 单技能内部的执行编排:状态管理、错误恢复与重试策略
路由只解决了"选谁",真正决定项目稳不稳的,是技能执行体内部的处理逻辑。我见过太多项目把技能执行体写成一个大函数,从上到下跑一遍,没有状态概念、没有错误分类、失败了就直接抛异常。这种写法在小规模Demo里看不出问题,一旦并发量上来或者依赖的下游服务不稳定,整个Agent就跟着一起崩。
4.1 执行体应该是状态机,而不是线性脚本
技能执行体内部至少要能表达五种状态:pending(待执行)、running(执行中)、succeeded(成功)、failed(失败)、canceled(已取消)。状态迁移图并不复杂,但有了状态之后,你能回答几个关键问题:这个技能现在跑到哪一步了?失败发生在哪个环节?可不可以从某个中间点恢复?
我之前做内网告警处理技能时,执行体要依次做"解析告警内容、查询关联机器、生成处置建议、回调工单系统"。早期用线性脚本,每次回调失败整个任务从头跑,浪费大量重复查询。后来改成状态机,每个步骤完成后落一个检查点,失败时从最近的成功检查点续跑,整体成功率提升非常明显。
4.2 错误分类:该重试的才重试
错误处理的第一个原则:分类,而不是统一重试。我把技能执行中的错误分成三类:
确定性失败,比如参数校验不通过、用户提供的ID不存在。这种错误重试一百次结果都一样,正确做法是立刻终止,返回清晰的失败原因给上游。瞬时失败,比如下游接口超时、网络闪断、限流返回503。这种错误可以重试,但要有退避策略,通常采用指数退避加抖动,避免重试风暴。语义失败,比如模型返回的内容不符合输出契约、外部接口返回了违背预期的结构。这种错误需要重新尝试生成或者把问题抛给上层去做兜底,而不是傻傻重试同一份请求。
4.3 幂等设计:重试不意味着重复执行
谈到重试就绕不开幂等。重试只应该在"重复执行不会产生副作用"的前提下进行。比如一个发送通知的技能,重试只会多发一条短信,用户可能投诉;一个扣减库存的技能,重试两次可能造成超卖。所以执行体入口必须接收一个requestId,由调用方生成,执行体内部用这个ID做去重:如果发现同一requestId已经执行成功,直接返回原结果,不再重复执行。
幂等键的生成规则要稳定,比如用户ID加时间戳加动作名,确保同一个请求无论被路由几次都拿到同一个ID。这套机制在接入外部支付、短信、邮件这类服务时几乎是强制要求,少了它,任何一次超时重试都可能变成业务事故。
5. 多技能协作时的上下文传递与冲突处理
单技能做得再完善,最终还是要面对复杂任务。现实业务里很少有"一个请求只触发一个技能"的情况,更多是"查一下订单顺便算一下快递还要几天"这种连环需求。技能怎么协作,成了技能库从玩具走向生产环境必须跨过的坎。
5.1 上下文槽位:技能间怎么传话
我的做法是给系统维护一个上下文槽位集合,相当于所有技能共享的一块白板。技能启动时从白板读取需要的输入,执行完毕后把产出写回白板。槽位名要全局统一,比如order_id、delivery_time、refund_amount,不能让"A技能的出参叫orderId,B技能的入参叫order_number",否则接缝处全是转换代码。
传递规则只有一个:技能只能消费声明过的槽位,只能写声明过的槽位。每个技能定义里增加一个上下文契约字段,标明依赖哪些槽位、产出哪些槽位。这个字段配合上面的JSON Schema,就构成了技能在数据层面的完整边界。有了声明,协作逻辑才是显式的,而不是靠开发者记在脑子里。
5.2 冲突检测:多个技能同时改同一个状态怎么办
槽位传递解决了顺序依赖,但解决不了并发修改。举个例子,技能A更新了订单状态为"已发货",技能B随后根据旧的订单状态生成了一份"待发货"页面文案,用户看到的信息就自相矛盾了。
我在设计里给共享槽位加了版本号,每次写入时执行体带入当前读取的版本号,写入前做版本比对,不一致就判定冲突,中止当前技能并提示上层重新编排。这相当于给上下文加了一层乐观锁。对于技能库这种"多个独立执行体协作"的场景,乐观锁比全局互斥锁实用得多,因为技能执行时间差异很大,全局锁会严重拖慢整体吞吐。
5.3 共享记忆与局部记忆的边界
最后分清楚哪些数据放全局上下文、哪些数据只在技能内部存活。我的划分标准是:会被两个以上技能消费的、或者需要跨会话保留的,放全局上下文;其余一切中间变量都留在技能私有作用域,执行结束就销毁。
这样设计的好处是:技能之间耦合最小化、排查问题时不需要把几十个技能的内部变量全部拉出来看。我曾经把一个技能内部的临时计算值顺手写进了全局槽位,结果后续技能误读到脏数据,排查了整整一天。那次之后我定了一个规矩:只写槽位契约里声明过的字段,其余一律私有。
6. 异步技能与流式反馈:处理长任务的正确姿势
技能库在真实业务里跑一段时间后,你一定会遇到一类需求:调用一个需要几十秒甚至几分钟才能完成的任务,比如生成一份深度调研报告、批处理一堆文件、等待一个外部审批回调。这时候如果让Agent同步阻塞等待,用户的体验会非常差,超时问题也会接踵而至。
6.1 任务工单与执行器分离
解决长任务的思路是异步化:技能执行体不直接跑真正的耗时逻辑,而是先创建一个任务工单,拿到一个task_id,把实际执行交给后台worker。上游Agent拿到task_id后立即向用户回复"任务已开始",然后再通过轮询或回调方式获知结果。
任务工单至少要记录这些信息:关联的会话ID、执行状态、创建时间、完成时间、最终结果或失败原因。执行器可以是多进程、多容器甚至独立的任务队列。核心原则是:技能调用方和执行方之间只通过task_id通信,不在调用链路里同步等待结果。
6.2 流式反馈:让用户看到任务在推进
对于一个需要几十秒的长任务,只告诉用户"任务进行中"是不够的,用户会担心是不是卡住了。更好的方式是流式反馈,执行体在运行过程中持续推送进度事件。
事件协议不需要复杂,一个类型加一段负载就够用。我常用的事件类型包括:task_started、progress_update、text_delta、tool_result、task_completed、task_failed。前端Agent根据这些事件拼装消息,用户可以实时看到"正在搜集资料-已完成40%-进入汇总阶段",信任感完全不一样。流式反馈的本质不是花哨,而是把长任务的不可见等待变成可见推进,这个对用户耐心影响极大。
6.3 取消与恢复:异步任务必须能叫停
异步任务不能只能启动和等待,还必须支持取消。用户可能会中途改主意,或者重复发了一个任务,这时不能任由后台worker继续空转。
取消机制有两个关键点:一是执行体要周期性检查cancel事件,在安全点终止执行,而不是强制杀进程,否则状态和数据会不一致;二是执行体在终止前要尽量保存中间检查点,让同一task_id后续可以恢复而不是全部重跑。我一般会要求长任务执行体每完成一个子步骤就写一次检查点,这样即使取消,之前的工作也不是完全白费。
7. 从零搭建技能库的完整链路与四个高频坑
最后这部分,我想把"从零开始搭建技能库"的完整链路拉一遍,再分享四个我在项目里反复踩到的高频坑。这一节的实操性最强,建议结合前面的章节一起看。
7.1 最小可用版本应该包含什么
不要一上来就追求大而全的技能平台。我的建议是,先以最小可用闭环为目标落地四件事:
第一,选定一个业务场景,拆出三到五个原子技能,每个技能都有完整的描述、参数契约和执行体。第二,建一个极简注册中心,可以是内存里的一个JSON列表,先不管多版本和灰度。第三,实现一条路由逻辑,用语义相似度打分加阈值回退,够用就行。第四,给每个技能写一个最基础的执行日志,至少记录输入、输出、状态和耗时。
先把这四步跑通,让一个真实用户请求完整走完"路由-技能执行-结果返回"的链路,然后再逐步补监控、补版本管理、补异步化。很多项目失败是因为第一步就铺得太大,团队疲于维护技能定义却始终没有完整跑通一个闭环。
7.2 坑一:描述与参数契约对不上
这几乎是所有技能库项目的通病。描述文本暗示"用户可以不给订单号我也能查",但参数契约却把order_id标记成必填,结果模型填不出参数,用户也被反复追问,体验很差。
根因往往不是技术而是流程:技术负责人同时让两个人分别写描述和参数契约,没有做一致性review。解法是把两者放到同一个文件里评审,并且专门用"这个描述下的典型用户请求,能不能顺利填出契约要求的最小参数集"作为验收标尺。
7.3 坑二:技能之间存在隐式依赖
技能A"查询订单详情"需要在技能"解析用户身份"完成后才能正确工作,但路由层面并没有强制这个顺序。某次用户换了个说法直接触发了技能A,执行体因为缺少用户身份上下文而失败。
这类问题看起来像执行错误,本质是依赖没有声明。我的建议是技能定义里显式声明前置依赖技能名,执行前由编排层检查依赖是否满足,不满足就先补执行依赖技能。这个检查看起来额外多了一步,但可以避免大量隐晦的时序Bug。
7.4 坑三:路由评测缺失
没有评测的路由就是盲调。我见过团队上线了几十个技能,但从不记录路由日志,出了误触发只能靠用户投诉来发现。没有量化的准确率、召回率和误触发率,做任何优化都不知道是变好还是变坏。
解法是给技能库建一个离线评测集,每个技能准备二十到五十条代表性的用户请求语料,标注正确的技能归属。每次调整技能描述、新增技能或修改阈值时,先跑一遍评测集,看各项指标变化再决定是否上线。这套流程看起来朴素,却能让技能库持续演进不失控。
7.5 坑四:技能数量膨胀失控
技能库活下来之后,技能数量一定会涨。涨到某个数量后,你会开始遇到两个技能描述高度重叠,同样一句"查一下我的优惠券"在两个技能之间随机横跳。根本原因是业务功能细分后,描述没有跟着差异化,语义空间撞车了。
养成一个习惯:新增技能之前,先跑一遍现有技能的语义相似度矩阵,如果新增技能和某个现有技能的描述相似度高于0.85,就必须把两者区别写清楚,否则宁可不加。这个矩阵也可以做成一个定期的健康检查任务,帮助及时发现技能分叉和重复。
几个项目做下来,我最大的体会是技能库的规范不是一次设计出来的,而是跟着业务迭代不断收敛的过程。最开始别指望定义出完美无缺的技能框架,先让三五个技能跑通一条完整的用户路径,再逐步扩展边界、处理冲突、补齐评测。最后分享一个小技巧:给每个技能维护一份独立的建议用例文档,记录它被误触发和正确触发的典型对话样本,新技能上线之前翻一遍,很多路由问题能提前避开。