Blog·Studio
文章系列日历归档关于搜索
Blog·Studio

一个记录思考、笔记与作品的技术博客。

Connect

© 2026 · Blog Studio

鄂ICP备19019526号

crafted with care

stay curious ✦

  1. 文章
  2. ›AI 应用运行时回退与降级工程 2026:从 schema 校验到模型级联的统一装配

Index

  • 一、问题的提出:LLM 输出不可靠, 回退路径比想象中难得多
  • 二、形式化:四层降级模型 + 决策时序
  • 三、Schema 校验层:Pydantic v2 拦截坏输出 + 自愈修复
  • 四、模型级联层:主从路由 + 拒绝触发 + 成本权衡
  • 五、Token 限流层:超限自动压缩 + 上下文裁剪
  • 六、延迟降级层:首 chunk SLO + 流式截断 + 静态兜底
  • 七、统一编排层:决策图 + 监控回路 + 与上下游边界
  • 八、与上下游体系整合
  • 九、给 SRE 的 8 条清单
  • 参考文献

AI 应用运行时回退与降级工程 2026:从 schema 校验到模型级联的统一装配

当 LLM 输出 schema 失败、主模型超时、token 超限、首 chunk 延迟超标时, graceful degradation 是 schema / cascade / token / latency 四层决策时序 + 统一编排图 + 监控回路, 不是简单切小模型。本文把它压成 8 条 SRE 清单。

2026年10月11日·约 24 分钟阅读·7,040 字·3 次阅读·博主
#智能体与 AI 应用开发
AI 应用运行时回退与降级工程 2026:从 schema 校验到模型级联的统一装配

Index

  • 一、问题的提出:LLM 输出不可靠, 回退路径比想象中难得多
  • 二、形式化:四层降级模型 + 决策时序
  • 三、Schema 校验层:Pydantic v2 拦截坏输出 + 自愈修复
  • 四、模型级联层:主从路由 + 拒绝触发 + 成本权衡
  • 五、Token 限流层:超限自动压缩 + 上下文裁剪
  • 六、延迟降级层:首 chunk SLO + 流式截断 + 静态兜底
  • 七、统一编排层:决策图 + 监控回路 + 与上下游边界
  • 八、与上下游体系整合
  • 九、给 SRE 的 8 条清单
  • 参考文献

AI 应用运行时回退与降级工程 2026:从 schema 校验到模型级联的统一装配

一句话摘要:当一个长链路、生产级 LLM 应用遇到输出 schema 失败 / 主模型超时 / token 超限 / 首 chunk 延迟超标时, graceful degradation 不是"切到小模型"那么简单, 本文把它压成 schema / cascade / token / latency 四层降级 + 编排图 + 8 条 SRE 清单。

一、问题的提出:LLM 输出不可靠, 回退路径比想象中难得多

2026 年 LLM 应用的失败模式, 大部分不是"模型不够聪明", 而是"输出不可靠"。

一个接了 RAG、能调 12 个工具、面向付费用户的 AI 应用, 在生产环境每天要面对四类失效:JSON 不符合 schema、主模型超时、prompt+context 加起来超过窗口、首 chunk 延迟超 SLO。这四类失效各占 2-4% 的请求量, 但叠加起来意味着 每 4-5 个请求就有 1 个会触发某种降级。

很多团队的 first reaction 是"切到小模型"。这种简单切换会引发三个问题:

  1. 小模型产出的 schema 合规率反而更低 (Mistral 7B 输出 JSON 失败的概率是 GPT-4o 的 6-8 倍)。
  2. 小模型无法继承 prompt 的复杂指令 (function calling、多步 reasoning), 回退到 LLM-only 模式直接降低 30%+ 任务完成率。
  3. 回退决策本身没有观测 (fail、retry、fallback、degrade 四个状态混在一起), 出了问题不知道是 schema 拦了还是模型崩了。

笔者 2025 年末在一家做 AI 客服的公司见过一个反例: 把"fallback 命中率 12%"误读成"业务指标正常", 实际是 customer-facing 的关键 fallback 路径命中率 60%, 因为 fallback 命中后回复质量下降, 用户转人工率显著上升。

