☰
Agent技能工程化:从注册、编排到灰度发布的生产实践
2026/10/14 9:23:34 网站建设 项目流程

一次不算复杂的任务,Agent 还是翻车了:让它把一批 PDF 账单整理进表格,它先调了文档解析技能,又拿结果去走报销分类流程,中间因为一次字段格式不匹配,它居然自己改了分类规则,最后产出了一份“看起来很合理、但数字对不上”的表。

排查下来,问题不在模型理解能力,而在技能层——Agent 对“能做某事”和“正确地做某事”之间缺了一层可管理、可校验、可回退的约束。这也是我觉得“agent-skills”这类话题值得单独拿出来聊透的原因。

平时看很多项目,大家把 Agent 当“会说话的 API 调用器”,给它配几个 Prompt、挂几个工具函数,就以为万事大吉。结果一上真实业务就暴露:工具多了模型选不准,参数错了没人校验,技能升级了旧会话还在用老逻辑,甚至两个技能同时操作同一份数据直接把状态搞脏。这些问题的根子,基本都是“技能”没有作为独立的工程对象被设计和管理。

这篇文章不会讲怎么调 Prompt 让模型更听话,而是从工程角度拆一套可落地的 Agent 技能体系:技能怎么定义、怎么注册、怎么编排、怎么评估、怎么长期维护。适合正在做真实业务 Agent、或者准备把原型往生产环境推的同学,哪怕你现在只是用低代码平台搭智能体,这套思路同样能用上。

1. 技能不是 Prompt 模板,是一等公民

1.1 为什么要把“技能”单独拎出来做系统

“Agent 技能”这个词现在被用得很泛。有人把一段系统提示词叫技能,有人把几个 Python 函数包一层叫技能,也有人直接把某个外部 API 封装一下就算技能。

我的观点很直接:如果技能只是“让模型知道可以做某件事”的文案,那它根本不配叫技能,顶多算提示词碎片。真正的 Agent 技能,至少要满足四件事:

  • 可描述:模型或者其他调用方,能通过一段结构化描述准确理解它能干什么、不能干什么。
  • 可调用:有明确的入参、出参、失败模式,而不是靠模型自由发挥。
  • 可验证:每次调用都留痕,能回放、能对比、能评估效果。
  • 可回滚:技能升级出问题时,可以快速切回旧版本,而不是整个 Agent 一起挂掉。

把这四点做到位,技能就变成了一种类似“插件系统”的一等公民,而不是散落在代码和 Prompt 里的幽灵。我们的经验是,越早把技能当成独立工程对象来治理,后面接业务的时候就越省心。

1.2 技能协议里的最小可用 Schema

先说清楚,我这里不想把 Schema 设计得很重,一上来就搞复杂规则引擎,团队根本推不动。建议从一份“够用但不啰嗦”的 JSON Schema 起步:

{ "schema_version": "1.0", "skill_id": "invoice_bill_import", "name": "账单导入与归类", "description": "解析账单文件并写入对账表,支持 PDF/CSV,自动提取金额、商户、日期并按科目归类", "input_schema": { "type": "object", "required": ["file_path", "target_sheet"], "properties": { "file_path": { "type": "string", "description": "账单文件路径,PDF 或 CSV" }, "target_sheet": { "type": "string", "description": "目标表名,如 finance_2025q1" }, "overwrite": { "type": "boolean", "default": false, "description": "是否覆盖已有记录,默认只追加" } }, "additionalProperties": false }, "side_effects": ["write:finance_table", "notify:audit_log"], "version": "2.3.1", "timeout_sec": 60, "idempotent": true }

这份 Schema 里,description的作用经常被低估。它不只是给人看的注释,而是模型做技能选择时的“匹配依据”。写得太宽泛(比如“处理文件”),模型就容易在多个技能之间犹豫;写得太细(比如把每种文件格式都枚举一遍),模型又会因为描述与用户问题之间缺乏字面匹配而漏选。

