如果你维护过线上大模型应用大概率遇到过这种尴尬离线 demo 看起来很好灰度一放量真实用户立刻把系统打出各种边界条件。同一个“帮我总结一下”请求测试集里是 800 字文章线上可能是 6 万字会议纪要同一个客服问答测试集里是标准问题线上可能夹着截图 OCR、错别字、方言缩写和情绪化追问同一个 Agent 工具调用测试集里只需要查一次库线上可能连续调用 7 个工具还会被用户中途改目标。传统软件上线前我们有单元测试、集成测试、压测、预发验证LLM 应用上线前很多团队只有一组手写 eval case。问题是手写 case 往往覆盖的是“我们想得到的风险”真实流量覆盖的是“用户真的会怎么用”。这两者差距很大。这篇文章聊一个越来越实用的工程模式LLM 影子流量回放。它不是简单把线上请求复制给新模型而是一套围绕采样、脱敏、上下文冻结、候选版本回放、语义评测、成本预算和发布闸门的工程系统。我的结论先放前面影子流量回放不替代离线 eval但能补上离线 eval 最缺的“真实分布”。它最适合验证模型替换、Prompt 改版、RAG 策略变更、工具调用策略变更和安全策略升级。LLM 回放的核心难点不是“怎么重放 HTTP 请求”而是“怎么让一次重放具备可比较性、可解释性和可回滚性”。如果没有脱敏、采样、成本上限和发布闸门影子流量很容易从质量工程变成新的线上风险。1. 为什么普通 eval 不够用LLM 应用的失败通常不是单点失败而是组合失败。一个 RAG 客服机器人看起来只是“回答错了”实际链路可能是用户问题被错误改写检索召回了相似但过期的文档rerank 把真正答案排到后面Prompt 中的约束没有被遵守输出解析器又把“无法确定”处理成了正常答案。如果只看最终回答你会觉得模型不行如果只测模型你会错过检索链路如果只测检索你又看不到模型在真实上下文下的行为。Braintrust 的 LLM evaluation 指南把评测拆成离线评测、在线评测、组件级评测、端到端评测。这个划分很有用离线评测像传统测试套件在线评测像生产监控组件级评测定位问题端到端评测判断用户目标是否达成。但在生产系统里还有一个常被忽略的阶段候选版本已经准备上线但还不应该被用户看见。这时你需要一种 0% 用户可见的验证方式。影子流量回放就是放在这个位置。2. 影子流量回放到底是什么在传统后端系统里流量录制回放常用于回归测试录制生产真实请求和依赖响应在测试环境里重放比较接口返回值和中间链路。得物技术的流量回放实践里就提到这类系统的核心价值是把生产真实数据转化成可复用、可执行的回放流量用来验证接口返回和链路行为。迁移到 LLM 应用后影子流量回放可以定义成从生产请求中按策略采样经过脱敏与上下文冻结后在用户不可见的环境中重放到候选模型、候选 Prompt 或候选链路并用自动评测与人工抽检比较候选版本和线上版本的质量、延迟、成本与安全表现。它有三个关键点用户不可见线上用户仍然只看到当前稳定版本的结果。真实输入分布请求来自生产而不是团队手写的理想 case。可比较同一批请求要能被线上版本和候选版本同时评估产出可解释 diff。不要把它和 A/B 测试混在一起。A/B 测试会让部分用户真实看到候选结果能测用户满意度、转化率、追问率影子流量不影响用户适合上线前兜底。我的推荐顺序是离线 eval → 历史流量回放 → 线上影子流量 → 小比例 A/B → 灰度放量。3. LLM 回放和普通接口回放有什么不同普通接口回放关心的是字段 diff、状态码、异常、性能。LLM 回放当然也关心这些但更麻烦的是下面 6 件事。3.1 输出不是字节级稳定的两个等价回答可能完全不同。比如“可以退款但需要订单未发货”和“订单未发货时支持退款”语义一致字符串 diff 却很大。因此 LLM 回放不能只做 exact match需要结构化断言、语义相似度、规则检查和裁判模型组合。3.2 上下文会漂移RAG 系统今天检索到的文档明天可能已经更新。工具调用今天查到的库存明天也会变。因此回放时必须冻结上下文检索结果、工具响应、用户画像、实验参数、系统 Prompt 版本都要进入 replay bundle。3.3 成本是测试预算的一部分普通接口回放多跑几万条成本主要是机器资源LLM 回放多跑几万条可能直接产生模型调用费用。没有采样和预算闸门回放系统会成为新的成本黑洞。3.4 隐私风险更高用户输入可能包含姓名、手机号、订单号、合同内容、病历、内部代码。影子链路如果把这些数据发给候选模型或第三方评测服务风险会被放大。所以生产流量进入回放系统前必须脱敏、分级和审计。3.5 裁判也会犯错用另一个模型当 judge 很方便但 judge 本身也有偏差。正确做法不是迷信一个综合分而是把评测拆成多个维度事实一致性、指令遵循、格式合法性、安全合规、拒答是否合理、成本和延迟。3.6 失败要能定位到组件候选版本得分下降时你需要知道是检索差了、Prompt 差了、模型差了、工具策略差了还是输出解析器变了。否则回放只能告诉你“别上线”无法告诉你“为什么别上线”。4. 一套可落地的架构我建议把 LLM 影子流量回放拆成 7 个模块模块职责常见坑Traffic Sampler从生产请求采样按场景、用户层级、风险标签分层只随机采样会漏掉低频高风险场景Redactor脱敏 PII、密钥、合同号、内部代码只做正则不够要结合字段语义Context Freezer冻结检索结果、工具响应、Prompt 版本、参数没冻结上下文会导致 diff 不可解释Replay Runner控制并发、预算、重试把请求打到候选链路直接复制线上 QPS 会把候选服务打挂Evaluator规则 语义 judge 人工抽检一个总分无法解释质量下降Diff Explorer展示线上版本与候选版本的差异没有 case drill-down研发无法修Release Gate根据质量、成本、延迟、安全阈值决定是否放量没闸门就会变成“看个报表继续上线”一条请求进入 replay bundle 后至少应该包含这些字段{request_id:req_20260608_001,scenario:refund_policy_qa,risk_tags:[money,policy],user_input_redacted:我的订单 [ORDER_ID] 还没发货可以退款吗,prod_trace:{prompt_version:support-v18,model:prod-model-a,retrieved_docs:[doc_refund_policy_v12],tool_results:[{name:order_status,result:not_shipped}],output:订单未发货时支持退款你可以在订单页申请。},candidate:{prompt_version:support-v19,model:candidate-model-b,temperature:0.2}}注意这里保存的是脱敏后的输入和引用 ID不应该把原始敏感内容随意落盘。对高风险字段可以只保存 hash、标签或加密后的值由受控环境临时解密。5. 一个最小可运行的回放评测器下面是一个简化版 Node.js 示例。它不调用真实模型而是模拟线上版本和候选版本输出重点展示 replay runner 和 evaluator 的结构。// replay-eval.mjsconstcases[{id:case-1,scenario:refund_policy_qa,riskTags:[money,policy],input:我的订单还没发货可以退款吗,expectedFacts:[未发货支持退款,订单页申请],prodOutput:订单未发货时支持退款你可以在订单页申请。,candidateOutput:一般可以退款建议联系客服处理。},{id:case-2,scenario:invoice_qa,riskTags:[finance],input:电子发票多久能开,expectedFacts:[支付后,24小时内],prodOutput:支付完成后通常 24 小时内可开具电子发票。,candidateOutput:支付完成后通常 24 小时内可开具电子发票。}];functionfactScore(output,expectedFacts){consthitexpectedFacts.filter(foutput.includes(f)).length;returnhit/expectedFacts.length;}functionriskPenalty(output,riskTags){if(riskTags.includes(money)/一般|可能|联系客服/.test(output))return0.25;return0;}functionevaluate(row){constprodfactScore(row.prodOutput,row.expectedFacts)-riskPenalty(row.prodOutput,row.riskTags);constcandfactScore(row.candidateOutput,row.expectedFacts)-riskPenalty(row.candidateOutput,row.riskTags);return{id:row.id,scenario:row.scenario,prodScore:Number(prod.toFixed(2)),candidateScore:Number(cand.toFixed(2)),delta:Number((cand-prod).toFixed(2)),gate:candprod-0.05?pass:block};}constreportcases.map(evaluate);console.table(report);constblockedreport.filter(rr.gateblock);if(blocked.length){console.error(release blocked:${blocked.length}regression case(s));process.exitCode1;}在本地跑这个脚本会得到类似结果case-1 refund_policy_qa prod1.00 candidate-0.25 delta-1.25 gateblock case-2 invoice_qa prod1.00 candidate1.00 delta0.00 gatepass这个 toy evaluator 很粗糙但它体现了一个重要原则评测器应该尽量可解释而不是只给一个漂亮分数。当 case-1 被阻断时研发能看到原因候选回答少了“未发货支持退款”和“订单页申请”两个关键事实还在资金相关问题上用了含糊措辞。真实系统里可以把 evaluator 拆成四层硬规则JSON schema、必填字段、禁用词、安全拒答、引用是否存在。业务断言退款政策、发票时效、风控边界、工具调用前置条件。语义评测答案是否覆盖关键事实是否与证据矛盾。人工抽检对高风险场景、低置信度 case、分歧 case 做人工复核。6. 采样策略不要只随机抽样很多团队第一次做影子流量最容易写出这种逻辑从生产日志里随机抽 1%。这当然比没有强但远远不够。LLM 应用的风险分布通常是长尾的高频问题未必高风险低频问题反而可能涉及资金、合规、医疗、法律、隐私、删除数据等敏感场景。更合理的采样策略是“分层 配额”sampling_policy:default_rate:0.5%strata:-name:high_risk_moneymatch:risk_tags contains moneyrate:20%max_per_day:2000-name:tool_callingmatch:trace.tool_calls_count0rate:5%max_per_day:3000-name:long_contextmatch:input_tokens12000rate:10%max_per_day:1000-name:negative_feedbackmatch:user_feedback in[thumb_down,complaint]rate:50%max_per_day:1000这套策略的目标不是还原整体流量分布而是让回放数据覆盖上线风险。上线决策可以同时看两类指标按真实流量加权的整体指标以及高风险分层的局部指标。7. 脱敏别把影子系统做成数据泄漏系统影子流量回放最容易被低估的是数据治理。因为它看起来只是“内部测试”实际却复制了生产输入、上下文和模型输出。我建议至少做三件事字段级脱敏手机号、邮箱、身份证、订单号、地址、银行卡、密钥、cookie、内部 token 必须处理。语义级脱敏用户可能在自然语言里写“我叫张三电话是……”。这类不能只依赖字段名需要正则 NER 大模型辅助标注组合。分级回放低敏 case 可以进入通用候选模型高敏 case 只能在受控环境回放极高敏 case 只保留统计指标不进入影子链路。脱敏不是越狠越好。把所有实体都替换成[MASK]后模型行为可能失真。更好的方式是类型保持手机号换成另一个合法格式手机号金额换成同量级金额城市换成同级城市。这样既降低风险又保留输入结构。8. 评测指标上线闸门应该看什么一个可执行的 release gate 至少要覆盖 5 类指标。指标示例阈值阻断原因质量高风险场景胜率不低于线上版本 98%候选版本在关键任务退化安全严重安全违规 case 0不能用平均分掩盖红线问题格式结构化输出解析成功率 ≥ 99.5%下游系统依赖格式稳定成本单请求平均成本上涨 ≤ 20%防止模型替换导致预算失控延迟P95 延迟上涨 ≤ 15%用户体验不能明显变差这里要特别强调不要只看平均分。如果候选模型在闲聊场景提升 10%但在退款、删除账号、合同解释等高风险场景退化 2%我会阻止上线。一个更实用的闸门写法是release_gate:block_if:-safety.critical_violations0-high_risk.win_rate 0.98-schema.parse_success_rate 0.995-cost.avg_per_requestbaseline.cost.avg_per_request * 1.2-latency.p95baseline.latency.p95 * 1.15require_manual_review:-judge_disagreement_rate0.15-unknown_intent_ratebaseline.unknown_intent_rate * 1.3这样的 gate 比“综合分 85 分以上可以上线”更像工程系统。9. 从影子回放到灰度发布影子流量通过不代表可以全量发布。它只说明候选版本在用户不可见的真实输入上没有明显退化。下一步还需要小比例 A/B 或灰度因为有些信号只有用户看到结果后才会出现用户是否追问、是否复制答案、是否点赞、是否投诉、是否完成任务。我的发布节奏通常是历史回放过去 7 天或 30 天采样数据验证候选版本不明显退化。线上影子复制当前实时流量观察 24-72 小时覆盖工作日和高峰期。1% 灰度只给低风险用户或内部账号可见实时监控质量和反馈。5%-20% 放量开始看用户行为指标继续保留自动回滚。全量保留影子对照一段时间用于发现长尾退化。这套流程看起来慢但对高风险 LLM 应用来说它比“周五下午直接切模型”便宜太多。10. 常见失败模式最后列几个我见过的坑。坑 1只保存输入不保存上下文。结果候选版本看起来退化其实是检索文档变了。解决保存 doc id、版本、片段、工具响应和 Prompt 版本。坑 2只用模型 judge不做业务断言。judge 觉得回答自然流畅但业务规则错了。解决高风险业务规则必须写成硬断言。坑 3采样没有风险分层。随机样本里 80% 是低风险闲聊结论很好看上线后资金场景翻车。解决分层采样和分层报表。坑 4没有成本保护。候选链路多了一次 rerank 和一次 judge回放 10 万条后账单爆炸。解决预算上限、并发控制、分阶段扩大样本。坑 5diff 不可读。报表只有分数没有失败 case、证据和 trace。解决每个失败 case 必须能 drill down 到输入、上下文、候选输出、断言失败原因。结语LLM 应用的上线质量不应该只靠“我感觉这个 Prompt 更好”。手写 eval 能覆盖已知风险线上监控能发现已发生的问题而影子流量回放填补的是中间层在用户看见候选版本之前用真实请求分布提前暴露退化。如果你的团队已经开始频繁替换模型、调整 Prompt、升级 RAG 或改 Agent 工具策略我建议尽早把影子回放纳入发布流程。先不用做得很复杂从 1000 条脱敏历史请求、10 个高风险场景、5 个硬规则断言开始就能比“凭感觉上线”稳很多。真正成熟的 AI Engineering不是把模型接进产品就结束而是让每一次模型和 Prompt 的变化都能被记录、回放、比较和阻断。参考资料Evaluation-Driven Development and Operations of LLM Agentshttps://arxiv.org/html/2411.13768v3How to Roll Out New LLMs Safely Using Shadow Testinghttps://www.codeant.ai/blogs/llm-shadow-traffic-ab-testingWhat is LLM evaluation? A practical guide to evals, metrics, and regression testinghttps://www.braintrust.dev/articles/llm-evaluation-guide订单流量录制与回放探索实践 - 得物技术https://tech.dewu.com/article?id22