ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent技能体系设计实战:让大模型稳定选对工具与参数

Agent技能体系设计实战:让大模型稳定选对工具与参数

开篇先交代一个背景。前阵子团队接了个活儿:给内部业务流程做一个多步骤的智能助手,要求它能听懂一句话需求,然后自动拆解成若干动作,调用不同系统把事办了。项目刚开始大家都觉得挺简单——大模型谁不会接,工具函数写一堆,提示词里列清楚不就行了。结果一跑真实场景就翻车了:模型要么在多个工具之间犹豫不决,要么把参数编得离谱,要么干脆用错技能,把一个查询类动作误触发成写操作。那段时间天天在日志里捞调用记录,一边看模型"演戏"一边反思。后来才慢慢意识到,问题不在模型笨,而在于我们根本没有一套像样的Agent技能体系。

所谓agent-skills,说的就是给智能体建立一套"能力清单 + 调用协议 + 运行管线"的系统化方案。它要解决的,不只是"模型会不会调函数",而是"模型能不能在几十个能力项里稳定选对、把参数传准、在失败后懂得补救"。这套东西设计好了,智能体才能从"玩具"变成"能扛活的员工"。这篇文章就把我踩过的坑和最终沉淀下来的设计思路完整拆一遍,适合正在做大模型应用、智能体流程编排、或者想把内部工具接入 AI 系统的开发者参考。

1. 技能系统到底在治什么病:三个逃不过的现实问题

很多人一开始把技能系统理解成"给模型加几个函数",其实这是本质性的偏差。函数调用只是协议层面的一小步,技能系统要治的是更现实的三类病:模型能力的"虚胖"、提示词空间的物理极限、以及业务逻辑与模型逻辑的纠缠。

1.1 模型什么都会背后的真实困境

大模型训练的语料太广,你问它"会写周报吗",它说会;"会查数据库吗",它也说会。但真让它去查库,它连连接串在哪都不知道。这就是典型的"知识幻觉——模型以为自己会,但实际上没有任何可执行的抓手"。技能系统的第一层价值,就是给模型一个"确定性抓手":每一项技能背后对应一段真实可运行的代码、一个真实的 API 请求、一条真实的业务规则。模型不需要"知道怎么做",它只需要"知道该选哪项技能",具体怎么做由技能实现者负责。

这里有个特别容易被忽略的点:技能本质上是把"模型的判断力"和"程序的执行力"做了分工。模型负责语义理解和意图匹配,程序负责确定性的执行逻辑。一旦你想让模型直接写代码去调外部接口,风险就失控了。所以我在设计时坚持一条铁律:技能描述里只能出现"做什么事",不能出现"怎么实现"。模型一旦开始自己推导实现细节,参数幻觉和错误调用就会成倍增加。

1.2 提示词塞不进第二十个工具说明

早期方案确实粗暴:把所有工具函数的说明拼进 system prompt,一个工具写两三百个字符,二十个工具就是六千字符,加上业务上下文直接逼近上下文窗口的舒适区。更要命的是,说明越长,模型的选择准确率越低。这是个被反复验证的现象——工具列表超过十几个之后,模型开始"选择困难",把相似工具的 description 混淆、把参数 schema 搞串。

技能系统的第二个价值,是在模型和真实工具函数之间加了一层索引和路由。系统先维护一份所有技能的结构化清单,模型只需要通过技能 ID、简要描述、参数协议来"点名",不需要看到完整实现。这就像一个大公司的前台:你不必认识每个部门的人,只需要告诉前台你要办什么事,前台帮你路由到对的人。技能清单就是那个前台,而且它的信息密度经过精心控制,保证模型在有限上下文里看得清、选得准。

1.3 技能与业务逻辑不能焊死在一起