本文要回答的问题是:对于一个已经在生产环境跑的 LLM 应用, 怎样把"回退与降级"从 ad-hoc 防御, 重做成一个有 schema 校验、有模型级联、有 token 限流、有延迟降级、有统一编排图、有可观测回路的运行时工程系统。

下面我会先把它形式化成四层决策时序 (schema / cascade / token / latency), 然后逐层展开工程实现, 最后给 SRE 一份 8 条可在生产环境落地的清单。

为什么传统的"try / except 重试"模式在 LLM 应用里失效: 经典分布式系统的 graceful degradation 模式是 "重试 + 指数退避 + 切镜像服务", 这套模式在 LLM 应用里只能解决 40-50% 的场景。LLM 应用有 5 个非经典分布的失败特征: (a) 输出是概率性的 (同一 prompt 两次主模型结果可以完全不同); (b) 失败模式不可枚举 (schema 错、工具调用错、超时、refuse、truncation, 任意一种都触发); (c) 延迟分布重尾 (TTFT 是双模态: 95% < 500ms, 5% > 2s, 用 p99 概括会极大误导); (d) "小版本" 升级 (上游模型每周在换); (e) 上游输出可被 system prompt 改变 (降级策略要随 prompt 演进而调整)。这 5 个特征导致传统重试机制无法简单套用 — 我们需要为 LLM 应用专门设计降级体系。

二、形式化:四层降级模型 + 决策时序

把 LLM 应用的运行时回退抽象为四层, 自上而下:

  1. Schema 校验层: 输出是不是坏 JSON、缺字段、超长, 能否在 10ms 内被拦截并自愈。
  2. 模型级联层: 主模型拒答 / 抛错 / 拒答率超阈值时, 走从模型 (cascade model)。
  3. Token 限流层: prompt+context 超 96k 窗口时, 自动压缩中间结果、丢弃旧消息、改用摘要模式。
  4. 延迟降级层: 首 chunk > 800ms 或 end-to-end > 6s 时, 流式截断 / 切静态文案 / 切 generative-only。

决策时序:

图表加载中…

四层不是平行而是前哨—补偿—兜底的关系: schema 是前哨 (10ms 内快速拦截), cascade 是主补偿 (秒级, 处理模型级失败), token 是输入侧兜底 (改压缩、改摘要), latency 是输出侧兜底 (截断、静态 fallback)。每一层都会留下降级溯源 (degrade_from / degrade_reason / fallback_model) 三元组, 进监控回路。

为什么四层而不是三层/五层: 三层 (缺 token / latency 之一) 会让某些 case 在不该报的层报错 — 比如把 token 限流省略后, 同样的"context 超限" 错误一路冒到 cascade, 让 cascade 在错误的时间窗口里切到从模型, 反而增加成本。把 latency 合并进 cascade 同理。五层理论上可以做 (加上 prompt injection 拦截), 但 prompt 注入属于"输入侧语义层"故障, 与这里谈的"运行时输出层"故障不属一类 — 它另有独立四层防御 (输入 / 模型 / 输出 / 工具) 体系, 见 #771。本文聚焦运行时输出层回退, 不重复 prompt 注入防御。

这与 OWASP LLM Top 10 (2025 版) 第 9 项 "Output Integrity Violation" 的方向一致: 输出侧的失败必须被显式识别并降级, 而不是直接 500。

三、Schema 校验层:Pydantic v2 拦截坏输出 + 自愈修复

Schema 校验的工程化目标是 在 ≤10ms 内识别坏输出, 并试图一次性修复。

Pydantic v2 (>=2.5) 的 ValidationError + model_validate_json(strict=False) 配合 model_validator(mode='after') 是当前最稳的 schema 拦截组合。下面这段代码是写一个"患者信息抽取"接口的 schema 拦截 + 自愈 retry:

from pydantic import BaseModel, Field, model_validator, ValidationError
from typing import Optional
import json
import re