我推荐在 description 里包含三类信息:技能负责的典型任务;技能的边界(不做什么);触发它最常见的用户表达。举个例子,“账单导入与归类”的描述里,我会加一句“仅支持对公账单,个人报销单请走 expense_claim 技能”,模型选错的概率会明显下降。

1.3 一个技能从想法到落地的生命周期

把技能当作工程对象之后,我们会给每个技能建立生命周期,而不是“写完代码就能上线”:

  1. 需求澄清:明确要解决什么任务,边界在哪,输入输出是什么。
  2. 接口设计:定输入 Schema、输出 Schema、失败码、幂等策略。
  3. 原型调用:让模型在一个沙箱环境里实际调几轮,观察描述是否足够让模型选对。
  4. 评测集建设:准备几十条典型样本和失败样本,跑回归。
  5. 灰度上线:先影子模式跑,再小流量,最后全量。
  6. 持续维护:看日志、看误用、看成功率,根据真实使用迭代。

这套生命周期看起来像是给传统后端接口设计的,但它对 Agent 来说甚至更重要。因为传统接口被误用了,通常报错或者数据异常,人能比较快发现;而 Agent 技能被误用,模型往往会尝试“修补”调用,结果产生一串看似合理、实际有毒的动作链,等到人发现已经晚了。

2. 技能注册中心:少做一个字段,后面排查多花三天

2.1 注册表应该记录哪些元信息

我们在做 agent-skills 项目的第一个版本时,注册表只存了三样东西:技能名、接口地址、一句话描述。结果上线一周就出问题——某个技能更新了参数格式,但注册表里的描述、示例、版本号全是旧的,模型调用的时候还用老参数,报错了它也不告诉你为什么报错,而是换个参数自己硬试。

后来我们重新梳理了注册表的元信息,以下是目前跑生产最有效的字段集合:

字段作用备注
skill_id全局唯一标识一旦发布不再变更
version语义化版本号遵循 semver,破坏性变更必须升大版本
description模型可读的能力描述要写边界和典型触发表达
input_schema / output_schema参数和返回结构模型和校验器共用,保证单源真实
side_effects可观察到的副作用比如写库、发通知、调外部接口
idempotent是否可安全重试false 的技能建议加幂等键
timeout_sec超时上限防止模型误判导致长时间挂起
owner_team负责团队出问题时能找到人
statusenabled / deprecated / sunset生命周期状态
changelog最近变更方便回溯“哪次变更导致行为变化”

这里特别想提醒side_effects,这个字段会在两种场景发挥巨大作用:一种是模型做规划时,需要知道某个技能会不会产生不可逆影响(比如“发送邮件”就不能被随便重试);另一种是排查多技能编排问题时,你能在日志里快速定位到底是哪个技能动了哪块数据。

2.2 技能发现与路由:别让大模型自己“猜”参数

技能注册表建好之后,接下来要回答的是:模型怎么从几十个技能里找到对的那一个?

很多方案是直接把技能清单全部塞给模型,让它在上下文里自己做工具选择。技能少的时候没问题,技能到了 30 个以上,模型的选择准确率就开始肉眼可见地下降,更麻烦的是,上下文里塞一堆冗长描述,还会挤占模型处理真正任务内容的注意力。

我们的做法是“先路由、后生成”。在模型做决定之前,用一个轻量的检索/打分模块把候选技能过滤到 5 个以内:

  • 把用户请求意图标签化(比如“导入”“查询”“统计”“导出”)。
  • 基于技能描述做向量召回,取相似度最高的前 N 个。
  • 再用规则过滤,比如用户提到的文件格式、业务域、权限范围,把不满足的可执行条件直接排除掉。
  • 最后把候选技能的结构化描述交给模型,让它从少量候选中做最终选择。

这套“粗筛 + 精排”的思路,本质上是把一部分模型容易做错的、计算成本又高的决策,前置到一个可控模块里。实践中技能选择准确率能提升到接近 98%,而对模型上下文长度的消耗反而减少了很多。

