AI Agent技能体系设计:从工具调用到可靠任务执行
如果你正在做AI智能体Agent相关的东西大概率遇到过这种让人上火的情形模型在对话里表现得非常聪明你说什么它都接得住可真让它去调一次工具、查一份数据、执行一步实际操作它就开始模棱两可——参数传错、返回结果读不懂、有时候干脆假装自己已经做完了。我一度以为是模型能力不够后来把问题拆开来看发现真正的瓶颈根本不是推理能力而是我压根没有给它一套结构化的“技能”Skills。这里说的“agent-skills”我把它定义为把智能体需要执行的每一项原子能力——查天气、算价格、写文件、调接口、查数据库——封装成带有明确意图描述、参数契约、执行逻辑和返回协议的可注册单元。这套机制解决的是一个大问题大模型知道“该做什么”但不知道“怎么做才算对”。这篇文章面向正在做Agent落地、或者在看开源Agent框架里“工具调用”部分的开发者。我会从技能的结构设计、注册与调度、工程化管理讲到实际踩坑排查和编排进阶全程基于我真实跑过的项目经验不绕弯子。其中不少结论是从反复调试和回滚里得来的不是翻文档能翻到的。1. 会“说话”的智能体为什么突然“不会做事”了1.1 一次排查了两天半的“工具调用失效”先说一个我印象很深的项目。某次做一个内部知识库问答Agent前期对话部分一切正常模型能流畅回答“这个模块的配置项有哪些”。但只要涉及实际操作比如“把这份报表导出成Excel并发送给指定接收人”它就翻车要么选错了工具要么传进去的参数是乱编的要么工具确实执行了但它对结果视而不见继续按自己的想象往下说。项目组一开始怀疑是模型推理能力不够于是换了更大参数的模型问题依旧。后来我把模型调用前后的完整日志拉出来一行一行对才发现问题根本不是“推理”环节而是技能这一层完全没有设计。我当时做的所谓“技能”就是在系统提示词里塞了一大段工具列表每个工具写一句话描述再加个参数说明。模型在几十个工具的列表里选一个最匹配的本身就在做一道高难度的语义选择题选错太正常了。那次之后我把所有工具调用重构为“技能”模型每个能力不再是一行文字描述而是一个独立的注册单元有明确的名称、用途、参数Schema、执行方法和返回约定。重构完的Agent工具选择的准确率从大概六成提到了九成以上。1.2 提示词里塞工具列表的极限在哪里很多刚接触Agent开发的人会问为什么不能直接在系统提示词里把所有工具写清楚确实可以在小规模、单一用途的Demo里完全够用。但一旦能力数量超过十个问题就来了。第一是长度问题。一个工具如果认真写描述、参数、示例至少要200到300个Token。十个工具就是两三千Token直接挤压宝贵的上下文窗口留给对话历史和业务数据的空间就少了。第二是选择歧义。工具列表是平铺的模型需要自己在几十个选项中做语义匹配。两个功能相近的工具——比如“查询用户订单”和“查询用户历史订单”——描述稍微含糊一点模型就会随机选一个而且你很难在Prompt层面从根本上消除这种歧义。第三是无法复用和维护。工具列表写死在Prompt里换个项目就要全部重写。技能系统则可以把“查订单”这个能力封装成一个包哪个项目需要就注册哪个描述、参数、执行逻辑都在一个文件里改起来也集中。所以技能系统的本质不是“把工具描述换一种写法”而是把模型外部的行为能力从“提示词附属品”升级成独立的、可管理、可测试的软件模块。这是我做完重构后最深刻的体会。2. 技能四要素一份可以稳定交付的“能力说明书”一个技能要能稳定工作至少包含四个部分意图描述、参数契约、执行体、返回协议。缺一个都会在某个边缘场景里给你制造麻烦。2.1 意图描述给大模型一个不带歧义的触发区意图描述是技能给大模型的“自我介绍”也是路由选择时最重要的依据。很多开发者在这里只写一句话比如“查询天气”结果模型在用户问“明天适合出门吗”时根本不会触发它因为描述里没体现“适合出门”这类隐含需求。我常用的写法是场景加行为加边界。场景说明什么时候用行为说明做什么边界说明什么时候不能用。举个例子description: 当用户询问某地当前或未来几天的天气情况 包括温度、降水、风力、穿衣建议时使用。 适用于今天天气如何明天会下雨吗适合去户外吗等表达。 不适用于查询历史天气数据。这样写的好处是给模型划出了一个清晰的触发区域。前两句告诉它在什么需求下调用第三句告诉它什么情况下不要调用。模型做选择时本质是在做文本匹配描述里的关键词覆盖率越高选错的概率就越低。我还做过一次实验把某个技能的描述从“查询订单状态”扩写成包含“物流、配送、快递、什么时候到、发货没有”等近义表达的版本触发准确率提升非常明显。当然也不能为了覆盖而堆砌关键词那样会让不同的技能描述互相污染这个坑后面细说。2.2 参数契约Schema是给模型看的“操作手册”参数契约定义了调用技能时需要哪些输入、每个输入的类型和约束。它既是给大模型看的也是给执行器做校验用的。我在项目里全部采用JSON Schema风格定义因为主流Agent框架都支持而且大模型对JSON格式的理解稳定。一个典型的参数定义长这样parameters { type: object, properties: { order_id: { type: string, description: 订单号格式如2024-OT-XXXX, pattern: ^2024-OT-\\d{4}$ }, include_items: { type: boolean, description: 是否返回订单内商品明细, default: False } }, required: [order_id] }这里有两个容易忽略的点。第一description不是写给人看的是写给模型看的。它需要解释这个参数的语义以及从哪里可以获取“订单号格式如2024-OT-XXXX”比“订单ID”好用得多因为模型会结合上下文去匹配“用户粘贴的那串号码”。第二尽量声明严格约束比如枚举、正则、格式让执行器能在运行时拦截非法输入而不是把脏数据一路传递下去。如果模型抽取参数时发现缺少必填项最常见的做法是让技能调度器返回一个“缺参追问”信号由Agent向用户补充提问。千万不要让技能用一个空字符串继续执行否则你会得到一堆看似成功但毫无意义的结果。2.3 执行体与返回协议成功不一定是“成功”执行体是技能真正干活的代码可以是函数调用、HTTP请求、数据库查询或任何外部操作。但比执行逻辑本身更重要的是返回协议。我在早期项目里吃过一次大亏技能执行函数返回了一个字符串Agent拿到后直接当成“最终答案”展示给用户。实际上那段时间执行经常失败但因为函数没有显式报错错误信息被吞掉返回的是一串空数据。Agent完全没察觉继续用空数据组织回答最后用户看到的是“查询成功没有订单记录”而真实的订单明明就在那里。从那以后所有技能统一返回结构化结果至少包含三个字段字段类型说明statusstring执行状态success / failed / partialdataany成功时的业务数据errorobject失败时的错误码和描述status为successAgent才可以直接使用data组织回答status为failedAgent应该把error信息转化为对用户的解释partial则用于“部分成功”的场景比如批量处理时前两条成功、第三条失败。这套协议虽然简单但它给了调度器和模型一个明确的判断依据让“执行结果”真正成为Agent决策链路的一部分而不是被忽略的旁路信息。3. 技能注册与调度一次调用在运行时经历了什么单个技能设计得再好也要被正确地触发、传参、执行、回填整个链路才算通。这一节讲调度链路里四个关键环节。3.1 技能清单的构建时机静态扫描和动态加载技能注册的第一步是把可用的技能整理成清单交给模型和调度器。静态扫描适合技能数量确定、变更不频繁的项目启动时遍历技能目录读取每个技能的元信息生成统一的清单。动态加载则适合技能数量大、需要按用户或场景差异化暴露能力的场景比如不同租户能看到不同技能集合。我建议即使是小型项目也在一开始就做一层注册中心抽象不要在每个调用点手写技能列表。后续加一个技能、下线一个技能都只需要改注册配置不需要动Agent主流程代码。这个投入很小但会让后面的维护轻松很多。还有一个细节技能清单最终要被序列化后提供给大模型所以技能名称和描述里不要放多余信息。名称要简短唯一描述要精炼能不用例子的地方就不用清单越长模型在长文本里定位准确描述的成本越高。3.2 意图匹配语义阈值、规则优先和模型选择的配合调度器拿到用户当前的需求文本后需要决定调用哪个技能。目前主流做法有三种嵌入向量相似度匹配、规则关键词匹配、让大模型直接选择。我的经验是不要迷信任何一种。纯向量匹配对近义表达表现好但经常在边界场景里给出低置信度的“像但不确定”结果纯规则匹配精确但覆盖率低用户换一种说法就失配让大模型选择最灵活但成本高、延迟大而且如果技能描述写得不清晰大模型同样会选错。合理的做法是分优先级。先在规则层处理那些意图极其明确的表达比如用户输入“查天气”就直接触发天气技能规则没命中时再走向量匹配并设置一个阈值比如相似度低于0.7就不触发如果向量层也不确定才把候选技能列表交给大模型做最终选择。这个三级漏斗可以大幅降低误触发率和重复计算成本。我实际跑下来的一个体会是向量匹配的阈值不是一个调试一次就固定的参数。它跟技能数量、描述写法、业务领域都有关系最好做成配置项上线后观察一段时间再调整。曾有一个项目因为阈值设得太低用户随口说一句“今天有点冷”就触发了“获取供暖建议”技能看起来荒唐根因就是向量匹配把“冷”和“供暖”的语义距离算得太近了。3.3 参数填充大模型抽参的可靠性与兜底机制技能选定之后下一步是填充参数。多数框架让大模型根据对话上下文生成一个JSON对象作为参数。听起来简单但抽参环节的失败率在真实场景里并不低。最常见的问题是模型把用户话里的无关内容也塞进来或者把两个参数合并在一个字段里。为了提升抽参可靠性我会刻意做三件事。第一在参数Schema的description里写明“从哪里获取该参数”告知模型去对话历史、用户资料或系统上下文中查找。第二给每个参数提供取值示例让模型对期望格式有直观感知。第三在调度器里加一层运行时校验用Schema声明里的类型、枚举、模式去严格检查生成结果不合法就让模型重新生成一次第二次仍失败则向用户追问而不是默认通过或默认放弃。兜底还有一个容易漏掉的维度模型可能填充了“合理的默认值”但这个值根本不是用户想要的。比如用户说“把最近一个月的订单统计一下”模型给时间区间默认填了“本月至今”结果统计口径完全对不上。这类问题靠校验解决不了只能在参数描述里明确“该参数必须从用户表述中推断不得使用默认值”同时让系统在返回数据时标注实际使用的参数值方便Agent向用户确认。3.4 执行与结果回填让模型“看清”执行结果再决定下一步执行完成后调度器需要把结构化结果回填给模型让它在后续回复里引用真实数据。这一步最容易犯的错误是回填时只给模型一段拼接好的字符串丢掉了原始结构。比如技能返回了十行订单数据回填时拼成“订单1xxx订单2xxx”模型能引用但容易遗漏或记混。我一般会保留结构化的数据内容同时附上一段精简摘要让模型先用摘要理解大意再按需查阅明细。这有点像给模型准备了一份“索引加正文”的资料既控制了Token占用又保留了细节。结果回填的格式最好在所有技能间保持一致不要A技能回填JSON、B技能回填纯文本不然模型在链式调用里很容易晕头转向。4. 把技能库当成一个软件工程来管理技能一旦多起来就不再是零散的函数而是一个需要认真管理的软件制品。这里有几个我踩过坑后才想清楚的工程化原则。4.1 目录与命名按业务领域聚合按能力原子化拆分技能目录的组织方式直接影响可维护性。我建议大方向上按业务领域分目录比如“订单域”“物流域”“用户域”每个目录下再放该领域相关的技能文件。但在能力划分上要尽量原子化一个技能只做一件事不要做一个“超级技能”。举个例子“处理订单”就是一个典型的反例。它太含糊了创建订单、查询订单、取消订单是三个完全不同的行为合在一个技能里参数Schema会变得异常复杂模型很难正确选择具体操作。拆成三个技能后每个技能的意图清晰、参数简单路由准确率和维护成本都会显著改善。不过原子化也不是越细越好。如果把“发送邮件”和“记录发送日志”拆成两个独立技能并且执行时必须先后调用反而平添复杂度。我的判断标准是看这个动作是否经常被并列提及。用户说“发邮件”时隐含的日志记录根本不需要模型感知但用户说“取消订单”时“订单号”是明确的独立参数拆开才有价值。4.2 版本管理技能变更不能悄悄破坏调用方技能和普通业务代码一样会演进参数变了、描述改了、内部接口换了。但技能有一个特殊性它的“调用方”一半是代码一半是大模型。代码层面的调用错误可以通过编译发现模型层面的误触发却往往是静默发生的。我在项目里给每个技能加了语义化版本号并在注册配置里记录兼容性约束。比如一个大版本变更意味着参数结构不兼容必须同步修改所有依赖方一个小版本变更只是描述文案优化对调用方透明。变更上线前我会用一个“回归技能集”跑一遍里面预先准备了几十条覆盖各种表达方式的测试问题确认触发准确率和执行成功率没有明显回退再发布。这套流程看起来笨重但省下来的Debug时间非常可观。因为没有版本管理和回归测试时你根本不知道一次看似无关紧要的描述改动会让模型在多少个边角场景里开始选错技能。4.3 复用与共享跨项目技能包的边界同一个技能经常要在多个Agent项目里复用比如“查询订单”可能在客服Agent和运营分析Agent里都要用。我建议把这类通用技能做成独立包通过版本依赖引入而不是复制代码。但复用要有一个边界通用技能和业务定制技能要分开放。通用技能保持纯粹只做“查数据”“算结果”这类无状态操作定制技能则可以在外层叠加业务规则比如“查询订单后自动判断是否超时”就属于客服场景的定制逻辑不应该污染通用查询技能。还有一个容易被忽略的问题是跨项目共享技能时的数据源依赖。某项目里的“查订单”连的是测试库另一个项目连的是生产库技能包被错误地从一个环境引入另一个环境结果就是线上数据被测试逻辑污染。我在实践中把数据源配置放在技能包之外由运行环境注入这样同一个技能包在不同项目里连接的数据源可以不同避免了这个隐患。5. 真实踩坑三个高频问题的完整排查链路这一节分享三个我真实遇到过、也非常普遍的问题。每个问题我都会还原排查链路因为多数疑难杂症的解决靠的是系统性的排除过程而不是灵光一闪。5.1 车轱辘话技能描述互相污染导致路由错乱现象模型频繁在“查订单”和“查物流”之间选错用户问“我的快递什么时候到”模型却调用了“查订单”技能。排查过程我先看调度日志里模型每次选择技能时的置信度和上下文发现两个技能的向量表示高度接近。接着我逐一检查技能描述发现问题出在“查订单”的描述里我写了“包含物流信息、发货状态、预计送达时间”等词句而“查物流”的描述里也写了“查询订单对应的物流进度”。两边关键词大量重叠模型分不清边界。修复方式重新划定两个技能的职责范围。“查订单”回归到订单金额、商品、下单时间等交易信息物流信息相关表述全部移交给“查物流”。同时为“查物流”补充典型触发表达比如“到哪了”“配送进度”“快递单号”。改完后跑回归集选择准确率恢复到正常水平。这个坑给的经验是技能描述互相污染比“描述不够详细”更隐蔽。在写新技能前最好对现有技能描述做一次重叠检测把语义距离过近的技能要么合并要么明确边界。5.2 上下文预算被技能参数吃光现象某一次运行中用户让Agent汇总一批文档内容Agent在处理到第三份文档时上下文窗口直接溢出了程序报错。排查过程我检查了Token消耗明细发现不是对话历史膨胀而是技能参数里被塞进了完整文档。原因在于那个“汇总文档”技能的参数Schema里有一个“content”字段描述写的是“要汇总的内容”模型于是把整篇文档原文塞了进去。一份文档几千Token三份就爆炸了。修复方式把“content”参数改成“doc_id”让模型只传文档标识由技能执行体服务端读取文档内容而不是让文档内容进入上下文。即便技能需要处理用户粘贴的临时文本也应该在参数Schema里明确长度上限并在校验层拒绝超长输入。这个问题的本质是“参数传递与上下文承载”的混淆。技能设计时要时刻记住凡是能从后端数据源获取到的内容都应该是引用式传递而不是把内容本身塞给模型。5.3 返回成功但业务失败状态失真的代价现象用户反映“系统提示导出成功但邮件一直没收到”。查看日志发现技能确实返回了成功但后端任务实际在生成文件步骤就抛了异常异常被内部逻辑吞掉返回了一个空的成功状态。排查过程这是典型的返回协议缺失。当时技能执行的函数返回值被包装成一层成功外壳无论内部有没有异常调用方都收到statussuccess。调度器和模型都信了这个状态自然不会有任何纠错动作。修复方式我给技能执行体加了一层统一异常捕获任何未预期的异常都会被转换为带有堆栈摘要的结构化错误返回statusfailed。同时给“导出文件”这类耗时操作增加了结果确认机制执行完成后验证文件是否真实生成、邮件是否真实进入发送队列并把这个确认结果体现在返回协议里。状态失真比功能失败更危险因为它会让整个链路继续往错误方向走。统一返回协议需要在每一个技能里强制执行不能只靠开发者自觉。5.4 一次链式调用超时的教训现象客服Agent在处理“查订单并计算退款金额”时偶尔会出现整体超时最长一次等了四十多秒。排查过程链路显示“查订单”调用正常返回“计算退款”技能也正常但“计算退款”的执行体内部又自动调用了“查订单”的HTTP接口等于同一个接口被重复请求了一次。再加上Agent在等待期间还会发起新的模型请求整个时间被拉长到不可接受。修复方式做了两层处理。第一层是技能执行体内部的依赖检查明确“计算退款”应该直接接收订单数据作为输入而不是自己再去查一次库第二层是调度层面增加调用深度限制和超时熔断任何技能一旦执行时间超过阈值就快速失败并告知模型避免无限等待。链式编排里经常出现这种“隐形的重复调用”排查手段是给每次技能调用生成唯一的追踪ID在日志里把整条调用链串起来看哪里重复、哪里耗时一目了然。6. 技能编排从单点调用走向可复用的任务流单个技能能正确处理一次调用但真实业务往往需要多个技能按顺序、按条件组合。技能编排是Agent能力从“工具调用”升维到“任务执行”的关键一步。6.1 条件分支让智能体根据执行结果改变策略简单地把多个技能排成固定顺序并不是编排。我在项目里更常用的模式是条件分支Agent执行完一个技能后根据返回的status和数据内容决定下一步是继续执行、切换技能还是直接结束。还是用“导出报表”举例。第一步“查询数据”返回partial状态说明部分数据查询失败。此时Agent不应该继续走“生成文件”技能而是应该向用户说明情况并询问是否继续导出已完成的部分。这个分支逻辑可以写在编排代码里也可以交给模型根据返回协议自行决策。我倾向于把关键分支用确定性代码控制把非关键分支留给模型发挥这样既能保证主线稳定又不限制模型的灵活度。6.2 结果校验技能返回内容不等于可靠内容技能返回了statussuccess数据也结构完整但仍有可能在业务层面不可用。比如“查询库存”返回了一个数值但这个数值已经过期了二十分钟再比如“调用推荐接口”返回了结果但没有附带推荐模型版本无法确认是不是最新策略。我建议在编排层加一道“结果校验”环节主要做两类检查。一类是业务规则校验比如库存是否大于零、金额是否为正另一类是时效性校验比如返回数据的时间戳是否在允许范围内。校验不通过时编排器可以自动触发“重新获取”或“降级方案”而不是直接使用脏数据。有一条原则值得反复强调技能内部保证执行不报错编排层保证数据对业务有意义。这两件事分开管系统才不至于在业务层面失真。6.3 多技能协作与重复劳动消解多个技能协作时容易出现两个问题重复获取数据和循环依赖。重复获取数据很常见比如“生成订单摘要”和“计算订单金额”都会去查订单信息如果不共享上下文里的订单数据等于同一个请求查了两次。我的做法是在编排上下文中维护一个“数据暂存区”前一个技能返回的订单信息被暂存后一个技能执行时优先从暂存区读取只有缺失时才发起新请求。循环依赖则更隐蔽。技能A的需求触发条件是“数据已处理”技能B又需要技能A的结果来启动而技能A又在等待技能B的结果形成互相等待。调度器需要内置依赖解析和循环检测最简单的方式是给每次编排任务维护一个已调用技能的有向图一旦发现回边就立刻中断并报错。解决完这些问题再回头看技能编排其实就变成了一个传统的工作流引擎问题只不过节点从“固定函数”变成了“由模型驱动的动态技能调用”。把这层想清楚了Agent系统的稳定性会上一个大台阶。最后说一点我个人的实操体会。做Agent能力建设最容易犯的错误就是过早追求“模型聪明”而忽视了技能体系的工程化。同样是让模型完成一个复杂任务技能定义混乱的系统会把大量精力消耗在纠错和兜底上技能结构清晰、边界明确、返回协议统一的系统则能把模型的能力真正释放出来。如果你正在做类似的项目我建议从最小的技能集开始先把两三个技能的注册、调度、返回协议跑通再逐步扩展。技能不是越多越好而是每个都可靠才算数。