1. 为什么Agent需要一套独立的“技能体系”
1.1 从“提示词堆砌”到“技能原子化”的转变
我最早做Agent的时候,思路特别朴素:把所有工具描述写进System Prompt,再把例子塞进去,让模型自己决定什么时候调用、怎么调用。最初几个场景确实能用,可一旦工具数量超过十五个,问题就接踵而至。模型开始混淆功能相近的工具,比如“查询订单状态”和“查询物流信息”在模型眼里几乎长得一模一样,偶尔还会把A工具的入参拼到B工具上。你让模型自由发挥,它就真的开始自由发挥。
后来我意识到,Agent的能力边界不应该靠提示词硬撑,而应该靠一套结构化的“技能体系”来管理。这也是agent-skills这个名字想表达的核心思想:把Agent能做的事情拆成一个个职责清晰、边界明确、可独立验证的技能单元,再通过统一的注册、描述、调度机制把这些技能串联起来。技能单元就像乐高积木,单块看起来很简单,但组合方式决定了Agent最终能解决多复杂的问题。
做了几个月的“提示词堆砌”之后,我踩出来的结论很直接:技能化是一次性的结构化投入,带来的收益却贯穿Agent的整个生命周期。举一个我印象最深的例子。早期做个客服Agent,每天被模型瞎调用工具搞得焦头烂额,后来我把“查订单”“算退款”“转人工”拆成三个独立技能,每个技能写清楚触发条件、必备参数、返回格式,模型的表现立刻上了一个台阶。工具数量没变,变的只是组织方式。
1.2 技能体系解决的核心问题
一个设计良好的Agent技能体系,解决的不是“能不能调用工具”这个基础问题,而是三个更深层的痛点。
第一个痛点是能力边界混乱。没有技能化的时候,Agent的能力是所有工具描述的集合,模型很难判断哪些是它该做的、哪些不是。技能化之后,每个技能就是一个明确的“行为契约”,模型看到的是“我可以做什么”,而不是“我面前有什么工具”。
第二个痛点是可观测性差。传统Agent调用工具,成功失败全看模型心情,出了问题极难定位。技能体系要求每个技能都有标准化的入参、出参、错误码,执行过程中每一个环节都可以被记录、追踪、回放。我后面做的监控告警,全是建立在技能执行日志之上的。
第三个痛点是复用性低。代码里的函数可以到处复用,但Agent的能力很难从一个项目搬到另一个项目。技能体系把“能力”和“业务流程”解耦成两层:技能层是通用的,比如“查天气”“算运费”“发邮件”谁都能用;流程层才是业务相关的,比如“处理退货申请”要串起三个技能。这样换项目时,大部分技能可以直接迁移,要重写的只有流程编排。
我给团队定过一个原则:任何新增能力,如果不能在三天内接入技能体系,那就说明这个能力的设计有问题。这个原则帮我们挡掉了不少临时凑合的实现,也逼着我们把技能设计想得更清楚。
2. 技能的结构化设计与描述规范
2.1 技能描述Schema的必填字段
技能描述是整个体系的基石。模型能不能在合适的时机选中合适的技能,全靠这份描述写得够不够清楚。我自己用下来,一份合格的技能描述至少要有七个字段:技能ID、技能名称、功能描述、触发条件、入参Schema、出参Schema、错误码定义。
{ "skill_id": "order_query", "skill_name": "订单状态查询", "description": "根据订单号或用户ID查询订单当前状态,包括待支付、已支付、配送中、已完成、已取消等状态。", "trigger_conditions": [ "用户询问订单现在到哪一步了", "用户想知道某笔订单是否已经付款", "用户反馈订单状态与预期不符,需要核对" ], "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,形如ORD20250101xxxx" }, "user_id": { "type": "string", "description": "用户ID,当不提供订单号时必须提供" } }, "required": ["order_id"] }, "output_schema": { "type": "object", "properties": { "order_status": { "type": "string", "enum": ["pending", "paid", "shipping", "completed", "cancelled"] }, "status_desc": { "type": "string", "description": "状态的中文描述" }, "estimated_arrival": { "type": "string", "description": "预计送达时间,仅配送中/已完成状态返回" } } }, "error_codes": { "ORDER_NOT_FOUND": "订单不存在,需要向用户确认订单号是否正确", "USER_NOT_FOUND": "用户ID不存在", "SERVICE_UNAVAILABLE": "订单服务暂不可用,请稍后再试" } }功能描述看起来简单,写起来最讲究。我试过两种风格。一种写得很宽泛,比如“查询订单相关信息”,结果模型在用户问“我昨天买了什么”的时候也调了它,返回了一堆订单号,用户看得一头雾水。另一种写得很具体,把适用场景、不适用场景全列出来,模型的判断准确率明显高很多。
触发条件字段特别容易被忽略,但它恰恰是帮助模型做“技能选择”的关键。我习惯把常见的用户问法直接写进触发条件里,模型的Few-shot其实就藏在这些自然语言描述中。模型看到“用户询问订单现在到哪一步了”,比看到抽象的“功能描述”更容易做出正确选择。
2.2 入参与出参的约束设计
入参和出参的约束设计是最见功夫的地方,也是我踩坑最多的地方。早期我图省事,入参只写字段名和类型,不写描述,结果模型经常把“用户ID”填进“订单号”里。后来我把每个参数都加上描述、格式约束、示例值,模型调用准确性直接提升了一个档次。
入参设计有一个原则:宁可多写约束,不给模型自由发挥的空间。比如订单号这个字段,如果系统里订单号一定是“ORD”开头的一串字符,那就把正则约束写上;如果某个参数要求是数字字符串,那就明确写“不要传number类型,请传string类型的数字”。模型对类型映射的处理比你想象中粗心得多,你多写一行描述,线上就能少接到几个报障。
出参设计同样不能含糊。我遇到过最典型的问题:技能返回了数据,但模型不知道怎么提取关键信息回复用户。后来我给每个出参字段都写清楚了“用户真正关心的是什么”。比如订单状态查询,状态枚举值只是中间数据,用户真正关心的是“我的货什么时候到”,所以“estimated_arrival”字段要明确标出来。这样模型在组织回复时,会优先使用这些字段。
还有一个反直觉但非常有用的经验:出参宁可冗余,不要精简。早期我觉得返回太多字段会占用上下文,所以出参做得特别精简。后来发现,模型在组织自然语言回复时需要各种维度的信息,一旦缺失某个字段,就会自己脑补,而脑补的结果往往不可控。多返回几个字段,让模型自己挑选需要的信息,远比让它猜要可靠。
2.3 技能元信息与依赖声明
除了必填字段,技能体系里还应该包含一些面向系统、面向编排器的元信息。我常用的是四个维度:调用成本、执行耗时、权限等级、依赖关系。
调用成本用来记录一次技能调用大约消耗多少Token或者调用多少次外部API,这个信息对后续做技能编排、路由决策很有价值。比如用户问了个很简单的问题,Agent会优先选择成本低的技能,而不是把所有相关技能全调一遍。
执行耗时也值得记录。我在做异步任务编排时,需要估算整个流程的总耗时,技能的耗时数据就是计算基础。比如“生成报表”这个技能平均要跑20秒,那流程编排器就会把它放到后台任务,而不是让用户在前端干等。
权限等级这个字段非常重要。不是所有技能所有用户都能调用的。比如“查询用户手机号”这种技能涉及隐私信息,可能只有客服主管角色才允许调用。我把权限信息写进技能元数据,由调度层统一校验,而不是让模型自己去判断能不能调。模型判断权限这件事,我试过,完全不可靠。
依赖声明描述的是技能之间的调用关系。比如“计算退款金额”这个技能依赖“查询订单状态”和“查询支付流水”两个技能。有了依赖声明,编排器才能自动拆解任务、检查技能是否可用、计算整体执行计划。依赖关系我建议用DAG来描述,后面做并发优化、失败重试都方便。
3. 技能的注册、调度与执行链路
3.1 技能注册与索引
技能写好了,接下来要解决的问题是:Agent系统里挂了上百个技能,怎么让模型在这么多技能中找到最合适的那个?
把所有技能描述全部塞进Prompt肯定不现实,上下文窗口撑不住,模型也会被海量信息搞晕。我用的方案是两级筛选。第一级是用检索的方式从技能库中召回候选技能,第二级才是把候选技能的描述交给模型做最终决策。
技能注册时,我会为每个技能建立索引元数据,包括技能名称、功能关键词、触发场景词、服务等级等。检索时,把用户当前的问题翻译成查询向量或者关键词,从技能库里召回最相关的Top N个技能。这里Top N我一般设置为8到10个,太少了容易漏掉真正需要的技能,太多了又会让模型选择困难。
这个方案上线之后,效果非常明显。技能数量从十几个涨到七八十个,模型的选择准确率不但没有下降,反而因为每次只看到候选技能,选择干扰变少,准确率提升了。这也验证了我的一个判断:限制模型的选择范围,比增强模型的推理能力更能提升稳定性。
技能注册还有一个容易忽略的细节——技能下线管理。业务调整时,某些技能可能不再使用,但用户的历史会话里仍然可能提到相关需求。如果直接删除技能,模型遇到这类需求就完全没招了。我建议技能表里维护一个状态字段,用“可用”“下线”“灰度”三个状态管理,下线的技能保留描述信息但不参与检索召回,这样既不影响线上效果,又能随时回溯。
3.2 模型如何“看到”技能:上下文窗口中的技能清单
模型能看到什么,决定它能做什么。技能体系最终要解决的问题,就是让模型在同一时刻看到的内容足够精准、足够结构分明。
我在构建Agent上下文时,技能部分固定采用三段式布局:候选技能列表、技能调用规则、当前任务上下文。候选技能列表就是由检索召回出来的技能描述,每份描述就是上文定义的Schema。技能调用规则是全局的,告诉模型什么时候应该调用技能、什么时候不应该调用,以及调用失败后应该怎么处理。当前任务上下文则记录了用户输入、之前几轮的对话摘要、已经执行过的技能结果。
[候选技能列表] 1. skill_id: order_query 功能:查询订单状态 ... [技能调用规则] - 当用户表达的需求与某个技能的description高度相关时,调用该技能 - 当用户表达的需求在候选技能列表中找不到匹配项时,直接回复无法处理,不要编造结果 - 当技能调用失败时,将error_codes中的原因向用户说明,并引导用户重新描述或转人工 - 一次只调用一个技能,等待结果后再决定下一步动作,禁止并行发起多个技能调用 [当前任务上下文] 用户:我刚下的单现在到哪了? 历史对话:... 已执行技能:无规则这块我优化过很多轮。最初我加了一条“如果多个技能可以同时满足用户需求,选择一个最合适的即可”,结果模型经常犹豫不决,反而频繁编造结果。后来我把这条改成“一次只调用一个技能,等结果再决定下一步”,模型才稳定下来。对现阶段的大语言模型来说,串行执行比并行可靠得多,等结果再决策也符合人类处理任务的直觉。
3.3 技能执行与结果回填
模型决定调用某个技能后,执行链路就进入了技能运行时。这一层的职责是把模型生成的JSON格式调用请求转换成真实的函数调用、API请求或服务调用,再把执行结果回填给模型。
执行链路中最关键的环节是入参校验。模型生成的参数经常不符合Schema要求:类型不对、格式不对、必填字段缺失。我在这一层加了两道校验。第一道是程序化校验,严格比对JSON Schema;第二道是语义校验,比如手机号是不是11位、日期是不是合理范围。两道校验都通过了才会真正执行。
校验失败时的处理策略也很重要。早期我让流程直接报错,把错误抛回给模型,让模型自己修改。这个方案的问题是模型会反复修改多次,浪费时间和Token。后来我改成“自动修复”策略:如果是格式层面的小问题,比如日期格式少了个前导零,系统自动帮模型修正;如果是逻辑层面的错误,比如把用户ID填到了订单号字段,那就返回错误码加修正建议,提示模型重新生成请求。
// 模型第一次生成的调用请求(错误示范) { "skill_id": "order_query", "parameters": { "order_id": 123456789, // 错误:传成了number类型 "user_id": "u_1002" } } // 程序化校验拦截,自动修复为 { "skill_id": "order_query", "parameters": { "order_id": "123456789", // 修复:转成string类型 "user_id": "u_1002" } }结果回填时,我会把技能执行结果包装成统一格式,包括执行状态、业务数据、耗时、Token消耗、相关的错误信息。这样模型看到的并不是裸数据,而是带上下文的数据,它更容易理解如何基于这些数据组织回答。回填内容我也会尽量保持精简,只保留当前对话需要的信息,避免噪音字段干扰模型判断。
4. 多技能协同与编排实战
4.1 编排模式:顺序、并行、条件分支
单技能调用只能解决简单问题,真实业务场景中,一个需求往往需要多个技能配合完成。技能编排要解决的就是:怎么把多个技能组织成一个可预测、可控制、可维护的流程。
我常用的编排模式有三种:顺序执行、并行执行、条件分支。顺序执行最简单,一个技能的结果作为下一个技能的输入,适合流程固定的场景,比如“生成采购单”要先“查库存”再“算价格”。并行执行适合多个技能之间没有依赖关系的场景,比如用户问“今天天气怎么样,顺便帮我看看明天的会议安排”,可以同时调用天气技能和日历技能,减少整体耗时。条件分支则是根据不同情况走不同的技能路径,比如用户问订单问题,先判断订单是否存在,不存在就转入人工。
实际工程中,我的建议是:能用顺序解决的就别并行,能用规则解决的就别让模型决策。模型不是不能处理并行和分支,但在多技能场景下,模型每一步决策都可能出错,链路越长、分支越多,错误累积的概率越大。为了让流程更稳定,我把有些分支判断前置到代码层,比如先判断订单号是否存在再做后续流程,而不是让模型去判断。
4.2 一个典型的跨技能任务拆解
用一个真实场景来演示跨技能编排。假设企业内部的智能助手收到一条需求:“帮我查一下研发部这个月的打车报销有哪些人超了标准,然后通知他们重新提交。”
这个需求在代码层面会被拆成六步:
- 调用“查询部门人员”技能,获取研发部全员名单及其管理者信息
- 调用“查询报销记录”技能,获取研发部本月的打车报销记录
- 代码层根据公司报销标准(比如每月上限800元),过滤出超标的记录和金额
- 对每个超标员工,生成一条“重新提交报销单”的通知,对应“发送站内通知”技能
- 最后汇总通知结果,生成一份给管理者的摘要
第1步和第2步之间没有依赖关系,可以并行执行。但考虑到企业内部系统的限流策略,并行执行如果同时打两个接口,可能触发频率限制,所以实际我仍然采用了顺序执行。这个例子说明了编排设计中很重要的一点:技术上的最优解不一定是系统上的最优解,限流、稳定性、成本都需要综合权衡。
流程执行完之后,Agent把结果回填给用户:“研发部本月共有3人打车报销超标,分别是张三(超120元)、李四(超45元)、王五(超67元),已发送重新提交的通知,平均处理时长1.2秒。”这就是一次完整的跨技能协同。
4.3 编排器的设计与容错机制
编排器是承接“拆解”和“执行”的中间层。它接收模型输出的技能调用序列,把它们转换成实际可执行的任务图,然后统一调度、监控、容错。
任务图的数据结构我用的是DAG。每个节点对应一个技能调用,边代表依赖关系。DAG的好处是可以直观地看到哪些环节能并行、哪些环节必须等前置节点完成。调度执行时,编排器维护一个就绪队列,入度为0的节点就可以先跑,跑完释放下游节点的依赖计数,循环往复直到整个图执行完毕。
容错机制上,我设置了两个层级。第一层是单节点重试,技能调用失败时最多重试3次,采用指数退避策略,间隔分别是1秒、2秒、4秒,避免瞬时故障反复触发。第二层是整个流程的超时控制,流程总耗时超过30秒就中断返回最后一次局部结果,配合前端走异步轮询。这两层机制上线以后,线上流程的失败比例下降了八成。
还有一个细节是我后来才加上的——部分成功策略。比如并行执行三个技能,其中两个成功、一个失败,这时候不应该让整个流程都回滚。我会记录已成功的部分结果,把失败节点的错误信息单独返回给模型,让它判断是继续基于部分结果处理,还是让用户重试。这种“partial success”的模式更贴近真实世界的容错逻辑,模型也能更好地理解当前状态,做出合理的下一步决策。
5. 技能评估、迭代与踩坑记录
5.1 评估指标体系:效果、成本、稳定性
技能体系上线之后,质量怎么度量?我搭建了一套自己的评估看板,核心关注三个维度:效果、成本、稳定性。
效果维度主要看技能调用的准确率、任务完成率、用户满意度的间接指标。准确率指的是模型选对技能的比例,我会对历史对话做抽样标注,统计模型选错技能、漏选技能、多选技能的情况。任务完成率则看用户最终是否拿到了想要的结果。
成本维度看的是Token消耗、API调用次数、平均单次请求处理时长。技能体系上线后,我明显感觉到成本结构变了:虽然引入了检索和技能调度的额外开销,但模型因为减少了瞎猜,重试次数变少,整体Token消耗反而下降了。这个数据在项目汇报的时候特别好用。
稳定性维度看的是技能执行的成功率、超时率、报错分布。针对每个技能,我都统计它的调用次数、失败次数、失败原因。如果某类错误反复出现,比如“参数校验失败”一直排最高,那就说明技能的Schema描述有问题,需要迭代优化。评估看板我要求团队每周过一遍,不为了看数字而看数字,重点是发现优化机会。
5.2 常见问题与排查实录
做技能体系这么久,我踩过不少坑,挑几个最具代表性的记录在这里。
问题一:模型频繁选错相似技能。症状是两个技能的功能描述高度相似,比如“查订单进度”和“查物流信息”,模型经常搞混。排查之后发现,问题不在模型,而在技能描述太过模糊。我把两个技能的触发条件细化,分别补上更多业务场景和反例,准确率立刻上来了。现在我的规则是:如果两个技能的描述有超过30%的相似度,就必须重新设计。
问题二:参数明文错误反复出现。系统已经做了程序化校验和自动修复,但有些错误还是会直接暴露给用户。后来发现是语义校验没生效。比如日期参数,程序化校验能拦截非日期格式,但拦不住“2025年2月31日”这种不存在的日期。我新增了一层针对枚举值、范围值的规则校验,把常见的业务非法值都拦在技能执行之前。
问题三:长对话中技能选择漂移。用户跟Agent聊了很久之后,模型越来越容易调用错误的技能。排查发现,对话轮数多了,上下文中的历史信息会干扰模型的当前判断。我采取的方案是:每轮对话开始时,把历史对话压缩成摘要,只保留当前轮次的核心意图,并强化技能调用规则的权重。这个改动上线后,长对话场景的准确率显著拉回来了。
问题四:模型编造技能执行结果。这是最严重的问题。有个场景是Agent调用了“查库存”技能,技能服务超时,但模型没有等待真正的结果,而是自己“脑补”了一个结果返回给用户。排查之后,我在技能调用规则里加了一条硬性约束:技能未返回结果或返回错误时,禁止向用户展示推测性结果,只能返回错误信息。同时在编排器层加了超时拦截,技能执行超过预期时限后强制返回错误状态,不给模型编造的机会。
问题五:单技能耗时过长拖垮整体响应。有些技能涉及复杂的后端计算,单次执行可能要好几秒。在串行执行的链路里,一个慢技能会拖慢整个流程。我的解法是把这类重计算技能单独摘出来,凡是耗时超过3秒的技能,统一降级为异步执行模式。流程拆成两段:先快速返回用户中间状态,任务完成后再通过消息通知结果。这个改动对体验的提升非常明显。
5.3 技能体系的上线节奏与灰度策略
技能体系的引入,和业务系统上线还不一样,它直接影响的是Agent的每一条对话行为。我没敢做一次性全量切换,而是分了三步走。
第一步是影子模式,新旧两套逻辑并行跑,但用户侧看到的还是老逻辑。新技能体系在后台记录决策日志,我通过对比新旧逻辑的决策差异,判断技能体系的选技准确率、执行成功率是否达标。这一步的关键是日志要打全,因为后续所有的优化依据都在这批日志里。
第二步是灰度放量,先切5%的流量到技能体系。我会特别关注两个指标:真实用户的高投诉问题数量和错误率的分布。如果5%流量下的错误率比旧逻辑高,就停止放量,回头查日志。灰度放量阶段最容易发现的是极端边界情况,比如某些用户会问出各种奇怪的业务场景,而这些场景在离线测试中根本覆盖不到。
第三步是全量切换,但这不代表结束。全量之后我仍然保留了一个应急开关,一旦发现问题可以随时把全部流量切回旧逻辑。这个开关直到新体系平稳运行了一个月之后才摘除。做Agent系统,永远要把“兜底方案”作为默认前提。
5.4 几个容易忽略但影响极大的设计细节
最后分享几个容易被忽略、但对整体效果影响极大的设计细节。
第一,技能描述的语言风格要保持统一。我见过有的技能描述很口语化,有的又写成技术文档,模型对这种风格差异很敏感。统一的语言风格能降低模型的认知负担,建议团队维护一份技能描述的写作规范,明确禁止“大概”“可能”这些模糊词汇,每个功能描述都要写清楚边界条件。
第二,技能之间的命名要避免相同前缀和相似词。有一次我建了“send_email”和“send_emails”,区别只是一个复数,模型频繁混用。后来我强制规定技能ID必须具有语义区分度,不能只用单复数做区分。这个规范很小,但节省的排查时间非常多。
第三,技能执行日志一定要结构化。日志里除了记录成功失败,还要记录模型当时看到的上下文摘要、候选技能列表、最终选择了哪个技能、参数是什么。有了这些结构化日志,后面做问题复盘、数据报表、模型调优,都会轻松很多。我的很多优化方向就是从这些日志里盯出来的。
第四,技能数量不是越多越好。我见过有人为了追求“功能完善”,一口气挂上上百个技能,结果模型的选择准确率直线下降。技能粒度上,我的经验是:把功能内聚、上下文相关的操作合并成一个技能,把功能独立、参数不同、验证逻辑不同的操作拆成不同技能。技能不是越细越好,合适的粒度是“一技能一职责,信息不冗余”。
Agent技能体系的建设是一个持续迭代的过程,从Schema设计到调度执行,从评估看板到灰度上线,每个环节都有大量细节需要打磨。这套方法论到现在已经支撑了我这边多个业务线的Agent应用,也是我在无数个“模型表现不稳定”的夜晚里,一点点试出来的经验沉淀。希望这些内容能帮你少走一些弯路。