2.3 版本与兼容策略

技能版本管理有个很容易被忽略的问题:你有几十个技能,Agent 会话可能持续好几天,如果会话中间某个技能做了破坏性升级,旧会话继续运行时行为就完全不可控了。

我们目前执行三个规则:

  1. 破坏性变更必须升大版本,旧版本保持可用至少 30 天。
  2. 每次调用都把 skill_id + version 记录进 trace,回放时严格按当时版本执行。
  3. 技能上线新版本时,自动刷新 wait 会话中的引用;长会话如果检测到技能版本不匹配,先提示用户结束会话或跳过低风险步骤。

这套规则听上去很基础,但它能挡住很多生产事故。我们曾经有一次因为技能版本未做迁移,导致模型在长会话里一直调用旧版解析逻辑,恰逢上游接口字段改名,产出的数据整整错了一天,排查起来非常痛苦。从那之后,版本兼容策略就变成了注册中心的红线。

3. 技能编排中最容易翻车的三个地方

3.1 上下文污染:技能A留下的消息,把技能B带偏

多技能编排的第一个坑是上下文污染。之前我们内部做一个“发票 OCR + 自动翻译”的流程,OCR 技能输出了一份结构化的字段清单,然后翻译技能接着处理。结果模型在生成翻译时,把 OCR 结果里的噪声字符也当成了需要翻译的正文,还“自作聪明”地补充了不存在的公司抬头。

问题在于,我们当时把 OCR 技能的输出直接追加进了主对话上下文,没有做隔离。翻译技能不仅看到了结构化数据,还看到了原始坐标、置信度、识别候选词。它没办法区分哪些是任务数据、哪些是中间过程数据,于是全都当成依据。

解决方案是在技能编排层引入“上下文隔离区”:每个技能的输入只能来自被显式声明的数据通道,技能产生的中间结果只进入技能内部的工作区,不自动混入主对话。需要在后续技能中引用的字段,必须通过data_ref显式传递。这个约束会让流程定义多写几行代码,但能避免掉 80% 以上的误导情况。

3.2 竞态冲突:两个技能同时在写同一份状态

竞态冲突在单 Agent 流程里不常见,但只要并发任务一上,问题立刻暴露。

我们有一个“日程管理”流程,日历技能和提醒技能是两个独立技能,它们都会更新用户的状态文件。有一次并发运行了两个任务:一个改了日历事件的标题,另一个读取旧标题去设提醒时间。因为两个技能分别读写同一份文件,后写的覆盖了先写的,最终日历标题变成了旧值,提醒时间也错位了。

排查下来,发现我们没有对共享状态做并发控制。后来我们定了一个规则:凡是声明了相同side_effects目标的技能,编排层必须给它们分配同一个“状态锁”,锁粒度是技能级而不是会话级。同时对共享资源采用乐观锁,写入前比对版本号,版本不一致就抛冲突错误,让模型决定是重试还是询问用户。

这个改动之后,类似的覆盖问题再没出现过。建议你在设计技能时,就明确每个技能写哪些资源,注册表里把这些资源 tag 统一标准化——这不是形式主义,而是并发控制的基础信息。

3.3 幂等失效:重试惹出的大麻烦

模型在调用技能时经常因为网络超时、上游返回慢等原因重试。如果你的技能不是幂等的,重试一次可能在业务侧造成双倍副作用。

最常见的就是“发送通知”类技能。某次外部接口实际已经发起了短信发送,但响应超时,Agent 判定失败后重试了一次,结果用户收到了两条一模一样的通知。到最后这个技能上线了一套强制幂等策略:

  • 每次调用,模型侧生成一个request_id(UUID)。
  • 技能执行前先查处理结果表,如果request_id已存在,直接返回上次结果,不再执行真实操作。
  • 技能执行后把结果写入处理结果表,事务与业务操作保持同批次提交。