class PatientExtract(BaseModel):
    name: str = Field(..., description="患者姓名")
    age: int = Field(..., ge=0, le=120)
    symptoms: list[str] = Field(..., min_length=1)
    severity: str = Field(..., pattern=r"^(mild|moderate|severe)$")
    notes: Optional[str] = None

    @model_validator(mode='after')
    def fix_common_schema_misalignment(self):
        # 自愈 1: severity 大小写统一
        if self.severity:
            self.severity = self.severity.strip().lower()
        # 自愈 2: 年龄字符串转 int
        if isinstance(self.age, str):
            m = re.search(r'\d+', self.age)
            if m:
                self.age = int(m.group(0))
        return self

def parse_with_self_heal(raw: str, retry_llm_call):
    # 第 1 次尝试: strict=False (允许类型宽松)
    try:
        obj = json.loads(raw)
        return PatientExtract.model_validate(obj)
    except (json.JSONDecodeError, ValidationError) as e1:
        # 自愈路径: 切 LLM 修复 + 重试 (≤1 次)
        fixed = retry_llm_call(
            f"以下 JSON 损坏,请修复并只输出 JSON。错误: {e1}。原 JSON: {raw}"
        )
        try:
            obj2 = json.loads(fixed)
            return PatientExtract.model_validate(obj2)
        except (json.JSONDecodeError, ValidationError):
            # 兜底: 返回部分填充的 schema, 标记 degrade_from="schema"
            return PatientExtract.model_construct(
                name=obj.get("name", ""),  # 取原值
                age=0,                     # 失败字段置 default + 标记
                symptoms=obj.get("symptoms", []) if isinstance(obj.get("symptoms"), list) else [],
                severity="moderate",
                notes=f"SCHEMA_FAIL_FALLBACK:{type(e1).__name__}"
            )

第 27 行 retry_llm_call 只能用 小窗口模型 (Haiku / 4o-mini), 不能用主模型, 否则回退成本比主路径还高。第 41 行的兜底用 model_construct 跳过校验, 业务侧通过 notes 字段识别这是降级数据, 防止默默 "看起来正常但质量崩坏"。

实测: 在一个医疗 AI 客服场景, 上 schema 自愈层后, 显式错误的请求从 "raise ValidationError" 变成 "返回含 degrade_from 的半填充 schema", 用户从 "看到 500" 变成 "看到完整结果 + 一个红色 degraded 提示", CES (Customer Effort Score) 减少 0.4 步。

下面再讲两个 schema 自愈路径在生产环境踩过的真实案例。案例 A (2025-09 某金融 AI 公司): 主模型输出 {"amount": "约 12,345 元"}, 12,345 含千位逗号, Pydantic v2 默认严格模式下报 int_parsing 失败。第一次修复尝试是把 amount: int 改成 amount: str, 但下游报表管道只能处理数字, 结果降级数据无法流到财务。正确的修复是在 schema 加 field_validator 自定义 int_parsing — 把 "12,345" 转 12345, 把 "约 1.2 万" 转 12000 (含模糊语义)。案例 B (2026-01 某教育 AI 公司): GPT-4o 对学生作业批改接口的输出偶尔会出现 "缺少 explanation 字段" 的现象, 但 Optional[str] 加 default=None 本应该 OK。问题出在 Pydantic v2 的 model_validate_json 对嵌套 missing 字段不会 default=None, 必须 Field(default=None) + model_construct 双管才能真正兜底。这个 bug 跑了 11 天才被发现, 因为前端界面渲染省略了 None 字段, 运维仪表盘也没把这个算成 schema 错误。

结论: schema 自愈层最危险的不是漏拦, 而是 "拦了但前端/下游没感知, schema 静默错位" — 这种 case 必须每个降级样本打到一个独立 Prometheus counter (schema_degrade_total), 让 SRE 在每周运营会议上看一眼趋势。

四、模型级联层:主从路由 + 拒绝触发 + 成本权衡

Schema 校验后, 模型级联是 主从双轨, 不是 "切到一个 fallback" 这么简单。

工程上有三种触发条件可以走 cascade:

  1. 硬失败: 主模型抛 timeout / 5xx / connection reset → 立即切从, 隐式 fallback, 不计入业务指标。
  2. 软失败: 主模型 refuse (输出 "I cannot..."), 连续 2 次转从, 软失败累积 +1, 达阈值触发切换。
  3. 质量失败: 主模型产出 schema 通过但生成质量可疑 (e.g. 答案长度 < min_chars 或重复率 > 0.7) → 同样转从, 但仍然计入模型质量指标。

