资讯动态

AI Agent灰度发布实战:A/B测试与流量染色

发布时间:2026/10/21 13:48:38 来源:尧图企业网站定制
做 AI Agent 工程的人迟早会撞上同一个麻烦一个新版本到底能不能上我今年大半时间都在打磨一套 Agent Harness最头疼的不是 agent 逻辑本身而是怎么给这玩意做灰度发布。AI Agent Harness Engineering 的灰度发布策略说穿了就是两张牌A/B 测试和流量染色。这两张牌帮我拦下了至少五次差点酿成事故的变更也让我摸清了 Agent 发布和普通后端服务发布的本质区别。这篇文章把里面能直接用的方法、参数和坑都摊开讲你照着搭一遍至少不会在灰度第一步就翻车。这篇文章适合两类人看一类是正在做 Agent 基础设施、被新版本不敢直接上困扰的开发者另一类是已经在用传统灰度工具但发现它们对 LLM 场景不太管用的团队。我会从为什么需要灰度、灰度的锚点放在哪、A/B 怎么测、流量怎么染色、回滚怎么做这五块展开尽量把每一个决策背后的原因也讲清楚。1. 先聊聊为什么 Agent 的发布值得单独搞一套灰度体系1.1 传统服务灰度的是确定性Agent 灰度的是飘忽不定传统后端服务做灰度本质上是给一段确定性的代码切流量。代码在给定输入下的输出基本可预期你灰度的是部署风险、配置风险、依赖风险。但 Agent 不一样Agent 的行为由模型权重 prompt 模板 工具配置 记忆上下文 护栏规则共同决定任何一个环节动了输出分布都会变而且这种变化没法提前用单元测试穷举。我举一个真实场景。有个天气查询 Agent工具列表里有查天气和查空气质量两个工具。某次升级只是调整了工具描述的顺序把查空气质量放到了前面。结果模型在用户问明天适合跑步吗时不再走先查天气再查空气质量的链路而是只查了一个工具就开始推理。行为本身没有报错任务结果看起来也像模像样但信息完整度明显下降。这种看起来没坏、实际上偏了的问题只有在真实流量分布里才暴露得出来。另一个麻烦在于 LLM 的非确定性。传统服务 A/B 跑出来的指标差异主要来自代码路径不同Agent A/B 跑出来的差异还会叠加采样随机性、温度参数、上下文长度变化。同一份 prompt同一批输入跑两次可能给出不同工具调用序列。所以灰度期间你要对比的不是一个稳定值而是两个分布。这也是为什么很多团队在 Agent 灰度上特别容易误判看到一点波动就以为是版本问题或者反过来看到指标平稳就放心了结果一全量就出事。1.2 传统灰度发布与 Agent 灰度发布的核心差异我整理过一个对照表每次给新同学讲 Agent 灰度时都会直接甩出来对比维度传统服务灰度Agent 灰度变更单元代码、配置、依赖prompt、模型版本、工具描述、记忆策略、护栏规则行为可预测性高给定输入基本有确定输出低真实输入分布下输出呈长尾主要监控指标错误率、延迟、CPU、内存成功率、工具调用质量、护栏触发率、成本、用户干预率评估方式单元测试、集成测试、性能压测离线评测集 LLM as judge 在线 A/B回滚代价通常较低重启/回包即可高涉及会话状态、记忆兼容、工具副作用灰度最小单位请求、实例、机房会话、用户且需要稳定的分流 key这张表看着简单实际每一条背后都是坑。比如回滚代价高这条传统服务回滚就是切回旧版本但 Agent 长会话里用户和模型已经来回聊了好几轮上下文里的记忆结构是用新版本格式写的你强行把运行版本切回旧版旧版可能根本解析不了新版写入的记忆片段照样出错。所以 Agent 灰度不能只考虑切换还要考虑切换后老会话怎么处理。2. 灰度放在哪里做Harness 层才是真正的锚点2.1 我理解的 Harness 是什么很多做 LLM 应用的人第一次听到 Agent Harness 会有点懵这个词在不同团队里叫法也不一样有的叫 Agent 运行时有的叫编排层有的叫控制套件。我倾向于把它理解成包在模型外面的一层工程化控制面。它负责上下文组装、工具调度、护栏执行、记忆读写、追踪埋点、版本路由这些脏活累活如果不抽出来最后都会以 if-else 的形式散落在业务代码里。Harness 层的位置大致是业务请求先打到 Harness它决定用哪一套 Agent 配置prompt、模型、工具、参数然后执行这个配置生成工具调用序列最后把结果返回给业务。模型本身只是 harness 里被调用的一个组件。这个分层最大的好处是版本变更的承载点集中在 Harness 的配置体系里而不是散落在模型层或者业务层。做灰度发布时Harness 层是天然的锚点。你可以在不改业务代码的前提下给一套 Agent 配置定义版本号、部署组、灰度比例甚至同一套配置内部还可以做 prompt 级的多变量实验。模型层做不到这一点因为模型层只负责出 token不知道上下文是什么、工具怎么接、护栏要不要触发业务层也做不到因为业务代码一旦和具体的 prompt 结构耦合你就没法独立发布 Agent 逻辑。2.2 分流点应该长在 Harness 的哪个位置我踩过一次坑一开始把分流逻辑直接写在业务网关里根据用户 ID 哈希决定走新 Agent 还是旧 Agent。当时看起来没问题后来业务方想在某个渠道单独跑实验发现改分流规则要从业务网关改起代码上线要先走业务发布的流程完全不灵活。后来我把分流收敛到 Harness 的请求入口所有 Agent 流量都必须经过同一个 variant resolver灰度能力才算真正长对地方。具体的流程是这样请求进入 Harness 后先在 variant resolver 里读流量上下文租户、会话 ID、来源渠道、染色标记接着根据灰度配置决定本次会话应该走到哪个 agent 变体。这里的 agent 变体不是指某个模型实例而是一整套配置快照包括 prompt 模板、模型路由规则、工具启用列表、温度参数、护栏等级。配置快照的管理可以像发布二进制一样做版本化和回滚prompt 也是代码配置也是代码。我常年在 Harness 里维护一份类似下面的变体配置variants: stable: prompt_version: prompt_agent_v2_0 model: provider: internal-llm model_name: agent-v2 temperature: 0.3 tools: [search_tool, weather_tool, calc_tool] guardrails: standard memory_schema: schema_v2 canary: prompt_version: prompt_agent_v2_1 model: provider: internal-llm model_name: agent-v2 temperature: 0.2 tools: [search_tool, weather_tool, calc_tool, news_tool] guardrails: strict memory_schema: schema_v3 traffic: canary_percent: 5这套配置里stable 和 canary 的模型一样但 prompt、工具、护栏、记忆 schema 都不同这正是 Agent 灰度最常见的形态一次变更往往同时动了好几个变量。如果不用这种配置快照管理你会很难定位线上某个问题到底是 prompt 引起的还是工具列表引起的还是模型行为漂移引起的。2.3 确定性分流同一个会话不能来回切灰度切流最忌讳的就是抖动。用户在一次会话里第一轮走旧版第二轮突然被切到新版体验和评估都会乱。所以分流 key 的选择非常关键我不用 IP、不用设备 ID而是用租户 ID 会话 ID 拼出来的稳定字符串做哈希。分流逻辑我用 Python 写过一版讲意思想表达清楚就够了import hashlib def select_variant(context, canary_percent5): key f{context.tenant_id}:{context.session_id} digest int(hashlib.sha256(key.encode()).hexdigest(), 16) bucket digest % 1000 if bucket canary_percent * 10: return canary return stable这里把比例放大到千分位是想支持 0.1%、0.5% 这种极低流量。早期我只用百分位最小只能切 1%对一些高成本 Agent 场景来说 1% 的流量已经足够造成几千块的额外成本所以千分位更实用。还有一点这个 hash 要基于一个在整个会话生命周期内不变的 key一旦用户开了会话不管灰度比例怎么调整他在本次会话里始终走同一个变体。3. A/B 测试落地分组、指标与评估闭环3.1 分组粒度以会话为最小单位A/B 测试的分组粒度决定了实验时长和结论可信度。Agent 场景下我强烈建议以会话session为最小分析单元而不是以请求request为单元。原因很简单一次用户请求可能引发模型多次内部推理和工具调用如果按 request 计数一个会话里出了问题会被重复计数最终算出来的显著性完全是虚胖。以用户为粒度会更干净但实验速度会慢很多。比如一个用户一天可能只产生一到两个会话你至少要积累几千个用户才能达到显著水平。以租户为粒度更慢一般只在大型企业客户灰度时才用。我的实践中toB 场景按租户隔离、toC 场景按用户隔离、内部和产品验证场景按会话隔离三者的优先级排开没有放之四海皆准的方案。分组时还要注意同一租户的多个用户可能共享一个环境。之前我遇到过一个情况某企业客户的所有员工共用一套账号体系会话 ID 才是真正唯一的。如果按租户 ID 做 hash 分流这个租户会整个被分到一个桶里A/B 就没法在租户内部做了。所以设计分流 key 时要去看目标系统里真正的隔离边界是什么而不是想当然。3.2 核心指标任务成功率之外还要看什么Agent A/B 的指标设计比传统服务复杂因为成功本身是模糊的。一个客服 Agent 帮用户查到了订单但语气冷淡、流程绕了三个来回算成功还是失败我通常会搭一套指标分层指标层具体指标用途业务层任务完成率、用户最终反馈、工单转化率判断新版本是否带来真实业务价值行为层工具调用次数、平均推理步数、用户中断率、重问次数判断 Agent 行为是否更高效质量层护栏触发率、有害输出率、关键字段错误率判断新版本是否引入了安全/合规风险成本层总 token 消耗、单会话成本、外部 API 调用量判断新版本是否值得用额外成本换效果这四个层缺一不可。我见过不少团队只看任务完成率和 token 成本结果新版本确实便宜了但护栏触发率翻了三倍问题在实验报告里被淹没了。反过来有些团队只盯质量指标忽略了成本上了新版本之后单会话成本直接翻倍业务上根本扛不住。另外LLM 场景里延迟的度量也要重新定义。传统服务的延迟是单次请求的响应时间Agent 的延迟是用户感受到的整个交互时间包括模型多轮推理、工具调用往返。灰度期间我会分别统计两个指标单次模型调用的 TTFT 和整个会话的完成时长。新版如果只是 TTFT 变快但推理步数变多用户感受反而更差。3.3 离线评估与在线 A/B 的衔接灰度发布不能只靠在线 A/B也不能只靠离线评测两者必须形成先后链路。我先在离线评测集上把新版本跑一遍通过后才允许进入灰度流量。离线评测集要有代表性不能只放几个 happy path 样例要加入失败样例、长尾 query、多轮对话场景。这一关能拦掉大部分明显回归。但离线评估有个陷阱评测集是静态的模型和 prompt 很容易被背下来。我见过一个团队连续几个版本都在同一套评测集上刷高分结果上线后真实用户表现一次比一次差。原因是评测集太小、太固定模型其实已经过拟合到了评测集里的表达模式。所以我每年会花不少精力做评测集定期扩充灰度期间还会从真实流量里抽样回流到评测集保证它跟得上线上变化。在线 A/B 和离线评估之间还要有一个影子评估过渡带。具体做法是新版本完全不接管流量而是把真实请求同时发给新版 Agent但新版的结果只进日志、不触达用户。影子模式可以跑一两天把新版在真实输入上的输出去离线评一遍看看和稳定版有多少偏离。这一步能提前发现很大的问题而且零用户风险。3.4 最少需要多少样本先算清楚再放量很多团队灰度到一半发现看不出差异本质上是样本量不够。Agent 场景比传统 A/B 更吃样本因为会话级分析单元天然比请求级少很多。我会在灰度前先做一轮粗略的功效计算公式不复杂[ n \approx \frac{16 \times p \times (1-p)}{(\delta)^2} ]其中 p 是基线指标比如任务成功率 0.7δ 是你期望检测到的最小提升比如 0.05。算下来 n 大概 2688 个会话。也就是说如果你要看到 5 个百分点的成功率提升每组至少要积累接近 2700 个有效会话而不是 2700 次请求。实际做实验时还要预留余量因为日志缺失、无效会话、护栏拦截会把一部分样本剔掉。我一般按 1.3 倍来定目标也就是每组要攒到 3500 个干净会话。灰度比例 10% 的话如果每天能产生一万个会话两组大约需要三天如果灰度比例只有 1%就要等上一个月。所以实验设计阶段就要把比例和时间窗一起规划好不然很容易出现数据攒够了但业务已经等不起的尴尬。4. 流量染色让每一笔 Agent 调用都有可追踪的身份4.1 染色标记本身是怎么打的流量染色这个词听起来玄乎本质就是给流经 Harness 的请求打上一个肉眼可查、系统可读的标记让全链路日志、指标、追踪都能按这个标记聚合。我习惯把染色标记直接放在请求头和内部上下文的 attribute 里用x-agent-stain这个头来承载取值类似gcanary,v2.4.0-rc1,ta。g 表示实验组v 表示版本快照t 表示染色来源。染色来源通常有三种。第一种是业务方主动指定比如内部测试团队、某个重点客户要求全量进入灰度环境直接在请求头携带标记第二种是 Harness 的 variant resolver 根据灰度比例自动给会话打标这个标记和分流逻辑共用同一个哈希结果第三种是实验平台侧下发常见做法是通过配置中心给满足一定条件的流量统一染色。三种来源最终都会归一到同一套标记规范否则后面看板聚合时会疯掉。标记要放在两个地方请求上下文和日志链路。请求上下文里的标记用于 Harness 内部做决策时读取日志和追踪里的标记用于离线分析和监控。我踩过的一个坑是只在上层请求日志里打了标记Agent 内部的工具调用日志没有继承结果某次排查新版 Agent 为什么频繁调用某外部接口时根本筛不出是哪笔流量调的。后来我把染色标记作为公共 attribute 注入到 Harness 生成的所有 span 和日志 entry 里才算把这问题根治。4.2 染色标记与全链路追踪的配合Agent 的一次请求在 Harness 内部会拆成很多段入口解析、prompt 组装、模型调用、工具调用、护栏判定、记忆读写。每一段都应该有自己的 span同时这些 span 共享同一个 trace_id也要共享同一个染色标记。这样才能回答一类关键问题新版 Agent 在调用天气工具时上下文长度是否暴涨具体做法是在创建根 span 时就从请求里解析染色标记然后作为 span attribute 写进去。下游所有子 span 通过 context propagation 自动继承。查问题时直接在追踪系统里过滤stain.group canary再叠加span.name tool_call就能看到新版 Agent 每笔工具调用的耗时、参数、返回结果。没有这层染色你只能把全量日志拉下来慢慢翻效率不是一个量级。我在日志字段里会约定一组固定的键stain_group、stain_version、stain_source、trace_id、session_id。前三个来自染色标记后两个来自链路追踪。查询时优先组合 session_id 和 trace_id 定位到单次会话再用 stain_group 做聚合对比。这套键位设计不复杂但非常实用几乎覆盖了 Agent 灰度期间所有的问题排查场景。4.3 染色数据的清洗与隔离防止实验污染流量染色不只是给观测用的它还是数据隔离的工具。Agent 灰度期间最容易出的一种事故是实验组的行为数据写进了共享的长期记忆服务然后对照组也读到了这些记忆结果两组的表现互相污染A/B 结论完全失真。我遇到过一次实验组新版 Agent 给某企业客户写入了一条错误的业务标签对照组后续会话读到后也跟着出错。最后查了整整一天才定位到是记忆写入没有带隔离键。所以我的原则是只要数据会回流到 Agent 的上下文记忆、缓存、意图识别模型就必须以租户 会话 染色组为键做隔离。比如记忆服务的存储 key 设计成mem:{tenant_id}:{session_id}:{stain_group}这样实验组写入的记忆永远不会被对照组读到对照组也不会污染实验组。另外灰度流量不能被当成普通训练语料。在离线数据清洗时我会把stain_group不等于 stable 的样本单独归档绝不能直接混入下一版模型的微调集合。原因很简单灰度版本本身可能有缺陷把有缺陷的 agent 行为当成标准答案去训练等于给模型喂毒。染色标记在这一环节再次发挥作用数据管道按染色标记分区不合格的实验组样本可以整段剔除。5. 灰度发布的标准流程、回滚细节与现实中的坑5.1 我常用的灰度发布流程Agent 灰度不能一上来就切线上流量我一般会卡五道闸门。第一道闸门是离线评测在静态评测集上跑完回归测试、影子模式跑完真实输入评估结果达标才继续。第二道闸门是内部灰度把比例压在 1% 以内流量来源限定为内部员工和测试租户主要看功能是否可用、链路是否报错。第三道闸门是小流量灰度比例 5% 到 10%开始接真实用户这时候我会重点盯质量层和行为层指标比如护栏触发率、用户中断率。第四道闸门是半量灰度比例 25% 到 50%看业务层指标是否稳定比如任务完成率、单会话成本。如果走到这一步都没问题才进第五道闸门全量。每一道闸门我都要看当天统计的指标是否达到预设标准标准没达到就暂停放量宁可多等一天也不要冲过去再说。有一个原则我在团队里反复强调灰度比例不能只增不减。如果 25% 阶段出现了明显异常按流程要立刻回到 5% 或直接归零而不是反正就差一点了再放放看。Agent 的异常往往是滞后的因为一个会话可能持续好几分钟等到你在监控上看到问题时已经有一大批用户在错误路径上跑完了。5.2 回滚不是关开关而是要处理残留会话传统服务回滚只需要把新版本下线让流量回到旧版本。Agent 场景多了一个麻烦灰度过的新版本可能已经写入了长期记忆、已经触发过外部工具副作用、已经改变了用户会话的上下文结构。我遇到过必须回滚的场景新版本 Agent 在工具调用参数解析上有个 bug导致大量订单查询失败。当时我把分流配置切回 stable新会话立即恢复正常但那些已经用新版本跑了一半的会话重启后上下文里仍带着旧格式的结构化记忆。为了处理这种残留我会在 Harness 里维护一个会话版本映射表记录每个活跃会话当前实际使用的 agent 版本。回滚时不只是把分流比例改成 0还要对流量映射表中处于 canary 的会话做两类操作一类是会话内容无关紧要的直接终结并引导用户重新发起另一类是业务上重要的把会话上下文迁移到兼容的稳定版记忆格式。后者的成本不低但对比让用户在一个错误上下文里继续对话这点成本完全可以接受。自动回滚条件也要提前配好不能等人发现。我一般设两条硬规则一是灰度组任务成功率连续 15 分钟低于稳定组 10% 以上二是灰度组护栏触发率超过稳定组 3 倍。触发后由 Harness 自动把分流比例降到 0同时告警推送给我。这套自动止血机制我是在一次晚上十点的事故里定下的。当时新版本在少数长尾 query 上反复触发违规回复如果等人发现再手动处理影响面会再扩大一倍。5.3 踩过的几个典型坑第一个坑是 LLM judge 的顺位偏置。早期我用同一个大模型给新旧两个版本的输出打分prompt 里固定先展示 canary 的输出后展示 stable 的输出。结果连续两轮实验 canary 的评分都偏低后来发现是 judge 对后出现的文本更挑剔跟版本好坏没关系。修复方案很简单把两个版本的呈现顺序随机打乱或者用双向对比取多数。这个小细节直接影响了灰度版本的取舍判断。第二个坑是实验组记忆污染对照组这个我在上一节讲隔离时提过。它的变体还有缓存污染实验组新版 Agent 把某个问法的答案写进了共享缓存对照组命中缓存后共用结果等于对照组也在隐形地跑新版逻辑。后来我把缓存 key 也加上了染色组标识才彻底切断这条污染路径。第三个坑是外部供应商限流。灰度期间 canary 流量一旦触发外部 API 的限流策略影响的可能不只是 canary 组连稳定组共用的同一供应商配额也会被拖累。我处理的方式是给 canary 流量单独配置一份限流额度同时为外部 API 调用设置更短的超时时间。Novel 的地方在于稳定组和 canary 组需要独立统计配额不能在同一个共享计数器里扣。第四个坑是样本统计的会话长度偏差。新版 Agent 如果比旧版更容易让用户问更多问题那么新版组的会话普遍更长长会话天然更容易出问题最终表现出来的成功率可能被会话长度结构夸大到失真。分析的时候如果不按会话长度分层很容易得出新版成功率低的错误结论。我现在做 Agent A/B 分析时默认都会把会话轮数作为协变量放进去看分层结果。第五个坑是长尾会话的归属问题。灰度期间用户可能开了会话放在那里等 20 分钟后再回来继续聊这时流量比例已经变了。如果按当前版本去归类会话统计口径就会混乱。我的做法是会话一旦创建就固定版本归属无论后续流量比例怎么调整该会话的染色标记和版本归属都不再变化。这跟之前说的同一个会话不能来回切是同一件事只是从统计分析角度又强调了一遍。最后再分享一个小技巧。灰度发布的时候我会额外打印一条结构化日志内容包含会话 ID、染色组、版本号、分流 key 的哈希桶、决策原因。这条日志平时看着多余但一旦出现线上纠纷它是定位这笔流量到底是被哪个决策逻辑分到哪个版本的唯一铁证。流量染色这件事如果你不能证明流量去了哪里那它基本等于没做。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