幂等设计应该成为所有非查询类技能的默认要求。尤其是那些写库、发消息、生成订单、更新状态类的操作,宁可多写这几行保障代码,也不要去赌模型“只重试一次”的侥幸心理。

在为技能配置幂等策略时,我建议:

技能类型幂等策略示例
查询类无需幂等查库存
写库类业务唯一键 + 去重表提交报销单
发送类幂等键 + 重复检查后再发送通知用户
创建资源类客户端生成幂等键,冲突返回旧结果创建任务

4. 评测与灰度:怎么证明你的技能没把 Agent 变蠢

4.1 技能评测集的构造原则

技能系统上了生产之后,大家最关心的问题通常是“这次技能升级到底有没有让效果变好,还是变坏了”。很多团队凭感觉判断:跑几个例子,觉得“看起来差不多”,就发布了。这样很容易把隐性问题带回线上。

我的经验是,每个核心技能至少准备三类样本:

  • 典型正例:正常情况下最常被触发的任务,保证基本功能不回归。
  • 边界样本:参数临界、输入格式不规范、时间敏感等场景,比如账单金额超过 999999 会不会精度丢失。
  • 对抗样本:模型容易误用的场景,比如某技能描述里明确说了“不支持个人报销”,但用户硬塞个人发票,测试模型是否会错误调用并继续执行。

这三个维度加起来,一个核心技能我建议至少配 60-100 条用例。不是每一条都要人工标注输出结果,但技能输出必须可以被自动判定,比如结构化字段比对、写入结果的正确性检查。

4.2 回归测试与“黄金样本”

我们内部训练了一个“黄金样本库”的概念,其实就是一组历史真实任务及其最优执行轨迹。每次技能改动之后,我们会用同一批任务重新跑一遍完整的 Agent 流程,然后从三个维度做对比:

  • 任务完成率:最终目标有没有达成。
  • 路径偏差度:和黄金轨迹相比,步骤顺序和工具选择是否出现异常分支。
  • 资源消耗:往返调用次数、Token 开销、外部 API 调用频率。

如果任务完成率不变,但路径偏差明显,就值得警觉。因为这说明技能变更可能让模型走了更绕的路。短时间看不出问题,但积累下去会变成诡异的行为漂移。

还有一个容易被忽视的点:修改技能描述,跟修改代码逻辑一样需要回归测试。很多人以为“只是改几个字,不影响功能”,结果模型因为描述里新增了某个关键词,开始频繁误触发该技能,其他技能的使用率反而骤降。

4.3 灰度发布的三段式

技能灰度不同于普通后端接口的灰度,因为 Agent 行为是非确定性的,你不能只验证“接口响应是否正常”,还必须验证“模型是否在合适的时机使用了它,并且没有因此打乱其他步骤”。

我们目前用的是三段式:

  1. 影子模式:技能只记录,不真正执行。在线流量到达时,路由模块照常做技能选择,但选中的结果只写日志,线上 Agent 依然走旧逻辑。这个阶段可以收集“如果新技能上线,会被哪些请求触发”,从而判断触发频率和场景分布。
  2. 小流量模式:选 5%-10% 的新会话,允许它们真正调用新技能。观察成功率、副作用是否异常、以及是否出现了旧技能未出现过的新错误类型。
  3. 全量发布:等小流量阶段数据稳定后,再逐步放开到全量,保留快速回滚开关。

灰度期间我会重点关注两个指标:一是技能报错率,二是“模型主动放弃该技能”的频率。如果模型在灰度中频繁选择了新技能又中途放弃,通常不是模型问题,而是技能的输入输出设计和实际场景不匹配。

在这套流程跑顺之后,我们内部的技能发布周期从“谁改完谁发”变成了有纪律的“按批次发布”,生产事故率明显下降,而迭代效率反而提升了,因为少花了很多时间在线上救火和排查诡异的模型行为上。

