Agent技能工程化:从注册、编排到灰度发布的生产实践
一次不算复杂的任务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 一个技能从想法到落地的生命周期把技能当作工程对象之后我们会给每个技能建立生命周期而不是“写完代码就能上线”需求澄清明确要解决什么任务边界在哪输入输出是什么。接口设计定输入 Schema、输出 Schema、失败码、幂等策略。原型调用让模型在一个沙箱环境里实际调几轮观察描述是否足够让模型选对。评测集建设准备几十条典型样本和失败样本跑回归。灰度上线先影子模式跑再小流量最后全量。持续维护看日志、看误用、看成功率根据真实使用迭代。这套生命周期看起来像是给传统后端接口设计的但它对 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 会话可能持续好几天如果会话中间某个技能做了破坏性升级旧会话继续运行时行为就完全不可控了。我们目前执行三个规则破坏性变更必须升大版本旧版本保持可用至少 30 天。每次调用都把 skill_id version 记录进 trace回放时严格按当时版本执行。技能上线新版本时自动刷新 wait 会话中的引用长会话如果检测到技能版本不匹配先提示用户结束会话或跳过低风险步骤。这套规则听上去很基础但它能挡住很多生产事故。我们曾经有一次因为技能版本未做迁移导致模型在长会话里一直调用旧版解析逻辑恰逢上游接口字段改名产出的数据整整错了一天排查起来非常痛苦。从那之后版本兼容策略就变成了注册中心的红线。3. 技能编排中最容易翻车的三个地方3.1 上下文污染技能A留下的消息把技能B带偏多技能编排的第一个坑是上下文污染。之前我们内部做一个“发票 OCR 自动翻译”的流程OCR 技能输出了一份结构化的字段清单然后翻译技能接着处理。结果模型在生成翻译时把 OCR 结果里的噪声字符也当成了需要翻译的正文还“自作聪明”地补充了不存在的公司抬头。问题在于我们当时把 OCR 技能的输出直接追加进了主对话上下文没有做隔离。翻译技能不仅看到了结构化数据还看到了原始坐标、置信度、识别候选词。它没办法区分哪些是任务数据、哪些是中间过程数据于是全都当成依据。解决方案是在技能编排层引入“上下文隔离区”每个技能的输入只能来自被显式声明的数据通道技能产生的中间结果只进入技能内部的工作区不自动混入主对话。需要在后续技能中引用的字段必须通过data_ref显式传递。这个约束会让流程定义多写几行代码但能避免掉 80% 以上的误导情况。3.2 竞态冲突两个技能同时在写同一份状态竞态冲突在单 Agent 流程里不常见但只要并发任务一上问题立刻暴露。我们有一个“日程管理”流程日历技能和提醒技能是两个独立技能它们都会更新用户的状态文件。有一次并发运行了两个任务一个改了日历事件的标题另一个读取旧标题去设提醒时间。因为两个技能分别读写同一份文件后写的覆盖了先写的最终日历标题变成了旧值提醒时间也错位了。排查下来发现我们没有对共享状态做并发控制。后来我们定了一个规则凡是声明了相同side_effects目标的技能编排层必须给它们分配同一个“状态锁”锁粒度是技能级而不是会话级。同时对共享资源采用乐观锁写入前比对版本号版本不一致就抛冲突错误让模型决定是重试还是询问用户。这个改动之后类似的覆盖问题再没出现过。建议你在设计技能时就明确每个技能写哪些资源注册表里把这些资源 tag 统一标准化——这不是形式主义而是并发控制的基础信息。3.3 幂等失效重试惹出的大麻烦模型在调用技能时经常因为网络超时、上游返回慢等原因重试。如果你的技能不是幂等的重试一次可能在业务侧造成双倍副作用。最常见的就是“发送通知”类技能。某次外部接口实际已经发起了短信发送但响应超时Agent 判定失败后重试了一次结果用户收到了两条一模一样的通知。到最后这个技能上线了一套强制幂等策略每次调用模型侧生成一个request_idUUID。技能执行前先查处理结果表如果request_id已存在直接返回上次结果不再执行真实操作。技能执行后把结果写入处理结果表事务与业务操作保持同批次提交。幂等设计应该成为所有非查询类技能的默认要求。尤其是那些写库、发消息、生成订单、更新状态类的操作宁可多写这几行保障代码也不要去赌模型“只重试一次”的侥幸心理。在为技能配置幂等策略时我建议技能类型幂等策略示例查询类无需幂等查库存写库类业务唯一键 去重表提交报销单发送类幂等键 重复检查后再发送通知用户创建资源类客户端生成幂等键冲突返回旧结果创建任务4. 评测与灰度怎么证明你的技能没把 Agent 变蠢4.1 技能评测集的构造原则技能系统上了生产之后大家最关心的问题通常是“这次技能升级到底有没有让效果变好还是变坏了”。很多团队凭感觉判断跑几个例子觉得“看起来差不多”就发布了。这样很容易把隐性问题带回线上。我的经验是每个核心技能至少准备三类样本典型正例正常情况下最常被触发的任务保证基本功能不回归。边界样本参数临界、输入格式不规范、时间敏感等场景比如账单金额超过 999999 会不会精度丢失。对抗样本模型容易误用的场景比如某技能描述里明确说了“不支持个人报销”但用户硬塞个人发票测试模型是否会错误调用并继续执行。这三个维度加起来一个核心技能我建议至少配 60-100 条用例。不是每一条都要人工标注输出结果但技能输出必须可以被自动判定比如结构化字段比对、写入结果的正确性检查。4.2 回归测试与“黄金样本”我们内部训练了一个“黄金样本库”的概念其实就是一组历史真实任务及其最优执行轨迹。每次技能改动之后我们会用同一批任务重新跑一遍完整的 Agent 流程然后从三个维度做对比任务完成率最终目标有没有达成。路径偏差度和黄金轨迹相比步骤顺序和工具选择是否出现异常分支。资源消耗往返调用次数、Token 开销、外部 API 调用频率。如果任务完成率不变但路径偏差明显就值得警觉。因为这说明技能变更可能让模型走了更绕的路。短时间看不出问题但积累下去会变成诡异的行为漂移。还有一个容易被忽视的点修改技能描述跟修改代码逻辑一样需要回归测试。很多人以为“只是改几个字不影响功能”结果模型因为描述里新增了某个关键词开始频繁误触发该技能其他技能的使用率反而骤降。4.3 灰度发布的三段式技能灰度不同于普通后端接口的灰度因为 Agent 行为是非确定性的你不能只验证“接口响应是否正常”还必须验证“模型是否在合适的时机使用了它并且没有因此打乱其他步骤”。我们目前用的是三段式影子模式技能只记录不真正执行。在线流量到达时路由模块照常做技能选择但选中的结果只写日志线上 Agent 依然走旧逻辑。这个阶段可以收集“如果新技能上线会被哪些请求触发”从而判断触发频率和场景分布。小流量模式选 5%-10% 的新会话允许它们真正调用新技能。观察成功率、副作用是否异常、以及是否出现了旧技能未出现过的新错误类型。全量发布等小流量阶段数据稳定后再逐步放开到全量保留快速回滚开关。灰度期间我会重点关注两个指标一是技能报错率二是“模型主动放弃该技能”的频率。如果模型在灰度中频繁选择了新技能又中途放弃通常不是模型问题而是技能的输入输出设计和实际场景不匹配。在这套流程跑顺之后我们内部的技能发布周期从“谁改完谁发”变成了有纪律的“按批次发布”生产事故率明显下降而迭代效率反而提升了因为少花了很多时间在线上救火和排查诡异的模型行为上。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 技能系统的复杂度是慢慢长出来的一开始多留一个钩子后面好过十倍。