LLM 应用功能开关与渐进式发布工程 2026:从细粒度路由到 Kill-Switch 的统一装配
把 LLM 应用的产品能力拆成一棵可逐项打开、关闭、灰度的配置图,以特性-策略-回退三段式治理在生产环境里跑出可控、可回滚、可观测的发布。
约 42 分钟阅读12,492 字2 次阅读博主

把 LLM 应用的产品能力拆成一棵可逐项打开、关闭、灰度的配置图,以特性-策略-回退三段式治理在生产环境里跑出可控、可回滚、可观测的发布。

一句话摘要:把 LLM 应用的产品能力拆成一棵可逐项打开/关闭/灰度的配置图,以"特性→策略→回退"三段式治理在生产环境里跑出可控、可回滚、可观测的发布。
把一个 LLM 应用从 demo 推到生产,最难的不是"再调一次 prompt",也不是"再换一次模型",而是"让两个不同时段的 prompt、两个不同版本的 embedding 模型、两套不同的索引并存于同一套服务里,在每个请求粒度上由配置决定走哪条路径,并在任意一条路径出现事故时把流量切回健康路径,且整套切换在 30 秒内完成、5 分钟内可回滚"。这件事在传统 Web 应用里有 feature flag / canary / blue-green / traffic mirroring 这套基础设施,搬到 LLM 应用上则问题变成多解的:模型权重、prompt 版本、检索索引、工具 schema、护栏规则、引用格式、token 限速、流式输出节奏,每一个维度都有自己的版本曲线,任何一个维度的回退都不能让其它维度的回退跟着失效。
过去两年我做过的几个 RAG/Copilot 产品都踩过同一类事故:某次把 embedding 模型从 bge-m3 切到 text-embedding-3-large,首批用户查询的 recall@1 下降了 22%,但 trace 系统没有第一时间把"模型版本"作为事故维度的字段而是打到 free-form tag 上,on-call 花了 40 分钟才确认"是模型问题"还是"是 prompt 问题",最终不得不临时把 schema 版本号定死成 v2.3 全局回退,导致后面真正想用 v3 schema 的实验也被卡住。另一次是把 prompt 从 few-shot=0 切到 few-shot=3,流式输出的 TTFT 从 280ms 跳到 540ms,但因为没有 per-route 的限速开关,只能走"全量回退 prompt"的路径,把已经在跑的 v2.4 实验一并回退。这类事故的根因不是"没有回退机制",而是回退粒度太粗:发布系统只有"模型/提示/索引整体回退"这一个动作,没有"只回退某条路径的某个维度"的细粒度能力。
LLM 应用特有的发布维度比传统 Web 应用多出至少 6 个:模型权重、prompt 版本、retrieval 配置(chunk 策略、混合召回率、reranker 启用与否)、tool schema 版本、guardrail 规则、流式输出策略(token budget、thinking token 是否展示、partial JSON 是否允许)。这 6 个维度两两之间存在组合爆炸(6 个维度各 2 个版本 = 64 种组合),再加上"用户分群(免费/付费/企业)×(新用户/老用户)×(地域)×(语言)"作为路由键,组合数轻松突破 10⁵。任何一把这样的组合在生产环境都需要同时可观察、可回退、可比对。这就是为什么 LLM 应用需要专门的渐进式发布基础设施,而不是简单复用传统 SaaS 的 feature flag 平台 —— flag 系统的路由键要能把"模型/提示/索引/护栏"当成一阶公民,而不是只承担"开/关"的二值语义。
这篇文章想做的,是把这套渐进式发布基础设施拆成 7 节:配置层、路由层、流量分层、回退机制、可观测性、运营护栏(以企业级安全 / 合规要求为约束的开关策略)、工程化落地(以 Langfuse + 自研 flag 平台 + Argo Rollouts 三件套为例)。它不是"flag 系统教程",而是"LLM 应用为什么要这样组织 flag 系统"的工程论证。
把渐进式发布形式化成三个互不耦合的层,是最容易让工程团队对得齐、严的入口:
发布决策 = 配置层(What) + 路由层(Whom) + 回退层(Fallback)
配置层(What) 定义"每个 flag 在每个状态下应该返回什么值"。LLM 应用的 flag 至少包含 5 个原子类型:
5 个原子类型构成 6 个发布维度(模型/提示/索引/工具/护栏/输出策略)的最小完备集,任何发布动作都可以映射到这 5 个原子的组合上。
路由层(Whom) 定义"哪些请求应该走哪条配置"。路由键至少包含 5 个维度:
5 个路由键的笛卡尔积定义了一棵路由树,每个叶子节点都有自己独立的 flag 集合。
回退层(Fallback) 定义"当配置层或路由层出现事故时如何切回健康路径"。回退至少包含 4 个层次:
三段式结构的核心好处是:配置 / 路由 / 回退三者可以独立审计、独立回滚、独立责任归属。事故响应里,on-call 可以只动"回退"那一层而不动配置,这样 canary 实验不会被全局中断;反过来,如果发现是 prompt 本身出问题(而不是 canary 比例问题),可以只动"配置"那一层把 prompt 回滚而不影响其它维度。这种"按层独立责任"的工程纪律,比"一个 flag 系统把什么都能做"的设计哲学更不容易出现"为了多灵排错引发多遍次扰动"。
LLM 应用的 flag 配置不能简单 KV 化存储,原因是 flag 内部包含 prompt 模板、retrieval 配置、模型权重指针等多类结构化数据,而且这些结构化数据之间还存在引用关系(prompt 模板可能引用 model_id,retrieval 配置可能引用 index_version,guardrail 规则可能引用 pii_pattern_version)。把 flag 配置当作一阶公民,意味着每条 flag 都应该有:
存储选型上有两条主流路径:用 Git 当 flag 存储后端(适合 flag 数 < 200 条的小团队),用专门 KV + CRDT(适合 flag 数 > 1000 条的规模化场景)。前者好处是 flag 变更天然有 PR review + Git blame,坏处是 PR review 不能做"紧急 flag 滚动回滚"这种秒级动作,只能做"30 秒一次"的 git pull。后者好处是支持秒级 update + 缺点是变更 audit trail 要自己做。
工程经验上,我倾向于双源存储:Git 是 source of truth,KV 是 hot cache。日常 flag 读取走 KV,变更走 Git PR + 自动 sync 到 KV。这套模式让我之前的一个项目把"flag 变更 audit"做到了零成本(用 Git blame 看是谁改的 flag,GitHub Actions 跑 policy check,事故回滚用 git revert <flag-commit> 然后 sync 到 KV 即可)。sync 频率可以做到 30 秒一次,这对大多数 LLM 应用已经够用;真有紧急滚动回滚需求时,KV 这一层要支持"in-memory hot patch"作为兜底。
flag 内容变更要做的另一个工程动作是生产环境影子:每次 flag 变更不只是改 KV,而是要把变更同步到"影子 flag 集合",影子集合对真实流量不可见、但对离线评估集可见。这样每次 flag 变更后能立即对离线评估集跑一次回归,看 recall@1 / 答案质量分 / 引用覆盖率有没有降。如果回归失败,flag 变更被自动 rollback,on-call 会收到 P1 alert。这是 LLM 应用特有的发布纪律 —— flag 变更是 prompt 变更,prompt 变更必须经过离线评估集回归,而不能让"先上线再说"的传统 swag 模式继续生效。
传统 SaaS 把"用户分群"当成营销/定价维度,但在 LLM 应用里,用户分群是发布维度:同一个 prompt 版本对免费用户可能太慢、对付费用户可能不够准确,这种"按维度付费"的需求只能靠分群路由来满足。具体来说,路由层要做 4 件事:
第一,路由键规范化:每个 flag 的路由键都要在 schema 层定义清楚。一个常见反模式是路由键直接用 string concatenation({country}_{plan}_{lang}),字符串拼接容易出错(国家枚举值没对齐、lang 用 zh 还是 zh-CN 没统一),正确做法是用 schema 化的 route key({country: ISO3166, plan: enum, lang: BCP47}),让 schema validator 在 flag 提交时就校验路由键是否合法。
第二,路由求值的稳定性:路由求值函数必须是确定性的 —— 给定相同输入必须返回相同输出,而且要"幂等":同一个请求被求值两次必须返回相同 flag 集合。这点对流式输出特别关键:流式 LLM 响应是分 chunk 发出的,如果 chunk 2 的路由结果与 chunk 1 不一致,会出现"前半段用 v2.4 prompt 生成、后半段用 v2.5 prompt 生成"的诡异事故。稳定性做法是:在请求进入时一次性求值所有 flag,缓存在 request context 里,后续 chunk 复用同一份 flag 集合。这是 LLM 应用路由层比传统 SaaS 路由层更严的地方 —— 传统 SaaS 的路由决策通常在请求开始时一次求值,但 LLM 应用的 flag 集合需要在整个流式会话里持续复用。
第三,路由决策的 audit log:每次路由求值都要记录"为什么这个请求走到了 flag X",audit log 至少要包含 (a) 请求 hash、(b) 路由键值、(c) 求值后的 flag 集合、(d) flag 版本号。这份 audit log 在事故排查时是不可替代的:on-call 接到"为什么我这个请求返回的是 v2.5 而不是 v2.4"这类用户反馈时,第一步就是看 audit log 的"路由键值 + flag 版本号"两列,而不是翻 trace。
第四,跨租户的路由隔离:多租户 LLM 应用里,租户 A 的 prompt 不能影响租户 B 的 prompt,租户 A 的 canary 实验也不能让租户 B 的流量"误入"canary。隔离做法是给每个租户维护独立的 flag namespace,namespace 之间不共享 flag;跨 namespace 的 flag 必须显式 import,而且 import 关系要在 dependency graph 里被检索出来(A_pre=dd flag B 的 ddd 版本)。这是 LLM 应用特有的租户级发布纪律,传统 SaaS 通常不需要 —— 因为传统 SaaS 的 flag 通常是全局的,不会按租户分别定义 prompt 版本。
LLM 应用的流量分层比传统 Web 应用多出"shadow"作为影子评估层 —— shadow 流量对真实用户不可见、但用于离线评估集和在线质量回归。三层各有自己的角色:
Canary 流量是把真实用户请求按比例切到 canary 版本的实验流量。canary 的关键纪律是 per-cohort 而不是 per-random:不要用 random 切流量,因为 random 切会让同一个用户在不同时段看到不同版本的标签(用户在 t0 看到 v2.4,在 t1 看到 v2.5,在 t2 又看到 v2.4),这种"用户体感跳变"对长会话型 LLM 应用尤其致命(copilot/agent 应用里用户中途换 prompt 版本会让对话上下文断裂)。正确做法是按 cohort 切:用 user_id 哈希到 [0, 100],落在 [0, canary_percent] 区间内的用户走 canary,其它用户走 stable。这样同一个用户在整个会话期间看到的 prompt 版本是稳定的。
Shadow 流量是把请求复制一份到 shadow 路径,真实用户拿 stable 响应,shadow 路径用 canary 版本同时跑一次生成但只把结果存起来用于离线评估。shadow 的好处是可以在不暴露给真实用户的情况下评估 canary 版本的实际表现 —— 拿到一份"v2.5 在真实流量上的 recall@1 / 答案质量分 / 流式响应时间分布",作为是否要把 canary 比例从 5% 提到 50% 的依据。shadow 的工程成本主要是"双倍推理开销":每条请求跑两次 LLM(一次给真实用户,一次 shadow),对 GPU budget 是 2 倍压力,所以 shadow 通常只对 1-5% 的请求启用,而不是全量。
Mirror 流量是把请求的"输入 + 元数据"镜像一份到独立环境,用于评估新的 retrieval 配置、新的 embedding 模型、新的 chunk 策略对真实查询分布的覆盖率。mirror 与 shadow 的关键区别:shadow 是用真实用户请求跑 canary 版本,mirror 是用真实用户请求的输入跑离线评估环境(可能是离线 embedding + 离线 reranker + 离线 LLM),完全不影响真实用户。mirror 在 retriever / reranker 这种"看不见的中间件"做版本切换时尤其有用 —— 用户感受不到 retriever 变化,但 retriever 内部的 recall@1 变化能立即通过 mirror 评估出来。
三件套的工程化落地,通常用 Argo Rollouts(对 Web 应用 traffic split) + Langfuse(LLM-specific trace + shadow) + 自研 flag 平台(路由键 + cohort 哈希) 三件组合实现。Argo Rollouts 负责 canary 比例切流,Langfuse 负责把 shadow 流量的结果打 trace + 与 stable 流量做 diff,自研 flag 平台负责 cohort 哈希 + 路由键求值。三件组合的关键集成点是cohort key 必须跨系统一致:Argo Rollouts 切流用的 cohort key 和 Langfuse 打 trace 用的 cohort key 必须用同一个哈希函数、同一份 user_id 到 cohort 的映射表,否则会出现"Argo 把用户切到 canary、Langfuse 却把这条 trace 打到 stable"的错位事故。
LLM 应用的回退机制要做到"30 秒内可触发、5 分钟内可回滚"这种节奏,需要把回退分四层:
in-flight 回退(秒级):对当前正在处理的请求立即中断,把流式输出切成 fallback 模型响应。这种回退通常由流式响应里的 token budget 超时、guardrail 规则触发、模型 rate limit 触发等"硬约束"事件驱动。in-flight 回退的关键是"中断已经发出的 chunk、要给客户端发 SSE close event 让 UI 知道结束",而不是悄悄断连。in-flight 回退一般不需要人介入,自动触发。
traffic-level 回退(30 秒级):在 LB / 网关层把进入这个 flag 的流量按比例切到 fallback 路径。traffic-level 回退通常由 on-call 触发,触发条件是"发现 flag 的某项指标在恶化)"但还没严重到要全局回退。traffic-level 回退的关键是回退粒度可调:可以从 50% canary 切到 10% canary,也可以从 10% canary 切到 0%(等于全量 stable)。这套"细粒度回退"是 LLM 应用的标配 —— 传统 SaaS 通常只支持"切 0% 或 100%",但 LLM 应用因为 prompt / retriever / guardrail 任何维度都可能出问题,需要细粒度回退。
session-level 回退(分钟级):把属于这个会话的流量切走,但允许新会话进入。这种回退适合"老用户看到 bad response 但不能立即切走,只能等会话自然结束"的场景 —— 长会话型 agent 应用里,如果用户在 t0 看到 canary 版本的坏 response,我们要做的是"让这个会话剩下的对话用 stable 版本",而不是粗暴地"把这个用户会话所有写状态都丢了"。session-level 回退的实现是给每个会话打一个 lock_to_stable flag,这个 flag 一旦设置,后续 chunk 都用 stable prompt 版本。
global rollback(5 分钟级):整套 flag 集合全量回退到上一稳定版本。这是事故级别 >= P0 时的兜底,触发条件是"traffic-level 回退 + session-level 回退 + 在 capacity 上都失败"。global rollback 的关键纪律是只能回退到已知稳定版本 —— 不能回退到"未经过 5 分钟观察期的中间版本",因为中间版本可能本身就有 bug。global rollback 通常用一个 atomic flip:把所有 flag 集合从一个"experiment set"切到"stable set",DB 层用 transaction 保证要么全切要么全不切,不会出现"50% flag 切到 stable、50% 还留在 canary"的撕裂状态。
四层回退的关键工程纪律:每一层回退都必须有对应的"回退原因"字段。如果一次 traffic-level 回退没有记录回退原因,on-call 在事故后复盘时无法回答"这次为什么切了 50%、切到 0% 还是切到 10%",这是事后复盘最大的盲点。回退原因应该是一个结构化字段(不是 free-form string),至少包含 (a) 触发回退的指标名(比如 recall_at_1_drop > 5%)、(b) 触发时间、(c) on-call 邮箱、(d) 触发时的指标快照(在 trace 系统里 link 过去)。这套结构化回退记录,会在事故复盘时配合 RAG/Copilot 的 trace 形成完整证据链。
LLM 应用的可观测性要比传统 SaaS 多出至少 5 个维度,才能让 on-call 在事故发生 5 分钟内定位到具体是哪个 flag 哪个维度出了问题:
第一,per-flag 指标:每个 flag 都应该有独立的 latency p50/p95/p99、error rate、token usage、cost、quality score。quality score 通常用离线评估集 + 在线反馈的混合:离线评估集给 baseline quality score,在线反馈(用户 thumbs up/down、regenerate 率、人工 audit 反馈)给 delta quality score。per-flag 指标的 dashboard 要做到"一个 flag 一个 panel",不能把所有 flag 指标塞进一个大 panel —— 因为 on-call 在事故现场是要"按 flag 维度快速定位",不是"按全局维度看趋势"。
第二,per-cohort 指标:每个 cohort(免费/付费/企业 × 新用户/老用户 × 地域 × 语言)都应有独立的指标,这样可以发现"v2.5 prompt 对免费用户的 latency 偏高,但对付费用户正常"这种跨维度不均匀的事故。per-cohort 指标的工程成本主要是数据量大,但 on-call 价值极高 —— 多数 prompt 版本事故只在某个特定 cohort 下暴露,比如"只在中文 + 移动端 + 免费用户"组合下暴露的 UI 渲染 bug,传统全局指标看不出来。
第三,flag 版本回放:每次 flag 版本变更后,旧版本的 trace 要保留 90 天以上,新版本发布后能立即对旧版本的 trace 做"在新版本下重放"。这个能力对事故复盘极关键 —— 比如发现 v2.5 在某些 query 下掉质量,可以立即在 v2.6 下重放相同 query,看 v2.6 是否修了 v2.5 的 bug。trace 重放通常用 Langfuse 的 "replay" 能力实现,把 query + 中间 retrieval 结果 + 最终 LLM 输入 作为 input,新版本重新跑一遍 LLM,得到 diff。
第五,回退日志与审计:每次回退(包括 in-flight / traffic-level / session-level / global rollback)都要记录 (a) 触发时刻、(b) 回退原因结构化字段、(c) on-call 邮箱、(d) 触发时的指标快照 link。回退日志的保留期建议 1 年以上,这样年度复盘能看到"这一年我们回退了多少次、每次回退的原因分布、平均回退到恢复的时间(MTTR)"。MTTR 是渐进式发布系统最关键的 KPI 之一,通常落后事故 30 秒内能定位、5 分钟内能恢复,5 分钟是一个工程目标。
LLM 应用的渐进式发布不只是"实验流程",它还要承担合规 / 安全 / 成本这三个一阶约束。任何一个 flag 变更,都要经过这三道护栏的验证:
合规护栏:某些地区因为数据主权要求(比如 GDPR、CCPA、中国的《数据安全法》),必须强制使用 on-prem 模型而不能用 hosted API。这种约束通常用 per-region 的强制 flag 实现 —— 对 EU 用户,model_provider 强制等于 "on-prem-eu",其它 flag 不能 override。强制 flag 的工程纪律是任何 flag 变更都要过合规审查:不能允许工程师用 flag override 绕过合规约束。这点对全球化 LLM 应用尤其关键,某些事故就是因为"工程师为了让 latency 达标,在 EU region 把 model_provider 从 OIDC 切到 OpenAI hosted",这种事故在合规审计时会变成 P0 级事件。
安全护栏:LLM 应用特有的安全约束包括 prompt injection 过滤、jailbreak 防御、敏感信息(PII)脱敏、输出内容审核。这些约束必须作为强约束 flag 存在 —— 任何 prompt 版本都不能绕过 prompt injection 过滤,任何输出都必须经过 PII 脱敏。安全护栏的工程纪律是"安全 flag 不能被业务 flag override":即使 on-call 紧急想跳过 PII 脱敏让 latency 达标,也必须走"安全 audit + 多签 + 自动恢复"路径,而不是简单地"在 flag 平台里把 pii_redaction 设为 false"。这套"安全 flag 标红"机制是 LLM 应用的安全门,传统 SaaS 通常没这么严。
成本护栏:LLM 应用的成本主要由 token 使用量 + GPU 时间 + 检索调用次数决定,任何一个 flag 维度变更(比如换更大的 LLM、用更贵的 embedding 模型)都可能导致成本暴涨。成本护栏的工程纪律是"flag 变更必须经过成本预估":提交 flag 变更时,平台自动跑一次"假设这个 flag 在 1% 流量上启用一周,周成本是多少美元",预估超预算的 flag 变更被自动拒绝或要求 finance 审批。这套成本预估机制对 LLM 应用尤其重要,因为 LLM 推理成本是"token × 调用次数"的乘法,任何一个维度的成本上升都会被乘法放大。
三道护栏的工程落地,通常用 OPA(Open Policy Agent) + 自研 flag policy 双层实现:OPA 写"强约束规则"(比如 EU region 必须用 on-prem),自研 flag 平台写"软约束规则"(比如提示工程师想用更大模型时必须先填成本预估)。强约束规则在 flag 提交时硬拦截,软约束规则在 flag 提交时警告但不拦截,留给人工 review 决策。
把上面 7 节的内容凝练成 11 条可执行项,留给负责 LLM 应用发布的 SRE / 工程团队:
Conversation
0 条