ARTICLE DETAIL

资讯详情

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

Agent技能化实战:把工具调用从提示词中解放出来

Agent技能化实战:把工具调用从提示词中解放出来 1. 为什么我把Agent的“技能”从提示词里拆了出来先讲个真实场景。半年前我在做一个内部知识库问答Agent刚开始的做法很天真把所有工具说明、数据库字段含义、甚至回复话术全部塞进系统提示词。结果就是Prompt膨胀到八千多字模型经常把“查库存”和“查订单状态”搞混有时候明明该调A工具的却跑去调B工具改一次提示词要全量回归线上出一次岔子就得排查半天到底是哪段冲突了。后来我把思路换成agent-skills这套模式也就是把Agent能干的每件事拆成一个个独立的“技能包”每个技能包自带描述、入参约束、使用条件和返回约定。效果非常明显——因为底层模型拿到的不再是一锅炖的说明书而是一份按需调用的目录该用哪个才把哪个的具体用法展开。这个思路做对了之后工具调用的准确率肉眼可见地上涨改技能也不影响其他模块。这篇内容适合正在做LLM Agent、工作流编排、或者想把手头工具接入智能助手的开发者。无论你是用原生代码在写还是打算接各种Agent框架agent-skills的这套数据结构设计和调用机制都是通用的而且不限定具体模型供应商。我把踩过的坑、调优方法和评测办法都整理出来了中间涉及的代码片段都是可以直接拿走的。1.1 单体巨型Prompt的失控现场先复盘一下它为什么会失控。模型处理提示词是有注意力边界的内容越长、信息密度越高越容易在关键决策点“选择困难”。比如这样一份工具说明- 工具A根据用户提供的订单ID查询订单详情返回订单金额、状态、商品列表 - 工具B根据用户提供的商品ID查询商品库存余量 - 工具C根据用户提供的商品ID修改商品价格仅限管理员 - 工具D获取当前登录用户的信息包括用户名、角色、权限这看起来不算复杂但一旦工具涨到二十多个模型每次做Function Calling都要在“什么时候该用哪个”上凭空猜。更麻烦的是不同工具的字段经常长得像例如“用户ID”和“订单ID”同时出现模型就看岔了。更隐蔽的问题在于安全把敏感操作和普通查询平铺在一起写等于默认模型有能力分辨上下文边界但实际上它经常在用户话术里看到“价格”两个字就冲动调用改价工具。agent-skills化之后同样的能力会被拆成至少四个独立JSON文件每一项只有一两百字的核心描述命中之后才加载完整schema。模型的决策负担被大幅降低因为检索到哪个技能就只把哪个技能的参数放出来不确定性集中在一个可控的小范围里。1.2 技能化之后到底改了什么最重要的变化是表达层次变了。原来的工具列表是“平坦”的把所有能力一视同仁地堆在一起模型靠观察相似度硬猜技能化之后的能力是“分层”的先有一层轻量级索引再有一层详细定义模型走的是“查目录再翻页”的路径。在调用侧我的做法是把技能调用拆成三步。第一步根据用户当前会话判断需要哪些领域的技能第二步根据候选技能描述做精确匹配第三步再把匹配到的技能完整定义塞给模型。这么做的直接收益有三个上下文窗口从拥挤变得宽松模型不再被无关工具描述干扰。新技能上线不需要重写整个Prompt把新技能包丢进去就能生效。权限边界更清晰敏感技能不在常规检索范围内暴露必须显式命中权限条件才出现在模型视野里。这套设计跑通之后我把它完善成了一个小型调度内核对外暴露的接口很简单注册技能、查询技能、调用技能、回收技能。下面我从数据结构讲起。2. 技能的数据结构一个经得起推敲的JSON定义技能包长什么样直接决定了后续的注册、检索、调用顺不顺畅。我见过很多人做类似事情时只写一个name和一个description就收工了实际一跑就会发现召回率堪忧参数校验也是稀烂的。我这边经过几轮重构稳定下来的技能定义大概长这样{ skill_id: order_query, name: 订单详情查询, version: 1.2.0, domain: [订单, 交易, 售后], trigger_intents: [查订单, 订单详情, 快递到哪了, 买的东西发没发], description: 根据订单号查询订单的基础信息、商品清单、物流状态与金额明细。适用于用户查询自己或管理员查询任何一笔订单的场景。, input_schema: { type: object, required: [order_id], properties: { order_id: { type: string, description: 订单编号通常以大写字母O开头后接14位数字 }, include_items: { type: boolean, description: 是否返回商品行明细默认false } } }, output_schema: { type: object, properties: { order_status: { type: string, enum: [待付款, 已付款, 已发货, 已完成, 已取消] }, total_amount: { type: number }, items: { type: array } } }, security: { required_role: user_or_admin, allow_when_logged_in: true }, cost_hint: low }2.1 描述字段是召回率的第一决定因素很多人的description写得太像功能说明书比如“该技能用于查询订单详情”这基本等于没写。模型理解用户口语意图的时候靠的是意图之间的语义距离如果你的描述里只有“订单”两个字用户问“我昨天买的东西发货了没”时就很难命中这张描述。我在description里刻意加入了“快递”“买的东西”“发没发”这类偏口语的表达用下来召回率提升非常明显。另外我强烈建议把trigger_intents这个字段常态化使用。它本质上是给技能做“触发词”标注让检索系统多一条精确匹配路径。尤其在模型输出的function_name不稳定的时候有一层关键词兜底整个系统会稳很多。2.2 input_schema里藏着参数纠错的关键input_schema不仅是让大模型知道该传什么参数它更大的作用是触发模型在调用前做一轮“自查”。比如我在order_id的描述里写了规则“通常以大写字母O开头后接14位数字”模型在抽取参数时会照这个规则去核验用户给的消息缺了前缀会自动补全格式升级后模型能主动发起追问。这里有几个容易被忽略的细节每个字段的描述必须写清格式、取值范围、是否必填别指望模型自己猜。required数组要和properties里的定义严格对应有过一次required写了大写“OrderID”而properties里是小写“orderId”导致模型反复校验失败的情况。字段类型能收敛就收敛能enum就enum。比如订单状态写死一个枚举模型就不会自己编出“运输中”这种值。2.3 security字段是我后期补上去的早期版本没有安全字段结果在一次测试里模型靠着低权限会话调用了管理员的改价技能。后来我加了security对象并在检索层做了一个强约束检索阶段先判断当前用户角色和登录态如果不满足条件直接不把技能返回给模型从源头切断调用可能性。这比“调用前再校验”更可靠因为大模型一旦拿到了技能完整定义就可能想方设法拼出参数。说完单个技能再说说技能库整体怎么组织。3. 技能注册与召回别让Agent在一个大名单里大海捞针技能库刚建立的时候只有十几个技能我天真地认为把所有技能描述一次性塞给模型让它自己选是没问题的。等技能数量越过三十个之后问题就冒出来了一是上下文被撑满二是模型开始出现选择漂移——明明是个查询库存的问题它给我调了商品上架的技能。这时候我才认真做了两件事注册时的技能索引和召回时的两级过滤。3.1 注册时对技能做领域聚类注册不只是把技能塞进一个列表里。我在每个技能包上都加了domain字段注册中心会按照domain做一层索引。比如“order_query”的domain是订单、交易、售后“inventory_check”的domain是库存、仓储、供应链。后续召回的时候先根据用户的会话意图算出一个目标domain列表只在这些domain内部搜索搜索量直接砍掉六成以上。这个做法的前提是domain的命名必须维护好别一个技能挂五个风格迥异的domain。我的约定是最多三个并且按“业务域-子域”的层级写比如“订单-查询”“订单-售后”两个维度分别参与索引。3.2 召回时的两级过滤策略第一级是粗筛用领域索引加关键词命中把候选技能缩到五到十个以内。第二级是精排把候选技能的description和用户当前问题做语义相似度打分再加上一个“是否满足当前权限”的硬性过滤最后只把得分最高的前三个技能的定义拿给模型。我把这个策略叫“召回一个面精排一个点”因为最终给模型的选择空间宁可小也不要让它在一堆相似技能里犯难。这里有一个工程细节值得留意精排阶段不要只按相似度排序完就交差还要考虑技能之间的“互斥关系”。比如“发货”和“取消发货”两个技能在语义上很近但是同时暴露给模型模型很可能在用户说“搞错了帮我改一下”的时候选错。我的做法是在技能定义里加了一个conflicts_with字段如果两个技能同时出现在候选列表里就按业务优先级裁掉一个。3.3 召回失败的兜底逻辑再准的召回也有漏的时候。我的兜底策略是设置了一个“模糊回退”技能当精排后的最高分低于某个阈值时不强行调用技能而是让模型用通用对话能力回复用户说明“暂时无法处理该请求”。这个设计挽救了大量因为用户口音、拼写错误、新业务词汇导致的误判。阈值要反复试太低了会把无关技能带进来太高了又啥都召回不到。我目前用的规则是动态阈值如果用户问题里有关键实体比如订单号、商品名阈值可以适当降一点因为实体命中本身也是强信号。4. 技能编排做任务流设计时的三个实际取舍Agent只调一个技能的场景其实很少。多数情况下用户是带着一个多步骤任务来的比如“帮我查一下昨天那笔订单如果发货了就算了没发货就帮我把收货地址改了”。这个流程里至少涉及订单查询和地址修改两个技能还带着一个条件分支。技能编排要解决的就是这类问题——把多个技能按正确的顺序组织成一条可执行的任务流。4.1 顺序编排要以数据依赖为核心顺序编排最简单就是一个技能的输出喂给下一个技能做输入。但有一个原则必须坚持不要按“感觉上的步骤顺序”排而要按照“数据的依赖关系”排。举例来说目标是“查订单并改地址”正确顺序是先查订单拿到order_id再查该订单关联的物流状态最后才能决定要不要调修改地址。一旦中间某个技能返回的数据不完整后续步骤就应该中止而不是带着空参数硬走。我实际落地时用的是一个轻量的DAG有向无环图结构节点是技能ID边是参数传递关系。每个节点执行完都做一次输出校验不满足输入要求的边直接视为路径不通走失败分支。4.2 条件分支不要交给模型自由发挥这里是我的教训。早先我图省事让模型在编排阶段自己决定“if download ok then next else rollback”实测效果非常不稳定。模型经常在条件判断上过度自信明明技能A返回了一个error码它还是继续执行技能B最后整个任务流的结果是脏的。后来我把条件分支改成了显式的rule表达式在执行引擎里用代码判断而不是让模型判断。比如“订单状态是已发货”这个条件就写成order_status 已发货由执行引擎在拿到输出后直接做布尔计算。模型只负责产出参数和执行技能条件判断全部交由确定性代码处理这一块出错率直接降到了接近零。4.3 技能编排时要预留补偿动作分布式系统里有个概念叫Saga核心思想是每个操作都要有对应的反向操作。技能编排也一样尤其是涉及修改类技能时必须定义好补偿技能。比如“修改订单地址”的补偿技能就是“把地址改回原值”“关闭工单”的补偿技能是“重新打开工单并追加备注”。我在技能定义里增加了rollback_skill字段当任务流中后续步骤失败时执行引擎自动回滚前面所有带补偿动作的技能。这个设计把一个多步骤任务的失败影响控制在最小半径内也有利于在日志里追踪到底哪一步发起了回滚。5. 实测中的调参经验把技能调用的成功率从62%拉到91%讲完理论说点数据。我把这套agent-skills跑进一个真实项目后做过一轮完整的评测评测集是四百多条真实用户会话覆盖十二个技能。第一轮跑完技能调用成功率只有62%我当时的反应是不动脑子先调阈值结果越调越差。后来老老实实拆了错误类型才发现大头分别是召回漏掉17%、参数抽取错误占11%、权限拦截误伤9%。5.1 召回漏掉的常见修正路径召回漏掉的第一原因是description里缺少用户口语里常见的表达。比如项目里有个技能是“优惠券批量发放”但我只写了“发放优惠券”用户问“帮我给这批人发个福利券”模型怎么都匹配不上。修正方案不是在description里无脑堆同义词而是去看真实会话日志把用户提问频率最高的top50句式拉出来逐个技能比照把高频表达补进trigger_intents里。第二个原因是domain分类覆盖不到位。有些技能天然跨域比如“订单查询”既属于“订单”又属于“售后”。我刚开始只用单一domain召回率一直差一口气改成数组后用起来明显改观。5.2 参数抽取错误的三个规律观察下来参数抽取错误主要集中在这几种情况时间表达不规范“上周五”模型不知道转成具体日期、金额单位混用“八百块”和“800元”不一致、多个同类实体并存两个商品名摆在一起模型选了后一个。这些问题的共性原因是input_schema里缺少对常见口语模态的归一化指导。我的做法是在字段描述里主动加入“不要做什么”。比如日期字段的描述写成“用户可能说上周五、前天、月底等自然语言请先转换为YYYY-MM-DD格式再填入”这类负面约束对模型的效果比正面描述更直接。另外凡是出现多个实体的情况我会要求模型先列出候选列表再让用户确认而不是自作主张挑一个。5.3 权限拦截误伤的排查权限拦截误伤是体验杀手。本来用户是管理员因为会话里没有正确传播身份信息技能侧判定成了游客结果所有敏感技能全部被挡住。排查过程中发现问题出在会话状态管理上——技能检索发生在认证信息更新之前相当于拿着旧身份去做了新请求的授权判断。解决方法是把授权校验的执行顺序彻底后置先完成身份确认再走技能召回与权限过滤并且技能执行时把当前身份ID带到调用上下文里。这样权限误伤基本消失了而且权限相关日志变得更好追查。5.4 线上监控与技能回收的机制最后聊一下上线后的运维。我维护了一份技能调用热力图每天看一遍哪些技能是高调用量、哪些技能已经连续一周没被触发过。对于连续30天零调用的技能我会有个评估流程看是业务不需要了还是召回路径出了问题如果是后者就补召回如果是前者就下线归档。技能下线比上线更谨慎因为有正在执行的任务流可能还依赖它。我的做法是下线前先标记为deprecated保留72小时宽限期期间已开始的任务流可以完成收尾新任务不允许再注册。等宽限期过了才真正从索引里移除。这套机制跑到现在技能库保持着比较健康的新陈代谢没有出现越积越臃肿的问题。回到开头那个场景如果你现在也在跟“提示词越写越长、Agent越来越不听使唤”较劲不妨试试把能力拆成独立技能包。第一次拆分可能会觉得繁琐把描述字段反复打磨也确实花时间但等你把召回、编排、权限、监控的链路都理顺之后你会获得一个真正可控的Agent。我这套东西还在持续迭代下一步准备在测试集上加入更多对抗样本专门打击那些“用户说反话”“多轮对话中改口”的刁难场景。
返回列表