第三类问题来自工程侧。很多团队的代码里,业务流程和 API 调用逻辑是同构的,比如"创建订单"就直接调创建订单的接口,中间没有任何抽象。这种写法在传统后端里没毛病,但放到智能体场景就坏了:模型的一次决策可能需要跨多个系统,而每个系统的鉴权方式、数据格式、错误语义都不一样。如果你把这些差异直接暴露给模型,它一定会蒙圈。

正确的做法是把业务能力封装成标准技能的形态,技能内部自己去处理鉴权、数据转换、错误码翻译。外部看起来就是统一的输入输出协议,内部爱怎么折腾都行。这套封装思路借鉴了后端服务里的防腐层概念,只不过这一次的"下游调用方"不是另一个服务,而是大模型。封装做得好,技能的复用性和可测试性都会显著提升,这也是后面搭建技能市场的前提。

2. 技能描述层:让模型一眼选中正确技能

技能描述层是整个体系的灵魂。模型看不见你的代码,它只能靠名称、描述、参数协议来决定要不要调、怎么调。这一层写得好不好,直接决定了调用的准确率。我见过太多团队把 80% 的时间花在执行逻辑上,描述随便写两句,最后模型选错技能就骂模型不行,其实是描述层没下功夫。

2.1 一份合格技能说明书的三件套

一份可用的技能说明,至少要包含三个部分:技能名称、一句话功能描述、边界声明。名称要短且唯一,最好符合业务直觉,比如"query_order_status"就比"order"清晰得多。功能描述要讲清楚"这个技能做什么、适用于什么场景",而且要主动声明它不做什么。

我举个真实对比,同样的技能,两种写法效果完全不同。

写法一(不合格):

  • 名称:get_info
  • 描述:获取相关信息。

写法二(合格):

  • 名称:get_order_delivery_info
  • 描述:根据订单编号查询物流配送状态,适用于用户询问"我的货到哪了""什么时候送到"等场景。
  • 边界:仅支持查询已发货的订单,不支持修改配送地址、不支持催发货。

写法二的命中率几乎可以做到百分之百,写法一纯属碰运气。为什么边界声明这么重要?因为相似场景太多了,模型判断"这个技能能不能解决当前问题"时需要大量排除法信息。你告诉它能做什么,只是给了正例;你告诉它不做什么,等于给了充足的负例,模型的选择边界一下就清晰了。

2.2 参数协议的细腻度决定调用成功率

参数协议是模型最容易翻车的地方,也是最值得花时间打磨的地方。如果你只给一个字段叫date,类型是 string,模型根本不知道这个 date 应该是什么格式。是"2024-05-01"还是"2024/05/01"?是本地时间还是 UTC?需不需要带时区?所有这些都是模型的猜测空间,而猜测往往就是错误的开始。

在设计参数协议时,我总结了一套可操作的规则:

  • 每个字段必须有明确的业务含义描述,不要只写类型。
  • 给format或pattern约束,比如日期强制YYYY-MM-DD。
  • 枚举字段列出所有可选值和对应含义。
  • 可以给default默认值,特别是可选参数,让模型在犹豫时有兜底。
  • 对容易混淆的参数,在描述里写明"不要和另一个参数同时使用"。

以下是一个实际用过的参数协议简版示例,类型标注用的是 JSON Schema 风格:

{ "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,一般是字母加数字的混合字符串", "pattern": "^[A-Z0-9]{8,20}$" }, "timezone": { "type": "string", "description": "查询时区,默认 Asia/Shanghai", "enum": ["Asia/Shanghai", "UTC"], "default": "Asia/Shanghai" } }, "required": ["order_id"], "additionalProperties": false }

注意最后一行additionalProperties: false。它的含义是:模型只能传协议里声明的字段,不能自己发明字段。这个约束看似死板,实际上救了很多次命。模型的自由发挥能力远比我们想象的大,没有这个开关,它可能往请求里塞各种莫名其妙的参数。

2.3 用离线样例集给技能描述打分

