ARTICLE DETAIL

资讯详情

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

Agent技能系统实战:从工具堆叠到编排设计的完整指南

Agent技能系统实战:从工具堆叠到编排设计的完整指南 1. 为什么 Agent 需要一套技能系统而不是单纯堆 Prompt还记得我第一次给内部知识库助手加工具函数的时候系统里一共只有 3 个工具查文档、查联系人、建工单。模型选得又快又准几乎零失误。我心里想这不挺简单的嘛。于是陆续把搜索、权限校验、日程查询、报表生成……一个个往里加加到第 15 个工具的时候问题开始像雨后春笋一样往外冒用户问帮我看看库存还剩多少模型去调了订单创建接口用户说打印今天的销售报表模型居然把参数拼成了一串乱码。查日志的时候我很困惑单拿任何一个工具出来做测试模型都能正确调用为什么工具一多就乱套后来我开始研究 agent-skills 这类把工具集合升级为技能系统的做法才意识到问题不在于模型推理能力而在于我的组织方式——把一堆工具堆在同一个命名空间里让模型自己猜这本就是一种很低效的设计。1.1 工具堆积的临界点从 8 个工具开始失控我自己的实测数据是这样的工具数量小于等于 8 个时模型选择准确率基本稳定在 90% 以上超过 10 个之后开始明显下滑到 15 个左右准确率会掉到 70% 上下。这还是在我仔细写了每个工具描述的前提下。为什么会这样拆开看有三个原因。第一是上下文稀释。每个工具的定义至少占 200~400 token15 个工具就是 3000~6000 token。这些 token 挤占的是对话上下文和示例空间模型在长上下文中更容易忘记某些工具的存在。第二是描述歧义。工具越多描述之间的边界越模糊。比如查询订单列表和查询订单详情听起来很像但参数和返回结构完全不同。模型不是不理解而是面临的选项太多区分成本成倍上涨。第三是选择负担转移到了模型身上。工具堆在一起意味着模型每次决策都要在全部工具里做一次全局匹配。人做选择题时选项超过十几个也会烦躁模型其实也一样。所以你会发现单纯往提示词里堆工具很快会触到天花板。Skill 化的思路核心就一句话不要让模型面对一堆函数而是让它面对一组有边界、有层次的能力单元。这也是 agent-skills 这类项目真正想解决的问题。1.2 技能与工具的本质区别要理解技能系统先得分清两个概念。工具Tool是一个可被调用的函数。它回答的问题是我能做什么操作。技能Skill是一个完整的、可独立发布和复用的能力单元。它回答的问题是在什么条件下我应该用怎样的一套动作来完成什么目标。打个比方。工具是一把螺丝刀技能是一个电工换插座指南。指南里包含了什么情况该换插座、需要哪些工具、操作顺序是什么、遇到什么情况要停手。Agent 拿到的是指南本身而不仅仅是螺丝刀。从工程实现上看技能通常包含这几个部分技能元信息名称、版本、所属领域、适用条件描述。输入输出契约参数类型、约束、返回结构。这部分直接决定了模型能不能正确调用。执行逻辑可能是一个工具函数也可能是一段多个底层操作的编排。校验与错误反馈入参校验、失败原因分类、给模型的可修复提示。权限与审计信息这个技能能访问什么数据、调用后怎么记录日志。把工具升级成技能本质上是在模型和底层操作之间加了一个语义层。模型不需要知道技能内部调用了几个接口只需要知道这个技能处理什么场景、输给我什么结果。这个语义层做得好模型的决策负担会大幅下降。1.3 技能系统带来的三个直接收益收益不是理论上的我落地之后体会很深。首先是提示词瘦身。技能系统上线后我的主提示词从 8000 多 token 降到了 2000 出头。系统提示词里只保留角色设定、通用规则和一个技能清单指引具体技能描述全部放进函数定义或注册表里按需加载。上下文空出来了模型对指令的遵循度也明显提升。其次是准确率回升。把相似的、经常被混淆的操作合并成技能并给出清晰的适用边界后我那个 15 工具系统的选择准确率从 70% 回到了 88% 左右。而且由于每个技能内部可以处理参数归一化模型传参的格式错误率也降了一大截。最后是并行开发效率。以前改一个工具的行为要担心影响全局技能化之后每个技能有独立版本团队不同人可以同时改不同技能通过注册表做好版本声明和灰度。这个感受很像从单体应用拆成微服务拆的那一刻是有成本的但拆完之后团队的机动性完全不一样。2. 技能系统的基础设施从技能注册表到运行时调度理解了一个技能是什么接下来要回答的是这些技能放在哪里、Agent 怎么发现它们、调用时谁来兜底。我参照 agent-skills 的模块化思路把技能系统分成了四层注册表、发现器、调度器、执行引擎。下面逐个说。2.1 技能注册表所有技能的通讯录注册表是一个集中存放技能元数据的地方。最简单的实现是一个目录加若干 YAML 文件复杂一点可以做成带数据库和版本管理的服务。不管哪种形式每个技能都需要有一段类似这样的定义name: query_inventory version: 1.4.0 domain: inventory description: 当用户询问某个 SKU 的实时可用库存、剩余数量、缺货状态时使用。 如果用户只是问商品价格禁止使用本技能。 input: sku_id output: available_qty / warehouse_id / updated_at input_schema: sku_id: type: string required: true pattern: ^[A-Z0-9-]{5,20}$ description: 商品唯一编号如 SKU-100234 warehouse_id: type: string required: false description: 仓库编码缺省时查询全部门店 output_schema: sku_id: string warehouse_id: string available_qty: integer updated_at: string permissions: read:inventory tags: [inventory, order, after_sale]这几个字段不是摆设。description是给模型看的决定模型在什么时机选中它怎么写我后面专门讲。input_schema是给模型传参用的约束越细模型越不容易发挥。permissions是给执行层做权限校验用的一个技能可能涉及多个底层接口权限写在技能层比写在工具层更合理。tags供检索使用当技能数量大了之后你可以基于标签做粗粒度过滤。注册表设计的关键点是技能必须可以独立注册、升级、下线并且每个版本都要有记录。这样你才能回答这次模型行为漂移是不是因为某个技能从 1.3 升到了 1.4。2.2 发现与路由模型如何从几十个技能中选中正确的那个注册表建好之后下一环是发现机制。把全部技能描述都发给模型属于最粗暴的做法——技能一多问题又回来了。所以发现层要做收敛。我的做法是分两级。第一级叫技能域路由先把技能按 domain 分组比如库存、订单、物流、售后。系统会根据用户当前对话的主题先选出 1~2 个候选域。第二级才是把候选域内的技能描述发给模型做精确选择。这样做的直接效果是不管全局有多少技能模型每次完成选择时面临的候选集都被控制在了 5 个以内。选择范围越小准确率越稳定。你可以理解为——先按楼层找部门再在部门里找具体负责人效率远高于一进大门就喊名字。如果连 domain 都不好判断比如用户问得特别开放可以再加一道基于语义检索的召回把技能描述向量化用用户的输入去检索 Top-K 个相关技能把这 K 个技能作为候选发给模型。这一步在技能超过 50 个之后几乎必做。2.3 执行引擎超时、重试与幂等技能被选中之后执行引擎负责兜底。我对执行引擎的要求有三条。第一是输入校验前置。在真正调用业务逻辑之前先用注册表里的 schema 对模型传过来的参数做一次校验。格式不对就直接返回结构化错误让模型重传。这一步能避免大量脏数据进入下游系统。第二是超时与重试策略。每个技能都要声明自己的超时预算比如默认 30 秒长任务走异步。网络类错误可以重试 1~2 次业务类错误一律不重试直接返回错误码。重试逻辑必须放在技能执行体内部不要让模型感知到你刚才那次失败了请再试一次——那会白白消耗许多 token。第三是幂等性设计。技能应该设计成同一个请求执行两次结果一致且不产生副作用。比如创建工单这个技能入参里必须允许传入幂等键执行层用幂等键去重。否则模型在超时后重试一次用户就会看到两张一模一样的工单。这个坑我在生产环境里踩过代价不小。还有一个特别容易被忽略的点失败反馈必须结构化成模型能读懂的形式。不要返回一大段异常堆栈而要返回类似下面这样{ error_code: INVALID_SKU, hint: sku_id 不存在请检查编号格式后重新调用, retryable: false }模型看到retryable: false就知道换个方式处理看到hint就能直接用来修正自己的下一次调用。这套设计能让 Agent 的主动纠错能力上一个台阶。3. 技能拆分的艺术原子性、描述与契约设计技能系统的质量七成取决于技能本身设计得怎么样。这一节我想聊三个关键点怎么拆、怎么写描述、怎么定入参契约。3.1 原子性一个技能只做一件事很多人在设计技能时习惯做大而全。比如生成月度销售报表这个技能内部逻辑是聚合数据、计算同比环比、渲染图表、生成 PDF、发送邮件。一条链路五件事全塞进去。这个做法短期看着省事长期会很痛苦。因为渲染图表这个能力知识库问答场景可能也用得上发送邮件这个能力售后工单场景也要用。一旦你把这些能力拆在报表技能里面其他场景就无法复用。我建议的拆分标准就三条这个技能能否独立测试如果必须依赖其他技能才能验证说明边界没划清。这个技能是否会被至少两个场景复用如果是拆出去。这个技能的用途能否在 300 字以内讲清楚讲不清楚说明它承担了过多职责。按这个标准生成月度销售报表应该拆成多个原子技能aggregate_sales_data、render_chart、generate_pdf、send_email、assemble_sales_report。前面四个是被复用的原子件最后一个是当前场景特有的编排件。这样每个技能都能独立迭代、独立测试出问题时也容易定位。3.2 技能描述的写作方法让模型一眼知道何时用它技能描述是给模型看的说明书写得好不好直接影响触发准确率。我见过太多团队把描述写成功能罗列比如本工具用于处理库存相关操作。这种描述等于没写——模型根本不知道在什么场景、什么意图下该触发它。我的经验是描述要以触发条件为中心并且包含正反两个边界。正例是这样的写法当用户明确询问某个 SKU 或商品的实时可用库存、缺货状态、各仓余量时使用。当用户询问的是商品价格、销量趋势或采购建议时禁止使用本技能。这个描述里包含了两层信息什么时候该用、什么时候不该用。模型在做决策时不该用的排除信息往往比该用的正面描述更有价值它能帮模型排除相似但不同的场景减少误触发。另外一个实用技巧是在描述末尾附上一个简短的调用示例。比如用户问这款还有货吗时应传入商品详情页中的 SKU 编号。这相当于给模型提供了一个 few-shot 锚点实测下来对参数字段的提取准确率有明显帮助。写完之后一定要做验证。我每次写完描述都会拿 20 条真实用户问题和 20 条反向问题不该触发该技能的问题跑一遍统计触发率和误触发率。一正一反两条线都过了才算合格。3.3 输入输出契约宁可严格不要宽松很多技能出问题根源不是模型笨而是你给的参数 schema 太宽松。举个例子status字段你只写了一个string没给枚举值。模型就会自由发挥传一个awaiting_confirmation而后端只认PENDING两者匹配不上一个好好的流程就断了。所以参数契约的设计原则是能枚举就给枚举能正则就给正则能规定格式就规定格式。哪怕你觉得某个字段明摆着就是字符串也建议加上格式约束。模型看到约束之后生成的参数格式准确度会高很多。输出契约同样重要。技能返回什么结构必须稳定。因为上层编排逻辑要靠output_schema来决定下一步走哪个分支。如果同一个技能有时候返回available_qty、有时候返回quantity编排层就乱了。输出字段的命名、类型、是否可能为空都要在技能定义里写清楚并且用执行层的返回包装统一包裹。4. 技能编排顺序、条件、并行与它们的失败模式单技能能做的事很有限Agent 真正的价值来自技能的编排组合。这一节我讲讲四种常见编排模式以及每种模式在工程上要注意什么。4.1 顺序编排把流程画成显式 DAG顺序编排是最可靠、也最常用的一种一个技能执行完把结果喂给下一个技能。比如订单退款流程校验订单→计算退款金额→创建退款单→通知用户。顺序编排的关键是显式化。所谓显式化就是流程的每一步、每个分支都提前定义好而不是把整条流程丢给模型去自由发挥。这样做的最大好处是可控、可审计出问题的时候你知道问题出在第几步。工程上有三个细节。第一每步之间要定义好数据传递格式前一个技能输出的字段名必须和后一个技能输入的字段名对得上这一步在编排定义里就要声明。第二要设置整条链路的超时预算不能所有技能都跑完 30 秒才发现整体超时应该在编排层做总预算控制。第三每一步都要有失败分支的处理规则失败时是终止整个流程还是跳到某个降级技能这个必须在编排定义里写明不能让模型临时决定。4.2 条件与并行提高吞吐的代价条件分支解决的是不同情况走不同路径的问题。比如工单来自财务部门就走财务审核技能来自技术部门就走技术处理技能。实现上就是在两个技能之间加一个路由节点根据前一个技能的输出字段做匹配。并行编排解决的是多个独立信息源的问题。比如用户问一个商品的综合信息你可能要同时调查库存、查价格、查物流三个技能等全部返回后再汇总。并行的代价有两个一是模型在发起并行调用时需要一次性生成多个函数调用的 JSON这些 JSON 本身占用大量输出 token生成过程也更慢二是多个技能同时执行任何一个失败都会引出一致性问题——到底是整体失败回滚还是部分成功继续我的经验是并行调用的技能数量控制在 3 个左右超过就得不偿失。并且要提前约定部分失败的处理策略。比如三个技能里有一个超时是忽略它的结果继续汇总还是整个请求判失败我倾向于忽略非关键项但要给用户标注物流信息暂时获取失败避免输出不完整结果。4.3 循环与递归给动态流程一个边界有些任务天然需要循环。比如把这 20 个商品逐一查库存或者把这篇文章逐段翻译成英文。面对这种任务模型最容易犯的毛病是把 20 个商品拆成 20 次独立函数调用既慢又费 token还可能在中途断掉。我的做法是把批量处理设计在技能内部而不是靠模型反复调用。比如设计一个batch_query_inventory技能入参直接接收一个 SKU 列表内部去循环查询、统一返回结果。模型只需要调用一次效率和稳定性都高得多。换句话说循环逻辑应该下沉到技能实现里而不是上升为 Agent 的编排动作。Agent 编排层的循环只保留在确实无法预知迭代次数、必须依据中间结果反复调整的场景比如让模型根据用户反馈反复改写文案的场景。这种情况下一定要设最大迭代次数我一般限制在 3~5 轮超过就直接转人工提示。4.4 一个反直觉的结论显式编排优于让模型自由发挥这几年 Agent 领域特别流行让模型自己规划的说法仿佛编排层越灵活越高级。但我做了几个实际项目之后得出的结论正好相反高频的核心流程一定要显式编排低频的开放性任务才适合让模型自由规划。原因很简单。显式编排意味着每一步都是确定性的输出质量稳定成本可以预估出了问题能定位。模型自由规划则意味着每次执行路径都可能不同有时它会走出一个惊艳的路径更多时候它会在某个分支里兜圈子。所以我现在采用的是混合模式把订单处理库存查询售后索赔这类高频流程写成固定编排只有遇到这些流程之外的全新问题时才把控制权交给模型的自由规划。实际跑下来混合模式的成本比全自由规划低了近 40%而成功率高出 20% 以上。5. 与主流 Agent 框架的对接模式及选型思路技能设计好之后接下来要落到框架里。市面上主流的方式大概有三种直接用平台的原生工具调用接口、使用 Agent 框架的抽象层、以及技能包模式。我分别说说它们的适用场景和我的选型感受。5.1 三种主流形态对比第一类是原生 Tool Use。OpenAI 和 Claude 都提供了tools/functions参数你只需要把工具定义包括名字、描述、参数 JSON Schema传给模型模型会在需要时返回一个结构化的工具调用请求。这种方式最直接、最底层几乎没有额外抽象。优点是很透明你传什么、模型返回什么中间没有任何魔法。缺点是技能一多你需要自己解决如何筛选工具子集的问题所有编排逻辑都要自己写。第二类是Agent 框架的抽象层。比如 LangChain 的Tool、StructuredTool以及 LlamaIndex 的FunctionTool。框架帮你统一了工具的声明、执行、错误处理并且内置了和各类模型接口的适配。用起来确实方便我做原型验证时也经常用。缺点是抽象层偶尔会好心办坏事。我遇到过框架升级之后工具 schema 的自动转换规则变了模型的行为在不经意间就发生了漂移排查起来要翻框架源码。第三类是技能包模式。这是 agent-skills 这类思路的核心形态把技能打包成可独立发布、可导入、带版本的单元。就像 VS Code 插件一样一个技能包包含了元信息、描述、执行逻辑、依赖声明和版本号。团队可以同时维护多个技能包项目之间按需引用。这种模式最适合多人团队、多项目复用、技能数量持续增长的场景。它解决的问题不是怎么调用技能而是技能怎么组织、怎么演进、怎么跨项目共享。5.2 我的选型建议我整理了一个简单的决策表按项目所处阶段来选项目阶段技能数量推荐做法原型验证、Demo少于 5 个直接用原生 Tool Use别上框架单应用落地、模型相对固定5~20 个轻量自封装 维护一份技能注册表技能多、多项目复用、多人协作超过 20 个技能包模式 独立 CI/CD配套注册表服务一个朴素的道理是不要为了框架而框架。你先把技能的边界画清楚再决定用什么形态落地。如果你的技能只有三五个用原生工具调用和轻量封装就能做得很好技能量大到让模型看不过来的时候再去考虑技能包和注册表服务。5.3 需要避开的生态陷阱第一个陷阱是过度封装导致的调试困难。有些框架把工具的调用链包装得很深模型 → 框架内部 → 工具执行器 → 回调 → 返回值。一旦中间某一环出了问题错误信息层层包装之后你根本看不出来是模型传参的问题还是框架转换的问题。所以我现在的原则是核心链路上尽量少用魔法每一层都保留原始日志。第二个陷阱是框架升级带来的行为漂移。Agent 领域的框架迭代速度快得惊人一个版本的小改动比如工具描述拼接顺序变了、schema 转换规则变了都会影响模型的触发行为。我见过有人因为框架自动升级工具触发准确率一夜之间掉了 15 个百分点。解决方案很简单锁版本、锁依赖必要时把关键链路的工具 schema 序列化后存一份基线快照。第三个陷阱是生态绑定过深。如果你把技能实现直接写在某个框架的基类里将来换框架的成本会非常高。我建议技能核心逻辑写成纯函数或独立类框架对接层只做薄薄的适配。这样框架可以换技能资产不丢失。6. 落地过程常踩的坑与对应的排查思路最后分享几个我在真实项目里踩过的坑每个都是真金白银换来的经验。这些坑的共同特点是不会让你立刻挂掉但会慢慢地消耗你的效果和耐心。6.1 坑一技能描述过于抽象模型完全不知道该不该用一开始我给一个查询订单的技能写描述时写的是本工具用于查询订单信息。结果用户问我上次买的耳机发货了没模型死活不触发这个技能反而去调了一个查询物流的技能因为描述里写了物流两个字。排查之后我意识到模型是字面匹配的高手但不是意图推断的高手。它看描述时是在寻找与用户问题的字面重合度。你写查询订单信息用户说的却是发货、到货、还没有收到模型就很难建立关联。解决方法是把描述改成了这样当用户询问订单状态、下单时间、订单金额、是否发货、物流单号等与订单相关的信息时使用。用户说我的订单我买的东西我的快递状态时都可能是本技能的使用场景。改完之后触发准确率从 61% 提到了 89%。这个对比足以说明描述的重要性。6.2 坑二参数 schema 缺少枚举与格式约束有一个技能需要接收一个status参数我当时业务代码里合法的值是PENDING、APPROVED、REJECTED。schema 里我只写了type: string没写枚举。结果模型传进来的是awaiting_confirmation——这个词听起来更像人话但后端完全不认识接口直接报错。让模型迁就你、还是你迁就模型答案一定是让 schema 更严格。从那以后我的原则变了所有字段只要取值集合有限就一定写enum编号类字段一定写pattern正则时间字段明确格式YYYY-MM-DD。这样修改之后参数格式类错误下降得非常显著。这里面有个小技巧把字段的description写得像在教一个刚入职的实习生。比如sku_id 是商品详情页 URL 末尾的编号形如 SKU-100234不要传商品名称。模型的遵循度远高于只写一个商品编号。6.3 坑三职责重叠的技能导致随机路由系统里有两个技能一个叫get_weather_by_city一个叫get_weather_by_coordinates。我原以为模型会根据用户输入是城市名还是经纬度来选结果发现它经常随机选选错了自然拿不到结果。后来我想明白了模型对技能选择这件事的理解是粗粒度的它看到两个技能都写着查询天气就很难精细判断内部差异。尤其当用户表达方式不那么标准时比如上海现在多少度它更倾向于随便挑一个。我的解决方案是把两个技能合并成一个query_weather入参给city和lat/lng两个可选字段内部做自动判断。模型只需要记住查天气就调这一个。合并之后这一块的出错率几乎归零。这里也引出一个原则不要让模型替你做技术细节的决策。同领域的技能能合并就合并把差异下沉到技能内部逻辑里模型的负担越小越好。6.4 坑四失败信息没有结构化模型修复无从下手早期我的技能执行失败时直接返回一段 Python 异常堆栈。模型收到这一堆乱七八糟的字符完全不知道该怎么处理只能一遍又一遍地重试同一个调用白白浪费 token 和用户耐心。后来我设计了统一的结构化错误返回格式就是前面提到过的error_code hint retryable三段式。这里我想多补充一句hint的写法本身也有讲究。比如sku_id字段格式不对不要只写参数错误而要写sku_id 应为 SKU- 开头的 5~20 位字母数字串你传入的是 XXXX——直接把正确的格式和当前的错误值都告诉模型。这样模型不需要猜测直接按照提示改就行了。6.5 坑五验证只靠手工调试没有自动化评测这是最隐蔽、也最伤人的一个坑。我有一段时间每次改完技能都靠手工找几条测试用例跑一跑觉得差不多就上线。结果往往是今天调好的行为明天换个用户问法又出问题这个技能调好了另一个技能的表现反而下降了。后来我花了一天时间把过去两个月的真实对话日志整理成了一个技能评测集大概 200 条每条标注了用户意图、对应技能、期望参数。每次修改技能后跑一遍全量评测重点看四个指标触发准确率该触发的技能有没有触发、不该触发的有没有误触发参数合法率模型传参能不能通过 schema 校验任务完成率端到端的流程有没有走通平均调用次数完成任务平均需要多少次技能调用数值越高说明系统越啰嗦。这个评测集最大的价值是让手感变成了数字。有一次我调整了一个共享技能的描述直觉上觉得更好了结果评测显示某个边缘场景的触发率跌了 20%。没有评测集这种回归根本发现不了。我在实际项目中体会最深的还是那句老话Agent 的能力上限由模型决定但能力下限由工程决定。技能系统能帮你兜住那个下限——把模型的随机性关进笼子里让每一次调用都稳定、可预期。如果你也在折腾 Agent建议先从三五个核心技能开始把边界和描述打磨好再考虑扩张。先把技能系统的地基打稳后面加再多技能都不会乱套。
返回列表