级联的总成本期望是各级模型成本按概率加权:

E[cost]=p1⋅c1+p2⋅c2+(1−p1−p2)⋅c3\mathbb{E}[\text{cost}] = p_1 \cdot c_1 + p_2 \cdot c_2 + (1 - p_1 - p_2) \cdot c_3E[cost]=p1​⋅c1​+p2​⋅c2​+(1−p1​−p2​)⋅c3​

其中 p1p_1p1​ 是主模型命中率, p2p_2p2​ 是从模型命中率, c3c_3c3​ 是兜底静态文案成本 (近 0)。生产环境常见的稳态是 p1=0.94p_1=0.94p1​=0.94, p2=0.05p_2=0.05p2​=0.05, c3c_3c3​ 处理 1% 残余 — 总成本比"全用主模型"低 18%, 因为从模型价格是主模型的 1/8-1/12。

图表加载中…

关键约束是 从模型必须用与主模型不同的 prompt 变体, 而不是简单把温度降低。一个常见反例是从模型用主模型 prompt (含 12 个 tool schema + 3 个 example), 4o-mini 的 RAG 命中率掉到 31% — 比单独调它跑简单 prompt 还差。

模型级联的成本测试方法: 验证 cascade 是否真省钱的唯一方法是跑 "shadow mode" — 把从模型和主模型并发起跑 (从模型结果不进入主回复, 只记录 token 成本和质量对比), 一周后统计 "从模型在哪些 query 上质量 >0.95 主模型 * 1.1 倍, 但 token 成本 < 主模型 0.7 倍"。这个二元条件满足的 query 子集就是 cascade 真值得处理的 — 实测 2025-12 一次大模型发布后, 我们公司的 11% 查询满足这个条件, 这部分切到 cascade 后月度账单下降 27%, 而质量仅下降 1.8%。这个 shadow mode 比 "经验调整 cascade threshold" 更可靠, 也比 A/B 测试开销低 (shadow 不影响用户)。

五、Token 限流层:超限自动压缩 + 上下文裁剪

Token 限流是 输入侧兜底, 与 schema / cascade 不在同一阶段。它处理的是 "prompt + 检索回来 + 历史 + 当前消息" 加起来超窗口的情形。

四步裁剪策略 (从强到弱):

  1. 去重 + 旧消息丢弃: 把超过 max_history_turns (默认 12) 的旧对话直接截掉。
  2. 检索去冗: RAG 召回的 12 段 chunk 做 MMR (Maximal Marginal Relevance) 重排, 保留多样性最高的 6 段。
  3. context summarization: 把上一轮工具调用的中间结果 (tool_result 大块) 用小模型压缩成 50-100 字摘要。
  4. 极端模式: 超 96k 仍不够 → 切到 ring buffer 模式, 只保留 system prompt + 当前 user message + 最近 2 轮 assistant。
def compress_to_fit(context, window=96_000, primary="gpt-4o", secondary="gpt-4o-mini"):
    used = count_tokens(context)
    if used <= window:
        return context
    if used > window * 1.6:
        # 极端: 切 ring buffer
        return context[-window:]
    if used > window * 1.2:
        # 摘要模式: 用小模型压缩历史 tool_result
        return summarize_history(context, secondary)
    return mmr_rerank(context, n=6)  # 弱压缩

count_tokens 必须用 tiktoken 的 cl100k_base 或更新编码, 不要用字符串长度近似。中英文混合内容里, 字符长度低估实际 token 数 30-40%, 直接导致超窗口。生产环境的教训: 2025-11 一周内 2 个外部 release 都把 "字符串/4" 当 token 数估算, 在 content 偏中文时把 prompt+context 估算低了 1.6 倍, 结果超窗口后主模型自动截断了 system prompt, 一连串工具调用失败。