描述写得好不好,不能靠感觉,要建立一套离线评测机制。方法是准备一批典型的用户问题,比如"我的订单物流到哪了""这个包裹为什么三天没动静",标注好它们应该命中的技能 ID,然后批量让模型去选技能,统计命中率。

这套机制的效果非常直观。我第一次对自己的技能清单做评测时,命中率只有不到 70%。逐条看失败案例,发现大量问题出在描述歧义上。比如有个技能是"查询账户余额",另一个技能是"查询优惠券余额",模型经常把前者的参数套到后者上。后来我改了描述,明确规定"本技能只处理现金余额,不包含任何优惠券、积分、赠金",命中率立刻上到了 90% 以上。

评测集最好保持持续更新。每发生一次线上选错技能的 case,就把对应问题加进测试集,下次改描述或加边界声明后重新跑一遍,确保修复不是靠运气。这个习惯坚持两三个月,技能体系的整体稳定性会有质的飞跃。

3. 执行管线:从技能清单到安全可控的动作

描述层解决的是"选对技能",执行管线解决的是"安全地把事办了"。很多团队死磕 prompt 和描述,却忽略了运行时设计,结果模型选对了技能,执行层却因为权限、状态、异常处理乱七八糟而频频失败。执行管线是技能体系的骨架,它要保证每个动作可追踪、可中止、可恢复。

3.1 技能注册中心的那些元数据

所有技能在上线前,先要到注册中心登记。注册中心维护的不仅是技能 ID 和描述,还要有完整的状态机元数据。我常用的注册信息包括:

  • 技能名称、版本号。
  • 启停状态:启用、灰度、下线。
  • 权限等级:只读、写操作、高危操作。
  • 超时时间、重试策略、限流策略。
  • 负责人和变更历史。

权限登记这步至关重要,尤其当技能要接入多个系统时。传统后端面试里有个经典问题:如何控制越权访问?在智能体场景,这个问题被放大了——模型可能因为一次prompt语义理解偏差,就触发一个高危操作。所以我把技能分成三类:

类别示例调用策略
只读查询类查订单、查库存模型可直接调用
业务操作类创建工单、提交审批必须有用户确认步骤
高危操作类删除数据、转账、批量变更二次确认加权限校验,必要时临门一脚由人工完成

这个分类写在技能描述之外,模型感知不到,但执行管线强制校验。它能兜住模型最坏情况下的"自作主张"。

3.2 状态机与执行生命周期

每个技能调用实例,我都会用一个标准的生命周期状态机来管理,状态包括:pending(等待执行)、running(执行中)、succeeded(成功)、failed(失败)、canceled(取消)。别小看这个设计,它有三个直接好处。

一是可观测性。所有历史调用都能落库,方便回溯"模型哪一步做了什么决定"。没有状态机,日志就是一团乱麻,出了线上问题只能抓瞎。二是可中断。用户在模型还没执行完的时候说"算了别搞了",系统可以直接把 pending 或 running 状态置为 canceled,避免后续动作继续执行。三是可恢复。失败的任务可以快速定位到具体环节,决定是重试、跳过还是换一条链路走。

这里有个工程细节:状态变更必须走消息或事件机制,不要直接在业务代码里改状态字段。否则多个并发动作同时修改一个技能实例,状态就乱了。我实际用过的方案是:每个技能调用生成一个唯一的execution_id,所有状态流转都通过 Redis 锁再加数据库更新,保证并发场景下的一致性。

3.3 外部系统适配层与结果归一化

执行层对接外部系统时,最大的痛点是错误码和返回格式五花八门。有的接口失败返回{"code": 500, "msg": "error"},有的抛异常,有的返回 200 但 body 里藏着错误信息。如果这些原始差异直接传给模型,模型根本没法正确判断一次调用到底是成功还是失败。

所以我在所有技能实现里加了一层适配器,目标是让每个技能的返回结果都统一成固定结构:正常情况返回数据本体,异常情况返回错误码和可读的错误消息。结构如下:

