模型输出不稳定怎么兜底:从容错解析到降级链路的系统工程
把 LLM 当普通函数用,你会得到一个「偶尔返回 JSON、偶尔返回散文、偶尔什么都不返回」的诡异依赖。兜底不是补救,而是系统设计的一部分。
一、先认清现实:模型输出有哪些「不稳定」
在 Agent 系统中,模型输出的不稳定可以粗暴地分为四类,每一类的兜底手段完全不同:
| 不稳定类型 | 表现 | 典型原因 |
|---|---|---|
| 格式漂移 | 该返回 JSON 返回了 Markdown;字段名大小写不一致 | 指令理解偏差、模型版本切换 |
| 结构缺失 | 少字段、字段类型错误、数组被截断 | 输出长度上限、生成中断、多轮拼接丢失 |
| 内容漂移 | 同一输入两次输出语义不同、数值抖动 | 采样随机性(temperature)、模型更新 |
| 行为漂移 | 该调工具没调、工具参数错误、循环不退出 | 指令跟随不稳定、上下文过长 |
关键认知: 稳定不是「让模型变稳定」,而是「让系统在模型不稳定时仍然稳定」。工程上的兜底必须做到:即使模型返回了最差的结果,系统也不会崩溃、不会输出错误内容、不会卡死。
二、第一层兜底:结构化输出——把自由发挥关进笼子
2.1 强制工具调用格式
不要用「请以 JSON 返回」这种请求式约束,要使用平台级的结构化能力:
- OpenAI / Anthropic 的 Function Calling / Tool Use:模型必须按 schema 生成参数;
- Structured Output(JSON Schema 强制):OpenAI 的
response_format: { type: "json_schema" }在解码层保证输出合法 JSON; - 开源推理服务(vLLM 等)的 guided decoding / outlines:采样时直接限制 token 范围。
这三者的共同点:非法输出在生成阶段就被物理性禁止,而不是生成后再靠提示词「希望它改」。
2.2 输出 Schema 设计守则
Schema 设计本身就能减少一半的不稳定:
- 必填字段要少:能缺省就不要必填,用默认值兜底;
- 类型收窄:用枚举而不是开放字符串(
"level": "low" | "medium" | "high"),枚举之外模型无从发挥; - 拒绝自由文本:需要自由文本的地方单独给字段,不让模型把非结构化内容混进结构化字段;
- 版本化 schema:
schema_version字段随输出下发,解析侧按版本兼容处理。
三、第二层兜底:容错解析——模型乱写也能接住
即使强制了 schema,线上仍会出现解析失败(截断、非法字符、嵌套过深)。解析层必须宽容,而不是假设输入永远合法。
3.1 多级解析策略
原始输出
→ ① 严格解析(JSON.parse / schema 校验) [成功则结束]
→ ② 修复解析(清洗 Markdown 围栏、截断 JSON、修正尾逗号)
→ ③ 模糊提取(正则提取关键字段,如首个 {...} 块)
→ ④ 全量兜底(返回默认结构 + 原文作为"raw_text"字段)
def safe_parse(raw: str, schema) -> Parsed:
# ① 严格
try:
data = json.loads(raw)
return schema_validate(data, schema)
except Exception:
pass
# ② 清洗围栏与注释
cleaned = strip_markdown_fence(raw)
try:
return schema_validate(json.loads(cleaned), schema)
except Exception:
pass
# ③ 正则提取首个完整 JSON 对象
m = re.search(r"\{.*\}", raw, re.S)
if m:
try:
return schema_validate(json.loads(m.group()), schema)
except Exception:
pass
# ④ 兜底:默认结构 + 原文
return schema.defaults() | {"raw_text": raw, "parse_failed": True}
要点: 任何一层失败都不能抛异常中断用户请求,而是带着「解析失败」标记继续向下,由业务层决定是重试还是降级。
3.2 字段级修复
解析成功后,再对关键字段做类型与值域修复:
- 字符串 → 数字:
"价格是 100 元"中提取100; - 枚举漂移:映射表归一到合法枚举(
"high!" → "high"); - 空值兜底:
null/空字符串替换为 schema 默认值或标记unknown。
四、第三层兜底:校验与重试——失败是常态,重试是机制
4.1 分层的校验规则
LLM 输出必须经过「规则校验」,且规则优先于模型判断:
| 校验层 | 校验内容 | 手段 |
|---|---|---|
| 结构校验 | schema、必填字段、类型 | 代码校验(确定性) |
| 业务校验 | 枚举合法、数值范围、日期格式 | 规则引擎 / 正则(确定性) |
| 语义校验 | 内容与 query 相关、引用存在 | LLM 二次判定(概率性,成本高) |
规则:能用确定性校验解决的,绝不动用第二次 LLM。
4.2 自动重试策略
校验失败后按以下顺序处理:
校验失败
→ 修复型重试:把失败原因(校验错误信息)反馈给模型,让其修正(1~2 次)
→ 兜底型重试:降低要求(放宽 schema / 换小模型)再试 1 次
→ 降级输出:返回默认值 / 缓存结果 / 明确告知"生成失败"
重试的纪律:
- 必须携带失败原因重试,盲重试命中率极低;
- 限制重试次数(≤2 次),否则成本与延迟失控;
- 重试间可降 temperature,给模型换一条采样路径;
- 写操作绝不静默重试(防止重复下单),重试只适合读操作与生成操作。
4.3 降级链路设计
一个生产级 Agent 的输出链路应该是「阶梯式」的:
L1 完整回答(大模型 + 全量上下文) ┐
L2 简化回答(小模型 / 裁剪上下文) ├─ 逐级降级,任一成功即返回
L3 缓存命中(历史相似问题结果) │
L4 默认话术("暂时无法回答,请稍后重试") ┘
每一级的失败都会自动落到下一级,保证用户永远拿到响应,而不是错误页。
五、第四层兜底:确定性手段——让输出「可重复」
对稳定性要求极高的场景,可以牺牲部分质量换取确定性:
- temperature = 0:绝大多数模型在 greedy 解码下对相同输入输出基本一致;
- seed 固定:部分 API 支持
seed参数,配合 temperature=0 进一步稳定采样(注意:非所有模型保证严格复现); - 确定性采样:开源模型可用
do_sample=False+ beam search,输出完全确定; - 结果缓存:对相同输入直接返回历史结果,天然「零漂移」。
适用范围提示: 确定性手段适合「解析/分类/信息抽取」类任务;对创意生成类任务强行确定性会损失质量,应改用「校验 + 重试」而非「压随机」。
六、第五层兜底:版本管理与回滚——漂移的根本解药
输出不稳定的一个隐形来源是:你根本没有意识到模型或 Prompt 变了。 模型供应商的静默更新、Prompt 的随手修改,都可能让线上行为瞬间漂移。
6.1 三层版本控制
| 层 | 版本化对象 | 手段 |
|---|---|---|
| Prompt 版本 | 每个 Agent 的提示词 | Git + 语义化版本号,变更必须留档 |
| Schema 版本 | 输出结构 | schema_version 字段,解析按版本兼容 |
| 模型版本 | 模型型号 / 快照 | 显式锁定模型版本,禁止「跟随最新」 |
6.2 灰度与金丝雀
- 新 Prompt / 新模型先走 10% 流量灰度,对比输出质量指标(见第七节);
- 质量指标劣化(如解析失败率上升 1%、格式漂移率翻倍)时自动回滚到上一版本;
- 建立「回滚开关」:线上出现大面积漂移时,一键切回稳定版本,而不是现场排障。
6.3 变更即回归测试
任何 Prompt / 模型 / Schema 变更,都要跑一遍固定回归集(几十~上百条典型请求),对比输出 diff。允许内容合理变化,但结构、字段、格式不允许回归。
七、可观测性:让「不稳定」可量化、可告警
兜底机制是否生效、漂移是否在恶化,必须靠指标说话:
结构层:解析失败率、schema 校验失败率、字段缺失率
重试层:重试率、重试成功率、降级触发率(按级别)
质量层:格式漂移率、内容重复率(同输入异输出)、用户投诉率
版本层:当前 Prompt/模型版本、灰度流量比例、回滚次数
两个核心告警:
- 解析失败率异常上升——通常是模型更新或 Prompt 改动导致,超过阈值立即告警并触发自动回滚;
- 同输入不同输出的高离散度——在测试集上周期性对比输出一致性,作为内容漂移的先行指标。
建议把「最终兜底层(L4 默认话术)触发率」当作系统健康的红线:它不应超过 1%,一旦超了就说明主链路已经大面积失效。
八、完整兜底架构图
用户请求
└─> 生成(结构化输出约束)
└─> 容错解析(严格 → 修复 → 模糊 → 默认)
└─> 确定性校验(schema + 规则)
├─ 通过 → 输出 ✅
├─ 可修复 → 携带原因重试(≤2 次)→ 输出 ✅ / 降级
└─ 不可修复 → 降级链路(简化回答 → 缓存 → 默认话术)
上层保障:Prompt/Schema/模型三版本控制 + 灰度回滚 + 质量指标告警
设计原则总结:
- 生成阶段关笼子:结构化输出 + 约束解码,物理性禁止非法输出;
- 解析阶段多级兜底:任何解析失败都不抛异常,带着标记继续;
- 校验阶段确定性优先:规则校验永远走在 LLM 二次判定前面;
- 失败阶段阶梯降级:每一级失败自动落到下一级,用户永远有响应;
- 变更阶段版本管控:Prompt、Schema、模型三版本 + 灰度回滚,杜绝「静默漂移」;
- 运行阶段指标告警:把不稳定变成可量化指标,异常自动触发回滚。
九、关键结论
- 稳定的系统不是「模型听话」,而是「模型不听话时系统也不出事」——兜底架构的完备性比 Prompt 的精妙程度更重要。
- 结构约束 > 提示词请求:用 Function Calling / JSON Schema / 约束解码,把不稳定消灭在生成阶段。
- 解析必须宽容:多级解析 + 字段修复,让「脏输出」也能被接住。
- 失败要设计:校验 → 携带原因重试 → 阶梯降级,写操作绝不静默重试。
- 变更要管控:Prompt / Schema / 模型三版本 + 灰度回滚,从源头掐断「漂移」。
- 把「兜底层触发率」当作系统红线指标,主链路健康度一目了然。
兜底的终极形态是:无论模型今天「状态如何」,用户拿到的都是一个结构完整、内容合理、可追踪的响应。稳定性不是模型的属性,而是系统的设计。