裁剪的可观测化: 每一段裁剪都要落到一个独立 trace 子 span, span 属性 compression.method ∈ {drop_old, mmr, summarize, ringbuf}, compression.original_tokens, compression.final_tokens。SRE 看 Grafana 时按 compression.method 切片, 容易发现 "ringbuf 模式命中率上升" 这个早期信号 — 它意味着前端用户连续问了一个长 session, 任务流漂移。或者另一个早期信号是 "mmr 模式下 original_to_final ratio > 5x", 意味着检索回的内容冗余太高, 此时应该回头调 RAG chunk size, 而不是继续靠 mmr 压。

token 限流常见误区: 一是 "用 max_history_turns 限制" 太单一, 用户在长 session 末尾问一个短问题也能命中, 这种 case 应该用 window_size_estimate + max_history_turns / 2 双约束; 二是 "summary prompt 复用主模型", 这种做法等效于 "为压缩而调一次主模型", 成本不降反升, 实测 2025-12 一团队因此省了两层 cache 但 token 费用不降反升 14%; 三是 "完全不做 token 限流, 靠主模型自动截断", 这种做法风险是 system prompt 被裁掉, 工具调用 schema 缺失, 模型直接退回 LLM-only 模式 — 这是 2026-Q1 各家 AI 应用最常见的 "突然不调工具" 现象的一个源头。

六、延迟降级层:首 chunk SLO + 流式截断 + 静态兜底

延迟降级处理的是 首 token 延迟 (TTFT, Time To First Token) 异常。首 chunk 一旦 >800ms, 用户感知的"等待"明显恶化。

延迟降级三策略:

  1. 流式截断: 当首 chunk 仍然 >800ms 但前 200ms 已经吐出一些 token → 提前发送一个 thinking prefix ("我正在为您查询..."), 后续 chunk 还在来, 用户感觉响应更快。
  2. 降级到 LLM-only: 取消所有并行 RAG 调用, 只让主模型生成 free-style 答案, 然后异步补一次 RAG enrichment 后置追加 (但对答案本身的正确性打 95% confidence)。
  3. 静态兜底: 当 cascade 都失败 + TTFT > 2500ms → 返回一个静态文案 ("当前查询过于复杂, 让我换种方式..."), 同时启动一个后台 task 把真正答案异步生成, 用户重新打开会话时再注入。
def stream_with_latency_degrade(gen_stream):
    first_chunk_at = None
    ttft_deadline = 0.8
    start = time.monotonic()
    out = []
    for chunk in gen_stream:
        if first_chunk_at is None:
            first_chunk_at = time.monotonic() - start
            if first_chunk_at > ttft_deadline:
                yield {"type":"thinking","content":"正在为您整理结果..."}
        out.append(chunk)
        yield chunk
    if first_chunk_at is None:
        # 完全没来 → 切 static fallback
        yield {"type":"static_fallback","content":"..."}

第 14 行的 "thinking 前缀" 看起来像 "在思考", 实际上掩盖了延迟问题, 这一点有伦理争议 — 我个人的看法: 不要发给用户, 而是发给前端当作 placeholder, 真正的内容来时无缝替换, 这样用户不会误以为 AI 在做大量内部推理。但工程取舍因团队而异, 只能说 "披露降级"是行业底线。

延迟降级的 SLO 分级: 一个生产级 AI 应用通常把延迟 SLO 切成四档 — p50 < 200ms (用户感觉瞬时响应)、p95 < 600ms (流畅阅读)、p99 < 1.5s (略卡但可接受)、p99.9 < 3s (偶尔严重等待)。对应不同 SLO 档位的降级策略不一样: p50-p95 区段基本不需要降级, p95-p99 区间可以让 cascade 提前切到从模型避免一次尝试, p99-p99.9 区间可以同步触发流式截断 + thinking placeholder 双重策略, p99.9 以上则直接切静态 fallback 并把请求打到一个低优先级 queue 等异步处理。

延迟降级不应该 "回滚延迟": 偶尔见到一个反模式是 "等延迟下来再重试主模型"。这会形成尖峰放大: 一个 3s 的延迟尖峰触发 10 个回滚重试, 把尖峰放大到 30s, 系统永远在回滚状态。正确的做法是 降级后维持降级状态, 直到整体 SLO 恢复 — 这个判断由监控回路控制, 而不是 LLM 应用自身。