{ "status": "success", "data": { "order_status": "shipped" } }
{ "status": "error", "error": { "code": "ORDER_NOT_FOUND", "message": "订单不存在,请检查订单编号是否正确" } }

统一的返回格式对模型非常友好。模型在下一步决策时,只需要看 status 字段就能判断要不要换一种方案,而不需要去解析外部接口千奇百怪的错误。另外,错误消息必须写成模型能理解、用户能看懂的人话。比如"数据库连接池满"这种技术细节,模型读了也不知道该怎么办,但如果写成"系统繁忙,请稍后重试",模型至少知道这次动作没成功,可以换个方式继续尝试。

4. 多技能编排:一次真实出差行程的推演

单技能调用简单,真正体现 agent 价值的是多技能协作。这里我用一个"安排跨城市出差行程"的例子,完整拆一遍技能编排的过程。这个案例是我们当初做跨系统流程助手时的原型场景,几乎覆盖了编排里会遇到的所有典型问题。

4.1 观察-思考-行动循环在技能系统里的落地

用户输入一句话:"帮我看下下周三去某城市出差的行程,早上九点半开会,开完会下午还要拜访供应商,顺便订下住宿。"

这句需求里藏着多个技能调用点。系统要先解析出关键实体:时间(下周三)、目的地(某城市)、动作(查询车票、查询酒店)。然后进入一个类似 ReAct 的循环:观察当前状态,思考下一步要调用什么技能,执行调用,拿到结果后继续观察,再思考下一步。

实际落地时,我使用了一个面向功能的内部 DSL 来约束循环行为,核心元素包括thought(模型输出下一步理由)、action(调用技能名)、action_input(技能参数)、observation(技能返回结果)。模型每次只会输出一个 action,执行层跑完技能后把 observation 拼回上下文,再让模型继续思考。这个模式说起来简单,真正做稳很难,难点在上下文的组织——每次循环产生的大量中间结果不能全部塞回提示词,否则上下文爆炸。我采用的做法是为每一步循环生成一份摘要型的观察记录,只保留关键信息,比如"火车票查询到 3 个车次可选,最早的是 7:30 出发、9:15 到达",而不是把所有 JSON 返回都原样贴回去。

4.2 三种最常见的编排策略

编排策略在技能系统里一般分三种:顺序执行、并行请求、条件分支。出差这个场景里三者都会用到。

顺序执行最典型:先查车票,确定抵达时间,再根据抵达时间查酒店,因为酒店入住时间依赖于到达时间。并行请求也不少见:同时查车票和查供应商地址,它们之间没有依赖关系时,可以一次并行调用多个只读技能,减少模型循环次数。条件分支体现在"如果早上的会议准时结束,就约下午两点的供应商拜访;如果拖堂,就顺延到三点"。

在实现上,我会用一个轻量的调度状态来标记技能之间的依赖图。依赖图不是让模型直接生成的,而是通过模型产出的意图序列,在运行时动态判断。比如模型第一次调用了"查询车次",返回结果里包含"到达时间 15:30",那么下一步模型大概率会调用"查询酒店"并传入入住日期与期望区域。这时调度器会检查参数完整性,如果缺少关键参数,就向用户提问而不是直接让模型瞎猜。

4.3 防跑偏:步数上限、循环检测与交代边界

编排最大的坑是模型"跑偏":在循环里反复调用同一个技能、编出无人能兑现的计划、或者绕来绕去不落定。我见过一个极端例子:模型为了订酒店,先调了一堆天气查询、周边景点查询、历史订单查询之类的无关技能,才勉强回到酒店预订。不是模型坏,而是技能清单里相似描述太多,它把每个动作都想试一遍。

