AI Agent技能系统实战:从工具调用失控到稳定治理的工程方法论 先说一句掏心窝子的话Agent 项目最累的地方往往不是模型推理而是把“能力”收拾清楚。我去年接手内部一个代号叫“skills”的项目核心目标就一句话让 Agent 能稳定调用几十个内部 API而不是在工具之间乱撞。做之前我以为难点在模型调优做完才发现真正值得沉淀的是技能系统本身的设计思路。这篇就当一份实战复盘给正在做 AI Agent、工作流编排或 RPA 落地的朋友参考尤其是功能越接越多、开始出现“工具选择失控”迹象的团队。如果你还停留在 demo 阶段可能觉得这是小题大做但一旦你的 Agent 要 7×24 小时跑真实业务技能治理这件事早晚得补课。下面我按我们实际从零搭起来的方案来拆解包含数据模型、调度策略、生命周期管理和排查窍门可以直接照着改。1. 把“skills”当做一个项目来做1.1 从“一句话需求”到技能系统的演变产品最初提需求时很轻巧让 Agent 会调用各种工具。于是团队开始列清单——查天气、查库存、发起审批、计算报价、更新工单……列到第三十个大家沉默了。因为这不只是写三十个函数的问题更麻烦的是每个工具都有不同的入参、不同的鉴权方式、不同的返回格式、不同的失败语义。而 Agent 必须自己判断“现在该用哪个”这些信息如果没有结构化管理只能靠模型硬猜。我们试过最朴素的办法把所有 API 说明拼成一段超长提示词塞给模型。结果是五六个工具以内还能勉强撑住一旦超过十几个模型开始“串味”——用错参数、调错工具甚至把一个订单查询接口当成售后投诉接口来用。而且这种“提示词粘贴板”式集成几乎没有调试手段你根本不知道模型是基于什么理由做的选择出了问题只能靠猜。后来我们把“技能”从代码里抽出来不再当成一个函数而是当成一套独立的资源和系统。这个系统需要回答五个问题技能怎么描述怎么登记和注册怎么被 Agent 发现怎么执行怎么监控和治理这就是“skills”项目的起点。说白了它不是一个 NLP 优化问题而是一个工程治理问题。1.2 技能系统的核心能力边界给系统定位是第一步。我们定了四层能力第一层是描述层。让 Agent 通过结构化清单“看懂”每个技能是什么、什么时候该用、参数怎么填。这一层做不好后面全白搭。第二层是注册层。让开发者可以按标准格式把新技能接入系统不必改 Agent 的主逻辑也不必重新拼提示词。第三层是调度层。根据用户请求上下文从技能库里选出最合适的技能并完成参数生成、校验、调用。第四层是治理层。记录所有调用、评估效果、灰度发布、版本回滚、权限控制。这四层听起来像传统的 API 网关但我觉得它们和网关有本质区别网关关注的是流量和协议我们关注的是“语义和方法适配”。也就是说skills 的核心不只是“帮你把请求转发出去”而是“帮模型决定该转发给谁、用什么姿势转发”同时让这个决策过程有迹可循、可干预、可回退。所以我的建议是如果你决定做技能系统从一开始就按这四层去规划边界别把它当成一个简单的工具注册表。2. 技能系统的数据模型与目录设计2.1 技能描述文件让 Agent 看懂技能的 4 个核心字段搞技能系统第一步不是写代码而是定义描述文件。我们用 YAML 做技能清单一个技能一个文件核心字段分四块基本信息、触发条件、输入输出、执行方式。下面是一个真实场景简化后的描述文件id: order_status_query name: 查询订单状态 version: 2.1.0 description: 根据订单编号查询当前订单的处理进度适用于用户主动询问订单到哪一步的场景 tags: [订单, 物流, 售后] trigger: intent: query_order_status keywords: [订单状态, 订单进度, 物流到哪了] disallow_when: - 用户尚未登录 - 订单编号缺失 input: order_id: type: string required: true source: user_explicit description: 完整的订单ID格式为SO开头加12位数字 include_history: type: boolean default: false source: model_default description: 是否返回历史流转记录 output: status: string update_time: datetime history: array execute: endpoint: http://internal-gateway/api/orders/status method: GET auth: service-account-a timeout_ms: 3000这个文件看着简单里面其实有大量细节。我挑几个重点说说。第一个重点是description字段。我强烈建议写成“什么场景下用”而不是“这个接口是干什么的”。我们早期吃过亏技能描述写的是“查询订单状态接口”结果用户在投诉物流太慢时Agent 也去调这个接口因为模型的语义理解里没有“该接口只回答进度不处理抱怨”。后来我们把所有技能描述都改成“适用场景 不适用场景”的写法比如补一句“当用户表达不满或投诉时不应调用本技能应转接售后意图”。只改了描述意图路由准确率就有明显提升。第二个重点是trigger里的disallow_when。这个字段经常被忽略但它是硬规则的门槛。模型再聪明也需要在边界条件上被约束。比如“用户未登录时不允许查订单状态”这类前置条件如果只写在描述里模型可能忽略但写成结构化的disallow_when调度器就能在执行前做硬校验直接拦截。第三个重点是input里的source字段。它决定了参数从哪来是必须用户显式提供还是可以从会话上下文提取或者允许模型默认填充。这个字段是后面治理“参数幻觉”的关键。第四个重点是execute里的timeout_ms。没有超时控制的技能调用是线上事故的温床。一个下游接口如果慢响应可能拖住整个 Agent 的响应链路。所以每个技能都要单独声明超时上限。2.2 技能仓库的目录结构设计技能文件多了以后目录结构就成了隐藏的技术债。我们用的结构类似 monorepo按业务域划分skills/ ├── catalog/ │ ├── order/ │ │ ├── query_status.yaml │ │ ├── cancel_order.yaml │ │ └── apply_refund.yaml │ └── customer/ │ ├── search_profile.yaml │ └── update_profile.yaml ├── schemas/ │ └── skill_schema.json ├── libs/ │ ├── auth_adapter.py │ └── http_client.py └── registry/ ├── skills.db └── metrics.db目录按业务域划分的好处等到做权限隔离和灰度发布时才会体现出来。比如客服 Agent 只需要访问customer域和order域的部分技能数据团队 Agent 只需要访问analytics域。这时候业务域就是天然的权限边界。我见过不少团队一开始把所有技能平铺在一个目录里两三个月后开始要求区分业务身份只能大量重构。这个调整越早做越好——在写第一个技能文件时就按业务域建目录成本为零。另外schemas/skill_schema.json建议从一开始就维护。我们用它做 CI 校验团队提交新技能文件时先自动检查字段完整性不合格直接就发布失败了。2.3 技能 Schema 的版本兼容这一条是后来踩坑补上的。技能描述文件本身也是会进化的今天加一个字段明天改一个字段。如果没有 Schema 版本兼容策略就会出现老技能文件被新校验规则卡住的情况。我们现在的做法是skill_schema.json本身用语义化版本管理新增字段时如果只是加可选配置升 minor 版本如果字段语义有破坏性变化升 major 版本。每个技能文件里声明schema_version系统加载时按对应规则解析。这样就不会出现“今天系统升级、明天一堆技能挂了”的窘境。3. 技能调度与自动匹配的实现思路3.1 三层技能匹配策略从向量召回到 LLM 排序技能系统最核心的难点是路由决策。我们最初试过纯关键词匹配效果很差。原因很简单用户不会按说明书说话。比如“我那个手机收到了但屏幕碎了”这句话里没有“退换”“售后”这样的词但你的 Agent 可能既要调订单查询也可能要进入投诉流程。关键词匹配在这种表达面前基本失灵。后来我们改成三层匹配策略。第一层是向量召回把技能描述、标签和触发关键词都做 embedding向量化存库。用户输入进来后算相似度召回 Top 20。第二层是规则硬过滤根据当前会话上下文做前置条件检查比如用户未登录就过滤掉所有涉及隐私的技能。同时把不满足disallow_when条件的召回项排除。第三层是 LLM 排序经过过滤后剩下的候选技能通常只有 2 到 5 个这时候让模型在“候选集”里做最终选择而不是让模型“凭空想”。这个三层设计的核心思路是把模型的“无限选择空间”压缩成“有限候选集”。模型从一堆模糊选项里选一个和从两三个明确选项里挑一个准确率完全不在一个量级。实测下来纯第一种策略的准确率大概只能到 70% 出头加上硬过滤和 LLM 排序后能稳定在 90% 以上。这里给一段简化的路由流程伪代码def route_skill(user_input: str, context: dict): # 1. 向量召回候选集 candidate_ids vector_retrieval(user_input, top_k20) # 2. 规则硬过滤 valid_ids hard_filter(candidate_ids, context) # 3. LLM 排序选择 ranked llm_select( user_input, skills[registry.get(i) for i in valid_ids], contextcontext ) # 4. 返回最优技能 return ranked这里有一个细节llm_select这一步对 prompt 的写法很敏感。我们的模板每次都会把候选技能的 description 和 trigger 信息完整渲染出来并要求模型输出“选择理由 技能 id 参数填充建议”。这个理由会被记入日志成为后面排障的一手证据。3.2 参数生成与来源约束路由只是第一步更折磨人的是参数。模型经常“脑补”参数值。比如用户说“查一下我最近的订单”模型可能自己编造一个并不存在的 order_id然后傻乎乎地传给接口。我们在 skills 里加了两道防线。第一道是参数来源标注已经在前面描述文件里提过input字段的source标记了参数来源user_explicit表示必须用户显式提供context_extract表示可以从对话上下文里抽取model_default表示允许模型按默认规则生成。第二道是运行时校验。调用接口之前系统会先对参数做类型和格式校验不通过就进入“澄清流程”。这里说的澄清流程并不是死板的“请您重新输入订单编号”而是带着当前已收集到的部分信息回问用户。比如系统发现 order_id 缺失但会话里已经知道用户姓名那澄清的问题就是“请问您想查哪个订单是 9 月 3 日那个还是 8 月 20 日那个”这种带着上下文的追问用户体感好很多而且能一次把参数补全。这道防线收益极大。我们的接口层报错率从 38% 直接降到了 6% 左右。很神奇的是这个问题跟模型选型关系不大纯粹是系统该拦的时候没拦住。模型天生就倾向于把对话补完整你越依赖它自己判断它越会自信地给你编参数。3.3 上下文传递与状态保持技能调用不是孤立的尤其是多轮对话场景。我们刚开始做时每个技能调用之间没有任何状态传递导致用户说“点一下订单详情”时Agent 完全不知道“点一下”指的是哪个订单。我们的解决办法是在调度器里维护一份会话上下文对象包含当前用户、当前订单、当前页面等关键实体。技能描述文件的context_extract来源就是从这里取值的。但注意上下文对象有生命周期不能一直堆着。我们一般只保留最近五轮的关键实体更久远的信息如果需要就通过专门的上下文查询技能去拉取。这是为了避免上下文污染——会话里出现了两个订单 id模型容易搞混状态管理必须“当下明确过期清理”。3.4 多技能编排与冲突处理真实业务很少是单技能调用更多是几个技能串起来。比如“用户想退款”你需要先查订单、校验订单状态然后发起退款申请流程。这三个动作如果并列暴露给 Agent它很容易乱序。我们的做法是把这类原子技能用工作流模板包一层注册成一个“复合技能”。复合技能也有自己的 description 和 trigger但 execute 不再指向单个接口而是指向一个有向无环图。图中每个节点引用一个原子技能节点之间可以定义条件分支和循环。复合技能的内部编排依赖一个轻量的流程引擎。我们调研过一些现成的编排框架最后选用了一个自己写的小引擎原因是技能系统需要和 Agent 的对话状态深度融合通用编排工具反而要写大量胶水代码。引擎的核心抽象只有三个节点、边、上下文。每个节点执行完把输出写入上下文下一个节点读取一个简单的流程图长这样[订单查询] - [资格校验] - [退款申请] - [通知结果] | --失败-- [人工介入]至于技能冲突我们制定了一条优先级策略显式意图 上下文强关联 最近调用成功记录 通用兜底。举个例子用户说“帮我退货”订单域的退货技能和售后域的退货技能都能响应但当前上下文停留在售后工单页那就先选售后域。这条优先级策略看起来有点土但它在真实场景里的稳定性比让模型自由发挥高得多。4. 技能生命周期管理版本、评估与权限4.1 技能版本控制与回滚任何真实系统没有版本管理就是灾难。skills 里的每个技能文件本身就是版本化对象我们把它存在 Git 里发布时用 tag 标记版本号。Agent 运行时的快照会指向一组精确的版本清单而不是永远拉“最新版”。这里分享一个让我印象深刻的教训。有一次我们更新了订单状态接口的返回格式把status_code字段改成了status。按道理这是破坏性变更应该升级主版本号。但当时图省事只改了接口没升级版本结果所有旧版 Agent 还在按旧字段解析整整一个白天线上误报没人发现。后来我们定了一条铁律技能接口任何字段变更必须升级主版本号同时 Agent 的运行时声明自己兼容的技能版本范围。调度器在路由前先检查版本兼容性不兼容就直接报出“该技能需要升级”。这条铁律不单是给技能系统上规矩也是给接口提供方上规矩。现在每次接口变更后端同学会主动检查技能版本因为知道不升级就会被系统挡住。4.2 技能质量的自动化评估指标技能不是上线就完事了还要持续评估。我们每次技能调用都会记录四个核心指标调用成功率执行层有没有报错。参数有效接受率Agent 生成的参数被接口成功接受的占比。澄清率需要追问用户补充信息的比例。这个指标太高说明触发判断不准或参数描述不清。用户反馈修正率用户明确表示“不是这个意思”或“你理解错了”的占比。这四个指标按技能维度汇总一周出一份报表。偏差大的技能自动进入“待优化”列表由负责人去排查是描述问题、触发条件问题还是接口本身问题。除了线上指标我们还加了离线评测集。做法是拿历史对话样本做模拟测试看 Agent 能否选对技能、填对参数。这个评测集要定期清洗否则会过拟合——同一个场景反复出现三个月后模型已经“背下答案”了对新增技能完全不敏感。4.3 沙箱隔离与权限控制技能要访问外部系统权限问题避不开。我们一开始给 Agent 配了一个高权限主账号结果测试阶段就出了事故一个测试脚本错误调用了写操作接口把一批测试数据改乱了。后来我们把权限模型彻底改掉skills 里每个技能拥有独立的 service account遵循最小权限原则。查询库存的技能只有只读权限要改库存必须走另一个带审批流的技能。权限在技能描述文件的auth字段里声明调度器执行时动态获取临时凭证用完即销毁。沙箱执行也算一个重要环节。对内部 API 来说把每个技能丢进容器里跑并不现实但我们至少做到了四点超时上限、重试次数限制、敏感数据脱敏、请求体大小限制。其中数据脱敏很容易被忽略很多 Agent 事故不是模型问题而是某个技能把完整的敏感返回数据打进了日志。我们为此加了一个日志过滤器统一识别手机号、身份证、token 等敏感字段输出前自动打码。5. 常见问题与排查技巧实录5.1 技能匹配漂移模型突然“不听话”现象很典型昨天技能匹配还好好的今天突然开始频繁选错。团队第一反应是模型版本变了但查完之后发现模型没变。真正的原因往往是技能库本身的扰动——比如新技能上线后向量召回的结果被改变了或者某个技能的 keywords 被更新了。我们现在的做法是把每个技能的 embedding 固定在基线向量库中新技能发布时先计算与现有技能的平均相似度如果重叠度过高就拒绝发布要求修改技能描述中的差异化信息。这个机制有些“呆板”但它能强制大家在描述层面把相似技能区分开效果很稳定。还有一个配套手段每天凌晨跑一次技能匹配随机抽样自检把匹配结果和“专家标注期望”做对比偏差超过阈值自动告警。5.2 参数幻觉模型自己编造参数这是最难的坑。模型非常擅长把用户没说全的信息补齐补齐的不一定对。除了前面提到的参数来源标注和运行时校验还有一个应急技巧给input字段增加explicit_request_only枚举。一旦开启任何非用户显式输入的参数值都不允许直接填入必须走澄清流程让用户亲口确认。这套机制我们上线后挡住了 90% 以上的参数幻觉。剩下的 10% 属于语义级幻觉比如用户说“随便查一个最近的订单”模型真的随机挑了一个。这种情况没法全靠规则拦只能靠用户体验兜底——在回复里加一句“我为您查询了 9 月 3 日的订单 xxx如果不是这个请告诉我”。5.3 长尾技能利用率低技能库越做越大八成调用量集中在两成技能上是正常现象。剩下那些长尾技能不是没用而是 Agent 在绝大多数情况下“想不起来”。我的建议是不要急着删先做周级分析把零调用技能分成两类“该召回但没召回”和“确实没人需要”。前者是描述和触发条件问题需要优化入口后者可以先下线下线后如果用户通过兜底流程提出类似需求再灰度恢复。另外可以在 Agent 的系统提示词里给长尾技能加一块 mini 索引只列技能图标式的一句话介绍。这种轻量提示在关键时点能起到唤醒作用。但注意不能加太多否则提示词会膨胀反而影响模型其他能力。5.4 技能调用的日志与追踪最后说一个最容易被忽略、影响却最大的点技能调用的可观测性。没有完整的调用链讨论“Agent 为什么错了”基本等于瞎猜。我们为每次调用生成了一个skill_call_id把技能选型、参数生成、HTTP 调用、结果返回、澄清触发等环节全部串在一个 trace 里。实际排查问题时我最常用的是看两个地方。第一模型的“选择理由”字段它会输出为什么选这个技能。第二参数来源标志可以清晰看到哪个参数是用户给的、哪个是模型猜的、哪个是上下文抽出来的。有一次我们排查一个退款失败 case只看结果以为是退款接口问题把 trace 放开后才发现是上下文提取阶段拿错了 order_id——问题根本不在下游接口而在上游状态管理。这种排查效率是之前“对着日志瞎猜”完全比不了的。6. 最后分享两个小技巧再补充两个我在实际维护中摸索出来的小经验。第一个是关于技能描述语气的。写description时尽量用“当用户做了什么事、处于什么状态时使用”的句式而不是“该技能用于……”的说明文体。前者更容易被模型理解成触发规则后者更像文档。我们后来专门做了一次全部技能的描述文案重写只改了表达方式匹配准确率就提升了 5 个百分点。第二个是关于技能测试的。给每个技能建一个“最小测试集”包含正向场景、反向场景、参数缺失场景、歧义表达场景。这个测试集不需要很长每个技能十几条对话就够。关键是每次技能描述或接口变更时跑一遍能最快时间发现回归。我们把这个测试集挂到 CI 里新技能上线前必须过一遍旧技能的反向测试防止新技能描述污染了老技能的路由效果。说到底把 skills 当成一个独立子系统来设计最大的收获不是几个指标变好看而是团队终于能理性地讨论 Agent 的能力边界了。过去我们总说“模型不够聪明”后来发现很多问题其实是技能系统没有给模型创造一个足够清晰的决策环境。技能的描述、路由、监控、回滚这些工程细节做扎实之后模型反而显得更“聪明”了。如果你也被 Agent 的工具调用问题折磨得焦头烂额不妨从最小的技能描述文件开始整理。一个技能一个技能地规范化很快你就会看到变化。