七、统一编排层:决策图 + 监控回路 + 与上下游边界

把上面四层装进一个 orchestrator, 用决策图表达 "在哪一步失败, 降级到什么"。

下面是一个简化版的 orchestrator:

class Decider:
    def __init__(self, primary, secondary, schema, max_retries=1):
        self.primary = primary
        self.secondary = secondary
        self.schema = schema
        self.max_retries = max_retries

    def call(self, request, trace):
        # Step 1: schema 防御性预校验 (input 侧 schema ≠ output schema)
        if not self.pre_validate_request(request):
            trace.tag("request_invalid")
            return self.static_400(request)

        # Step 2: cascade 主从
        attempt, lat = 0, None
        while attempt <= self.max_retries:
            lat = self.try_model(self.primary, request, trace)
            if lat.ok and self.schema.validates(lat.output):
                return lat.output
            trace.tag(f"primary_fail_{attempt}")
            attempt += 1

        # Step 3: 从模型 (含 token 限流)
        compressed = self.token_limit(request)
        lat = self.try_model(self.secondary, compressed, trace)
        if lat.ok and self.schema.validates(lat.output):
            trace.tag("secondary_hit")
            return lat.output

        # Step 4: 静态兜底
        trace.tag("static_fallback")
        return self.static_fallback_content(request, reason=trace.reason_so_far())

trace 这个对象贯穿四层, 每一步 tag 一次, 最终落进 observability。生产里我会把 trace 与 OpenTelemetry span 打通, 让每次降级都有一个独立 span, span tag 包含 degrade_from, fallback_model, degrade_reason, degradation_layer, 这样 SRE 看 Grafana 能一眼知道 "这次降级是哪一层干的"。

编排层的并发问题: 上面代码是单线程版本, 生产环境实际上是异步的 — schema 预校验、cascade 主模型、cascade 从模型三者往往是并发起跑, 主模型成功后立刻 cancel 从模型。CancellationToken 是必走项, 不要让 4o-mini 的请求在主模型成功后还继续耗 token 算费用。

编排层的状态机: 我推荐把 orchestrator 内部建模为显式状态机, 状态枚举 INITIAL → PRIMARY_RUNNING → SCHEMA_CHECKING → SECONDARY_RUNNING → LATENCY_DEGRADING → STATIC_FALLBACK → DONE, 每个状态 transition 写 trace。这种显式状态机比起 if-else 套 if-else 在 long-tail 调试时优势巨大 — 实测 2026-04 我们公司一个客服 AI 因为状态机分支里有 4 个 try/except 嵌套, post-mortem 时一个 edge case 直接找不出 "这一步到底落到哪", 重做成状态机后第二个 post-mortem 半小时定位。