防跑偏的设计有几个关键点。第一是步数上限,单次任务限制最多 12 步调用,超出即停止并转入人工兜底。这招看着粗暴,但特别管用。第二是循环检测,系统记录最近 N 次 action 的名称和参数签名,如果出现重复调用同一技能超过两次,且中间没有有效进展,就直接打断并返回当前状态。第三是技能分组隔离,把"查询类"和"操作类"分开,模型一次任务里如果没有明确的业务对象,不允许先调用操作类技能。

另外还有一个容易忽略的点:任务的终止判断。模型什么时候觉得"事儿办完了"?很多系统里,它办完一个动作就欢快地说完成了,根本不管用户还有后续需求。我给 agent 加了一条规则:只有当用户确认"不需要后续操作"或者所有意图项都完成了,才能返回最终结果。否则继续循环或主动询问用户。

5. 实测中反复踩的坑:权限、幻觉与失败恢复

再好的设计,跑真实业务都会踩坑。这一节专门讲我们在实际项目中反复遇到的三个坑:权限边界渗透、参数幻觉、失败后的恢复。每一条都是用真金白银的线上故障换来的经验。

5.1 技能选错与参数幻觉的根因

所谓参数幻觉,是指模型调用技能时编造了不存在的参数值。比如用户说"查一下最近一笔订单",模型把order_id编成了ORD-2024-00001,但这个编号根本不存在。根因有两层:一是模型确实不掌握真实数据,只能根据上下文推测;二是技能协议没有给模型足够的"不确定时该怎么办"的指引。

我后来在参数协议里专门加了可选的query_by字段,允许模型用recent、latest、order_id三种方式查询。当用户没有明确给出订单编号时,模型就可以选择query_by=recent,再由技能实现内部去查最近订单,而不是硬编一个假的 ID。这相当于给模型的"编造空间"一个合理的出口,幻觉概率大幅下降。

技能选错这个问题,更多出在描述层的相似度上。我踩过的具体 case:同时接入了"查询门店营业时间"和"查询门店联系电话",模型对一句话需求"我想找这个门店问问情况"反复横跳,一会儿调营业时间技能,一会儿调联系电话技能。后来我把两个技能的描述彻底重写,一个强调"当用户需要了解开店打烊时段时调用",另一个强调"当用户需要找人沟通或拨打电话时调用",还在边界区写明彼此的排除关系,问题立刻缓解。

5.2 一次预订失败案例的完整排查链路

讲一个真实排障过程,过程比结果重要。某个预订类技能在测试环境一切正常,一上生产就频繁失败,模型的下一步决策经常是"尝试再次预订同一时间",形成死循环。

排查链路是这样的:一开始我怀疑是外部接口不稳定,但单独用脚本调接口,成功率接近 100%。接着怀疑是参数校验问题,于是把一次失败的execution_id拉出来,翻完整调用链。关键线索出现了:技能执行层收到的时间参数是2024-07-03T09:00:00+08:00,而外部接口要求的格式是2024-07-03 09:00:00,适配器没做转换,直接按字符串传过去了。接口返回的参数格式错误被归一化成"预订失败",模型拿到失败结果后又重试同一个时间,再次失败,循环往复直到耗尽步数。

根因找到后修复很快:适配层加时间格式转换。但这个问题真正给我的教训是,流程编排系统必须记录参数在每次循环前后的一致性。如果系统能检测到模型下一次调用传的入参与上一次完全相同,就可以提前拦截,而不是让模型白白重试。这也是调试智能体时最有用的一组日志:时间戳、技能名、参数快照、返回状态。

5.3 失败之后的兜底设计要提前做

失败恢复经常被忽略,直到线上出事才想起来。常规做法无非是重试,但智能体场景的重试逻辑需要有更细的粒度。

我把失败处理分成三个层次:

  • 第一层:重试。适用于超时、临时性网络抖动,重试前对参数做幂等校验,确保重试不会造成重复扣款或重复下单。
  • 第二层:降级。比如某技能依赖的主数据源挂了,如果备用数据源能提供大致可靠的信息,就降级调用备用数据源,并给模型返回一个标记,提醒它当前结果可能不是最新,需要向用户说明。
  • 第三层:人工接管。高危操作或连续两次失败,直接停止模型自主行动,把当前状态和决策历史打包,转给人工处理。