5. 经验沉淀:技能维护不是“写完就完”

5.1 技能日志与追踪

Agent 技能排障最怕的是“不可复现”。模型的一次随机路径,加上技能内部的网络交互,最终得到一个结果,如果你没有完整的调用链记录,出了异常就只能靠猜。

我们为每一个技能调用都加上了统一的 trace 日志结构:

{ "trace_id": "9c0a8f2e...", "session_id": "sess_771", "skill_id": "invoice_bill_import", "skill_version": "2.3.1", "request_id": "req_abc123", "input_hash": "sha256:...", "started_at": "...", "finished_at": "...", "status": "success", "output_ref": "s3://trace-outputs/9c0a8f2e.json", "model_call_count": 4, "retry_count": 1, "error_code": null }

这套日志的价值在复盘时立刻显现。某次上游接口异常导致一批技能失败,我们靠着 trace_id 把所有受影响会话全部捞出来,快速定位到具体技能版本和参数分布,半天之内就完成了回滚和用户侧补偿。

另外,input_hash 这个字段值得留意。它能帮你发现同一个输入被重复调用的情况,也是幂等策略的辅助验证手段。

5.2 技能复用和拆分的判断标准

随着技能数量增加,一定会遇到一个纠结:这个功能是并到已有技能里,还是拆成一个新技能?

我们内部有一套判断标准,满足任意两条就建议拆开:

  • 功能使用的业务领域不同(比如“查余额”和“改余额”不该混在一起)。
  • 输入参数重叠度低,合并后 schema 里会塞一堆“此字段仅在 XX 场景生效”的注释。
  • 副作用差异大,一个只读一个写库,或者其中某个会发外部通知。
  • 失败模式不同,其中一个失败后需要人工介入,另一个可以自动降级。

反过来也有过度拆分的问题。拆分太细,比如把“解析文件”拆成“解析 PDF”“解析 CSV”“解析 Excel”三个技能,模型选择负担会暴增,而且用户的表达稍模糊一点就不知道该调哪个。我们的经验是,把“同类基础能力 + 差异化格式参数”合并成一个技能,把“不同业务语义 + 不同副作用”拆开。

5.3 给团队的工具化建议

最后说点工具层面的体会。Agent 技能系统能不能在团队里真正跑起来,不能只靠文档和口头约定。我们到了后期,把技能管理做成内部团队工具的几个基本能力,这里也分享给你参考:

  • 技能注册与审查:提交新技能时自动校验 schema 格式、幂等字段是否缺失、描述是否写清边界。
  • 一键回归:每次改动后自动跑黄金样本库,生成对比报告,没有人工确认不能合入。
  • 日志检索面板:按 session_id、skill_id、trace_id、状态筛选调用记录,并支持回放某一次完整调用链。
  • 变更告警:技能成功率低于阈值,或输入异常率突增时自动预警。

这些工具能力不需要一上来就很重。最开始,我们用三个脚本加两张表就完成了注册审查和回归报告,后来随着技能数量上涨才逐步补了面板。工具不必一开始完美,但“技能变更必须留痕、必须回归、必须可回滚”这几条纪律,越早定下越好。

最后分享两个我踩过坑之后留下的习惯。

第一个是在技能 description 里写清楚“失败时怎么降级”,而不仅仅是“能做什么”。比如账单导入技能,我会在描述里注明“如果文件编码异常,不要重复尝试解析,直接返回错误码 FILE_ENCODING_UNSUPPORTED,并建议用户导出为 CSV 后重试”。模型拿到这个提示,就不会在同一个错误上反复打转,反而会更快走上正确的处理路径。

第二个是给所有非只读技能默认补上“预估副作用”的声明。即便你在当前流程里用不到这个字段,它将来也会帮你在权限控制、并发锁、人工审核这些环节省下大量沟通成本。Agent 技能系统的复杂度是慢慢长出来的,一开始多留一个钩子,后面好过十倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询