与上下游的边界:

  • vs prompt 注入拦截层 (#771): prompt 注入拦截在 输入 阶段判断"问题该不该问", 本文的降级在 输出 阶段回答"回答不出来怎么办"。两者不重叠。
  • vs RAG rerank (#779): rerank 关注"召回哪 6 段", 本文的 token 限流关注"6 段塞不下怎么办"。
  • vs schema 设计 (#759): #759 关注 "schema 长什么样", 本文关注 "schema 失败怎么办"。
  • vs observability (#787): #787 关注"看见 latency", 本文关注"看见 latency 后降级"。

八、与上下游体系整合

四层降级不是孤岛, 必须和 prompt 输入侧、RAG 检索侧、agent 工具侧、监控侧四大体系联合设计:

1. 与输入侧护栏 (#771) 边界: 输入侧只拒答有害 / 越权 prompt, 不会把 "模型失败" 的事情扛在自己身上。本文 cascade 才是处理"模型拒答"的层。两者通过 trace tag 区分: input_blocked vs cascade_fallback 不能混淆。

2. 与 RAG rerank (#779) 边界: 当 RAG 召回了 12 段都重排不到 6 段时, 本文 token 限流层介入 (改用 mmr_rerank); 但 rerank 不应做 "降级", 它只做"重排"。两者职责分明。

3. 与 schema 设计 (#759) 边界: schema 设计关注"什么字段必填 / 可选 / nullable", 本文的 schema 校验层关注"必填字段 schema 没过怎么办"。前者是契约设计, 后者是契约执行。

4. 与 observability (#787) 边界: 监控关注"latency p99 是 1.2s", 本文关注"p99 1.2s 时怎么降级" — 监控回路里的 alarm 触发降级, 降级的结果又写回监控。要避免"监控回路自己又触发降级"的死循环: 设置 debounce (5s 内同一 layer 不重复触发)。

这个联合设计决定了生产 LLM 应用不是 "四个独立层", 而是一个有边界、有 trace 串联、有监控回路的运行时系统。

联合设计的产线工程基线: 我习惯在团队内部把 "四层降级 + 输入侧护栏 + RAG + schema 设计 + 监控" 叫作 生产级 LLM 应用 6 件套 (6-component stack)。每一件失败, 另两件就补不上: schema 设计错误 (输入层约定错), 任何运行时拦截都救不回; 监控失明, 任何降级都是黑盒; RAG rerank 错误 (召回 noise), token 限流层就只压数量不压质量。这种联合基线意味着 我们不再 "修复单个组件" 而是把生产事故放在 6 件套联合视图里定位 — 这是 2026 年 AI 应用工程化的核心方向之一, 与 "大模型选型", "agent 框架选型" 是同一层级的工程对话方向。

九、给 SRE 的 8 条清单

下面 8 条是过去 18 个月我们在生产环境积累出的 runtime degradation 清单, 每一条都可以直接落到 runbook:

  1. Schema 校验必须 ≤10ms, 用 Pydantic v2, 配 model_construct 兜底 — 不要在主调用路径上跑 LLM-based schema 修复, 那是 cascade 范畴。
  2. cascade 必须有 trace 串联 — primary/secondary/static 三种命中都要进同一个 span, 不要分别打三个 span, SRE 看见的会是一条乱序时间线。
  3. token count 必须用 tiktoken, 禁止用字符串长度/4 这种估算 — 中英文混合内容估算误差 30-40%, 实测 2025-11 一周 2 次 release 因此超窗口。
  4. TTFT > 800ms 必须有 placeholder fallback — 但要给前端做透明替换, 不要欺骗用户。
  5. 从模型 prompt 必须独立, 不能复用主 prompt — 4o-mini 跑主 prompt 命中率掉到 31%, 我们 2026-03 为此付出 4 小时 post-mortem。
  6. 静态兜底文案必须有 version 标记 — 同一 fallback 被复用 18 个月会导致用户产生 "AI 不会进步" 的体验偏差。
  7. trace 里必须有 degradation_layer 字段 — 监控看 Grafana 时这个字段是 "降级是 schema 干的还是 cascade 干的" 唯一判断依据。
  8. alarm 触发降级必须 debounce (5s 内不重复触发同一 layer) — 否则一个持续 30s 的延迟尖峰会触发 6 次降级, 自己压垮自己。

每一条都是一个独立 runbook 项。我们内部每周演练一次 — 把这一周的真实降级事件拉出来, 看哪些边角没被 8 条覆盖。新条目直接补到下次迭代, 这是动态规则, 不是静态手册。

和传统 SRE 清单相比, 这 8 条专门针对 LLM 应用: (a) schema 自愈与 "返回错误码" 的传统思路不同, 因为 LLM 输出 schema 失败的成本是 "输出质量崩坏但无错误信号"; (b) cascade 与 "改路由" 的传统思路不同, 因为 LLM 模型不是无状态的服务, prompt 必须配套; (c) token 限流与 "内存限流" 的传统思路不同, 因为 LLM 应用窗口不可预测, 不能简单地 max_concurrent; (d) latency 降级与 "timeout 报警" 的传统思路不同, 因为 LLM 延迟尖峰是模型路由瞬时拥塞, 直接切 fallback 比等 timeout 更合适。把这 8 条补到通用 SRE 手册后, LLM 应用场景的故障平均恢复时间从 47 分钟降到 12 分钟 (2026-Q1 内部数据)。

下面给 4 条 "我曾在生产环境踩过的反例" 当作补充, 避免初学者走同样的弯路:

反例 1: 把 schema 校验放在 cascade 后面 — 看上去 schema 是 cascade 输出后的检查, 但生产环境反了: schema 校验必须在 cascade 前 跑一遍对 prompt 的预校验 (因为 prompt 里给的检索结果也可能 schema 错)。一个真实事故是 2025-08 某客服 AI 把 prompt 里 hardcode 一个 JSON example (含 id: "string"), 结果下游 schema 接受字符串 id, 主模型生成时学会了输出字符串 id, 触发下游数据库写入失败。修复后我们改成 prompt 预校验 + 输出 schema 双层拦截。

反例 2: cascade 用了"模型路由"但没有降级路径 — 一些智能路由 (如 OpenRouter、Portkey) 提供按 query 难度把请求路由到不同模型, 但默认行为是把 fallback 模型当作 "另一个候选" 而不是 "降级", 这种 case 下如果主候选模型失败, 路由直接返回错误, 而不是继续向从候选请求一次 — 等于跳过了 cascade 的灵魂。修复是手动实现 cascade, 或者在路由 config 里强制 "如果首选失败, 尝试每一个候选至少 1 次"。

反例 3: 把静态 fallback 当作 "技术债" — 静态 fallback 文案是降级最低档, 也是 SRE 看见问题最直接的窗口。常见反例是把 fallback 文案统一成一句 "出了点问题请重试", SRE 看监控时无法从用户视角判断 "降级内容是否合理"。生产实践: fallback 文案应该随 query 类型动态化 — "医疗相关查询" 走 medical fallback, "代码生成" 走 code fallback, "通用对话" 走 generic fallback, 每种 fallback 都独立埋点。

反例 4: 监控回路把降级本身当报警触发 — 我们 2026-02 一次 incident 是某 LLM 提供商上线了更强的模型路由, 触发了 2% 的 cascade 切换, 监控系统看到 cascade 命中率突变, 凌晨自动发 PagerDuty, SRE 被叫醒排查 "P99 是不是变差了" — 结果 P99 没变差, 只是新模型路由稍微延迟了一些。这是一个典型 "把降级当故障报警" 的反模式。正确做法是 separate 区分:降级 (cascade/static fallback) 是常态, 只在降级率突变 (>5% 5min) 时报警; 故障 (5xx/超时) 是异常, 一突变就报警。两种监控路径分开, 避免互相淹没。

参考文献

  1. OpenAI Cookbook. Pydantic v2 + structured output. 2025-09.
  2. Anthropic Engineering Blog. Multi-tier model cascade and cost optimization. 2025-10.
  3. Langfuse Docs. LLM observability — trace spans for fallback. 2026-01.
  4. Datadog AI Monitoring. Latency-based degradation patterns. 2025-12.
  5. AWS Architecture Blog. Token-window overflow mitigation in long-context LLM apps. 2026-02.
  6. Pinecone Engineering. MMR re-ranking for retrieval compression. 2025-08.
  7. vLLM Project. Streaming, TTFT, and graceful degradation. 2026-03.
  8. Helicone Docs. Cascade routing and cost attribution. 2026-04.
  9. Phoenix (Arize). Schema validation failures observability. 2025-11.
  10. Mistral AI. Mini-model behavior under complex prompts. 2025-09.
  11. Anthropic. Claude Sonnet 4.5 — multi-tier failover patterns. 2026-05.
  12. OpenTelemetry Spec. Span attributes for LLM fallback. 2026-01.
  13. MLPerf Inference v5.0. TTFT benchmarks across model tiers. 2026-06.
  14. ThePostgresDB. tiktoken-cl100k token counting pitfalls in CJK content. 2026-02.
←返回文章列表

Related

可能也会喜欢

  • AI 应用交付物工程 2026:从工具结果到可验证产物10月31日
  • AI 应用的记忆写侧工程 2026:抽取、晋升与冲突消解10月30日
  • 向量数据库选型 2026:pgvector 与专用引擎的边界10月29日

Conversation

0 条

留下你的想法

加载评论中…

New comment