这套分层设计不仅提升了成功率,更给了产品一个明确的承诺边界:模型搞不定的,至少会在原地站住,而不是越陷越深。这一点对用户信任的建立非常重要。

6. 从单体技能到技能市场:规模化的演进之路

一套技能体系能用是一回事,能让团队持续往里加新技能、不相互干扰、不越改越乱,是另一回事。随着业务扩展,我们逐步摸出了一些规模化演进的经验,这里分享最关键的三点。

6.1 技能版本化与制品化

技能不是写一次就完了。模型升级、外部接口变更、业务规则调整,都会导致技能行为变化。如果不对技能做版本管理,线上就是一个黑盒:你到底跑的是哪一版逻辑?说不清。

所以每个技能都是一个独立的制品,版本号语义化。发布路径是:开发分支编码 -> 离线评测集跑分 -> 灰度环境放量 -> 全量上线。模型侧的技能描述和参数协议也跟随版本走,不能只改实现不改描述。我踩过这个坑:某查询技能的底层逻辑改了返回值格式,但技能描述没改,模型还按旧协议取数据,结果一直取到空值,用户那边看到的全是"查无数据"。

版本化带来的另一个好处是回滚很容易。一旦上线的新版本在线上表现异常,直接把版本切回上一个稳定版,不用改代码,不用换模型,几分钟搞定。

6.2 评测集是技能质量的考试大纲

前面提到离线评测集用来检验描述质量,到了规模化阶段,评测集的意义就不止于此了。它应该像项目的考试大纲一样,覆盖每个技能的核心路径、边界条件、异常输入。我见过一个反例:一个新同事加了个"查询会员积分"的技能,描述写得很漂亮,但没测过"会员不存在"的场景。上线后用户一问到不存在号,模型就反复查,日志里全是错误码。

把每个技能的评测用例沉淀下来,纳入 CI 流程,任何技能变更都必须跑一遍全量评测集才能合并,是防止"改一个技能弄坏另一个技能"的最有效手段。尤其当技能数量超过几十个后,回归测试的价值无可替代。

评测集怎么建?我的建议是,每处理一次线上异常就补一个用例,库里的用例只增不减。用不着一次搞得很全,关键是持续沉淀。半年下来,你的评测集就是团队最值钱的知识资产之一。

6.3 组织级技能中心的价值与成本

当技能数量多到一定程度,共享就变得非常有价值。一个部门开发好的"发票识别"技能,另一个部门也有类似需求,如果没有技能中心,两边各做各的,重复开发不说,能力还参差不齐。

我的做法是把技能清单、评测集、使用文档放到内部统一的平台上,支持检索和申请调用。使用方不需要知道技能的底层实现,只需要在配置台上启用,然后把自己的鉴权信息接进来。技能的运行和维护仍由原团队负责,平台提供监控和调用统计。这条路走通后,新项目接一个新能力的时间从按周计缩短到按天计。

当然也有成本:技能文档要维护,权限要定期审计,废弃技能要清理。但总体来看,组织级技能中心属于越早做越划算的投入。它把我们前面讲的所有散点——描述规范、执行管线、评测集、版本管理——全部串成了一个体系。这也是我把这个项目命名为 agent-skills 的最终意义:技能不是零散的函数,而是组织能力的结构化沉淀。

最后再分享一个我个人特别受益的小习惯:所有技能描述上线前,先丢给模型做一次"阅读理解",让它用自己的话复述这个技能能做什么、不能做什么、需要什么参数。如果复述得乱七八糟,说明这个技能描述连模型自己都看不懂,上线后也会让别人看不懂。就靠这个简单的测试,我至少拦下了十几个质量不合格的技能。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进