博客.
博客.
(联系方式)

保持联系

如果你想联系我,无论是讨论合作机会、分享想法,还是只是打个招呼,都可以通过以下方式找到我。

(最后更新)

版权所有 / 2026

(快捷键)C
返回学习路线
(03 · AI Native)2026年8月

可靠性设计

把 LLM 当成一个「聪明但会撒谎、会宕机」的外部依赖——幻觉分类、校验循环、降级矩阵、幂等续跑,用分布式系统的老工具箱配上两件新武器。

可靠性设计

换个姿势看 LLM

做后端的人都懂一个常识:任何网络调用都不可信。数据库会慢,下游服务会挂,返回的数据可能是垃圾。于是我们发明了超时、重试、熔断、降级、幂等——一整套分布式系统的可靠性工具箱。

现在把 LLM 摆进这个图里。它是什么?一个 P99 延迟几十秒、会一本正经地编造事实、输出格式看心情、还会限流和宕机的远程服务。 它很聪明,但「聪明」不改变它的可靠性本质。这一章的核心观点就一句话:

不要发明新的可靠性理论。把分布式系统的老工具箱搬到 LLM 上,再补上两件它特有的新武器:幻觉治理和输出校验。

下面分四块讲透:幻觉治理、输出校验与修复、降级策略、幂等与断点续跑。每一块都会聊到「手段的有效性边界」——知道一个工具什么时候失效,比会用这个工具更重要。


幻觉治理:先搞清楚「幻觉」到底是哪种病

幻觉分类学:事实性 vs 忠实性

「幻觉」是个筐,什么错都往里装,就没法治了。学界常用的分类(见幻觉综述 arXiv:2311.05232)把它拆成两类:

  • 事实性幻觉(Factuality):模型说的与世界事实不符。比如「鲁迅获得了诺贝尔文学奖」——这是模型参数里的知识错了或根本没有
  • 忠实性幻觉(Faithfulness):模型说的与给它的上下文不符。比如你把一篇文档喂给它做摘要,它编了文档里没有的数字——世界知识可能对,但它不忠于输入

这个区分不是学术洁癖,因为两类幻觉的治理手段完全不同

幻觉类型病根有效手段无效手段
事实性参数知识缺失/过时检索接地、联网搜索校验引用(它引用的文档可能就是错的)
忠实性生成时偏离上下文引用校验、nli 一致性检查再检索一遍(文档没问题,是它没看)

检索接地(Grounding):治事实性幻觉的主力

RAG 的思路一句话:先给证据,再让说话。回答前先把相关资料检索出来塞进上下文,把闭卷考试变成开卷考试。开卷和闭卷的编故事概率,完全是两个量级。

但 RAG 不是银弹,它的有效性边界很清晰:

  • 检索质量决定上限。模型再强,检索回来的文档是错的或不相关,答案照样错——甚至会因为「文档权威感」编得更自信。embedding 模型、rerank、chunk 切分都要当成独立子系统优化,别把精力全花在 prompt 上
  • 检索不到的领域无能为力。纯推理任务(数学证明、代码逻辑)没有「文档」可检,RAG 帮不上忙
  • 必须在 prompt 里给退路:「资料中没有就直接说不知道」。不给这条退路,模型会把不相关的文档硬掰成答案——这就是忠实性幻觉开始滋生

引用(Citation):让忠实性幻觉可以被「检查」

治忠实性幻觉的关键是把「信不信」变成「查不查得到」:让模型的每个关键结论带回出处——引用了第几篇文档、哪个段落。好处有两层:

  1. 用户可以点过去核对,信任感完全不同
  2. 程序化校验成为可能:把模型引用的段落和源文档做一致性检查(字符串匹配、nli 蕴含判断、或者再发一个 LLM 调用问「这段原文是否支持这个结论」),对不上 = 大概率幻觉,打回重答

注意边界:引用校验只能保证「忠于文档」,不能保证文档本身是对的。垃圾进、看起来很有出处的垃圾出。

Self-RAG 与 CRAG:论文里的校验回路长什么样

两篇论文值得精读,它们代表了「校验回路」的两种设计哲学。

Self-RAG(arXiv:2310.11511)的思路是把「该不该检索、答案靠不靠谱」变成模型自己生成的特殊 token。它训练模型在生成过程中穿插四类反思标记:

  • Retrieve:这个问题需不需要检索?(不需要就别浪费检索成本)
  • ISREL:检索回来的段落和问题相关吗?
  • ISSUP:我生成的这句话被段落支持吗?(忠实性打分)
  • ISUSE:整体回答有用吗?

推理时按这些标记打分、筛选、重排序,选出「最被证据支持」的生成路径。实验结论(论文摘要层面):在开放域问答、事实验证类任务上显著超过普通检索增强的 Llama2-chat,长文生成的事实性和引用准确率超过当时的 ChatGPT。关键启示:校验不是外挂的补丁,可以内化到生成过程里。 代价是要专门训练/微调,工程门槛高。

CRAG(arXiv:2401.15884)则相反,校验是完全外挂的轻量模块,不改模型:在检索器和生成器之间加一个「检索评估器」,把检索结果分成三档——

  • Correct:文档靠谱,走正常 RAG,并对文档做「拆解-筛选-重组」提纯
  • Incorrect:文档垃圾,果断扔掉,触发联网搜索补充
  • Ambiguous:不确定,两者结合

CRAG 的校验回路:检索结果先过检索评估器,按 Correct / Ambiguous / Incorrect 分成三个分支分别处理

实验上 CRAG 在 PopQA、PubHealth 等数据集上明显好于朴素 RAG 和 Self-RAG 的对照设置。关键启示:一个便宜的小评估器 + 明确的三分支决策,往往比端到端大方案更好落地。 大多数工程团队的起点应该是 CRAG 式的外挂校验,而不是 Self-RAG 式的训练方案。

两条路线怎么选?实践中往往是组合:主干用 CRAG 式的外挂校验(便宜、可插拔、模型升级不返工),只在核心高价值场景为 Self-RAG 式的内化能力做专项微调。 还有一个经常被忽略的工程细节:校验器本身也会错(误判率不为零),所以校验结果应该影响「是否重试/拒答」这类可逆决策,而不要直接改写给用户的最终答案。


输出校验与修复:永远别信模型的格式

你要的是 JSON,模型给你的是「好的!以下是你要的 JSON:```json …」。你要的字段叫 user_name,它心情一好写成 userName。这是每天真实发生的事。

Schema 校验生态:三层工具

第一层:声明式 schema 库。 TypeScript 用 Zod,Python 用 Pydantic——定义一次结构,同时获得校验器和可读的错误信息。错误信息很重要,因为它本身就是给模型的修复指令。

第二层:约束解码。 OpenAI 的 Structured Outputs、各家的 JSON Mode,按 JSON Schema 在解码阶段直接约束模型只能输出合法结构,把格式问题消灭在生成时。能用就优先用,schema 校验退为兜底。注意它只保证「形状合法」,不保证「内容正确」。

第三层:校验即交互的封装库。 Python 的 Instructor 是代表作:它 patch 模型客户端,让你直接返回一个 Pydantic 对象,校验失败时自动把错误信息拼进上下文重试。看它的用法就明白整个模式:

import instructor
from pydantic import BaseModel, Field

class Answer(BaseModel):
    summary: str
    score: float = Field(ge=0, le=10)
    citations: list[str] = Field(min_length=1)

client = instructor.from_provider("openai/gpt-4o-mini")

answer = client.chat.completions.create(
    response_model=Answer,   # 校验失败会自动带错误重试
    max_retries=3,
    messages=[{"role": "user", "content": "总结这篇文档并打分"}],
)
# 拿到的是校验通过的 Answer 实例,不是裸字符串

TypeScript 侧手写这个循环也不复杂:

async function generateWithRepair<T>(
  prompt: string,
  schema: ZodType<T>,
  maxRetries = 3,
): Promise<T> {
  let feedback = "";
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const raw = await llm.chat(prompt + feedback);
    const result = schema.safeParse(extractJson(raw));
    if (result.success) return result.data;
    feedback = `\n上次输出校验失败:${result.error.issues
      .map((i) => `${i.path.join(".")}: ${i.message}`)
      .join("; ")}\n请只修正这些字段后重新输出完整 JSON。`;
  }
  throw new Error("输出修复超限,转降级流程");
}

修复循环的收敛性:它为什么不保证成功

「校验失败 → 反馈错误 → 重新生成」这个循环没有收敛保证。实践中会看到三种失败模式:

  1. 振荡:这次修好字段 A 弄坏字段 B,下次修好 B 又弄坏 A,来回横跳
  2. 躺平:模型开始输出空对象、空数组——校验上「合法」,业务上是垃圾(所以 schema 里要加 min_length 这类业务约束,不能只校验类型)
  3. 死结:任务本身超出模型能力,比如要求它输出一个它根本不知道的值

对策都是工程手段而非祈祷:重试硬性设上限(2~3 次足够,再多边际收益骤降);反馈要具体(只说「错了重试」成功率低很多,带上字段路径和错误原因);超限后必须走降级而不是无限循环。把修复循环的命中率埋点监控起来——命中率突然下降,通常是模型版本升级或 prompt 被改动的信号。

输出修复循环:LLM 生成后过 Schema 校验,失败则把错误反馈拼回 prompt 带反馈重试,超限转降级流程

Self-Critique 之争:到底管不管用

生成完再让模型自己挑一遍毛病(self-critique / reflection),是 Agent 框架的标配动作。但它有没有用,社区是有真争议的,正反方都有论文:

  • 反方:Google DeepMind 的《Large Language Models Cannot Self-Correct Reasoning Yet》(arXiv:2310.01798)发现,在没有外部反馈的情况下,让模型自我纠正推理错误,多数时候反而让正确率下降——模型不知道自己不知道,「挑毛病」常常把对的改错
  • 正方:CRITIC(arXiv:2305.11738)等工作证明,当 critique 能调用外部工具验证时(跑代码看报错、搜索核对事实、查计算器),自我修正确实显著提升表现

两边的结论拼起来其实一致,也是工程上的行动指南:self-critique 的价值取决于有没有外部锚点。 纯内省的「你再检查一遍」基本无效甚至有害;能接外部信号(工具执行结果、检索证据、单元测试)的 critique 才值得放进 pipeline。另外它对知识类幻觉天然无力——那正是引用校验的活。


降级策略:模型挂了,服务不能挂

线上真相:各家模型 API 都会挂,限流更是家常便饭。你的服务 SLA 不能建立在「模型永远在线」这个假设上。

决策矩阵:什么错误配什么动作

降级的核心不是「换个模型」四个字,而是一张错误类型 × 应对动作的决策表。直接用这张当起点:

触发信号典型含义应对动作千万别做
超时(>2~3 倍 P99)网络抖动或模型过载换备用模型重试 1 次同模型立刻重试(大概率再超时)
429 限流触达速率配额指数退避重试;常态化则换模型/加配额无退避猛刷,触发更严限流
5xx服务商故障熔断 + 切备用模型重试轰炸(对面正在救火)
400 / 上下文超长你的请求有问题截断/压缩上下文后重建请求原样重试(确定性失败)
内容审核拦截输入或输出触线换措辞重试或直接拒答换模型绕过审核
输出校验连续失败模型能力与任务不匹配升级更强的模型重试,再败则降级放宽 schema 放水

按兜底程度从高到低,完整的降级链是四层:

  1. 换模型:主模型失败 → 备用模型(别家同档,或自家小模型)。定义好优先级列表和切换条件
  2. 换能力:生成式做不了 → 退回检索直出(把 top-1 文档摘要直接展示),功能降级但结果可信
  3. 退模板:LLM 全挂 → 缓存的常见答案、预设模板、规则系统顶上
  4. 坦诚失败:明确告诉用户「智能服务暂不可用」,而不是让前端转圈三分钟

降级链四层:按兜底程度从高到低依次是换模型、换能力、退模板、坦诚失败

超时、重试、熔断:参数经验值

这些参数没有理论最优解,但社区有踩坑踩出来的经验区间,照抄再按监控调:

  • 超时:取你业务 P99 延迟的 23 倍。流式场景要区分「首 token 超时」(通常 510s)和「总时长超时」,不设超时的流式请求能把连接池耗死
  • 重试:最多 2~3 次;指数退避 base 1s(1s → 2s → 4s)+ full jitter(在 [0, delay] 区间随机取),防止大量客户端同步重试形成惊群;给重试设全局预算(如重试请求不超过总请求量的 10%),避免故障期流量放大
  • 熔断:滚动窗口内(如最近 1020 次请求)错误率 >50% 或连续失败 5 次 → 断开 3060s;之后放 1 个探测请求(半开),成功则恢复,失败则继续断。熔断的价值是故障期把钱和连接省下来,还能给上游留恢复时间
const models = ["claude-sonnet", "gpt-4o-mini", "local-qwen"]; // 按优先级

async function chatWithFallback(prompt: string) {
  for (const model of models) {
    if (breaker.isOpen(model)) continue; // 熔断中的模型直接跳过
    try {
      const result = await llm.chat(prompt, { model, timeout: 30_000 });
      breaker.onSuccess(model);
      return result;
    } catch (err) {
      breaker.onFailure(model, err);
      console.warn(`模型 ${model} 失败,切换下一个`, err);
    }
  }
  return getTemplateAnswer(prompt); // 最后一层:模板兜底
}

最后强调:降级路径不演练等于没有。 定期在预发手动掐掉模型调用,观察系统是否如设计表现;降级时的答案质量、延迟、成本都要进监控面板。


幂等与断点续跑:长任务不怕断

Agent 类任务动辄跑几十分钟、调几十次工具、烧掉几美元 token。跑到第 40 步进程挂了从头再来是不可接受的——不光浪费钱,有些工具调用(发邮件、写库、调支付)重复执行会出事故。好消息是:分布式系统三十年前就把这个问题研究透了,直接搬。

三个经典模式,搬到 Agent 场景

幂等键(Idempotency Key)。 给每个关键操作分配唯一键,执行前先查「这个键做过没有」:

def send_report(task_id: str, report: str):
    key = f"send-report:{task_id}"
    if store.exists(key):
        return store.get(key)  # 已经发过了,返回上次结果
    result = email_api.send(report, idempotency_key=key)
    store.set(key, result)
    return result

原则:所有有副作用的工具调用(写操作)都要幂等化,读操作随意。Stripe 的 API 就是这个设计——重试网络超时的扣款不会扣两次。你的 Agent 工具层应该长一样。

去重表(Dedup Table)。 消息队列和事件流场景的经典做法:消费前把消息 ID 插进一张带唯一约束的去重表,插入冲突说明处理过,直接跳过。搬到 Agent 场景:每一步工具调用落一条记录 (task_id, step_no, tool, args_hash, result),唯一约束天然防重放。它和幂等键是同一思想的两种存储形态——幂等键防的是「外部副作用重复」,去重表防的是「内部步骤重复执行」

租约与 fencing token(Lease / Fencing)。 长任务最怕的不是挂,是假死:worker A 卡住了,调度器把任务派给 worker B,结果 A 又活过来,两个 worker 同时写结果。经典解法是租约(worker 持有带 TTL 的租约,定期续约)加 fencing token(每次派发生成一个单调递增的编号,存储层只接受编号更大的写入,过期 worker 的迟来写入被拒)。Agent 场景的适配:每次断点恢复生成新 epoch,checkpoint 和工具结果都带 epoch 写入——僵尸 worker 恢复后的旧 epoch 写入直接失败,不会造成「两个 Agent 同时给用户发邮件」。

断点续跑:状态外置,步骤幂等

有了上面三件套,断点续跑就是很薄的编排层:把每一步状态(进度、中间结果、已完成操作的幂等键)持久化,崩溃恢复时——

加载存档 → 跳过幂等键命中/去重表已有的步骤 → 从断点继续跑(新 epoch)

LangGraph 的 checkpointer、Temporal 的 workflow replay,本质都是这个思想。两个落地建议:

  • checkpoint 的粒度对齐「工具调用边界」,而不是 token 边界——一次工具调用要么完整落档要么不落,避免恢复时 replay 半个副作用
  • 把人工审批也当成一种 checkpoint:高风险操作(发邮件、动生产库)前暂停任务、等待人工确认、确认结果写进存档再续跑。审批状态进了存档,恢复后就不会重复弹审批,也不会跳过审批直接执行

一个反直觉的附加好处:断点续跑还顺便缓解「上下文超长」——长任务可以把早期步骤的结论固化到存档里按需取回,而不是一直堆在 prompt 里。


常见误区

  • 用提示词治幻觉:「请不要编造信息」的约束力约等于贴在电脑上的「不许死机」便利贴。幻觉要靠检索、引用、校验这些系统机制来治
  • 分不清两类幻觉就下药:事实性问题去做引用校验(文档本身就是错的),忠实性问题去加重检索(它根本没看文档)——药不对症
  • 无外部锚点的 self-critique:「你再检查一遍」多数时候把对的改错,给它工具、证据、测试用例才有意义
  • 只校验不拒答:校验发现答案不靠谱,正确动作是拒答或重试,不是「加一句免责声明然后照发」
  • 重试不设幂等:重试网络超时的写操作,可能把同一封邮件发三遍。重试之前先想清楚这个操作幂等吗
  • 降级只在 PPT 上:不演练的降级路径等于没有
  • 拿平均值做容量规划:LLM 延迟分布尾巴极长,按平均延迟设超时和并发,等于按晴天设计雨伞

术语表

名词定义一句话直觉常见混淆
幻觉(Hallucination)模型生成看似合理但无依据的内容自信的编故事不等于「模型犯错」——逻辑错误、格式错误不算幻觉
事实性幻觉生成内容与世界事实不符它记忆里的知识是错的与忠实性幻觉的病根和药方完全不同
忠实性幻觉生成内容与给定上下文不符开卷考试还不看卷子不是知识缺失,是没忠于输入
检索接地(Grounding / RAG)先检索证据再生成闭卷改开卷只能提事实性,管不了忠实性;检索错了照样编
引用校验核对生成结论是否被引用源支持每句话查脚注能证「忠于文档」,不能证「文档为真」
Self-RAG用反思 token 把检索/自评内化进生成的训练方案模型边写边给自己打分与 CRAG 路线相反:要训练 vs 纯外挂
CRAG外挂检索评估器,按 Correct/Incorrect/Ambiguous 三分支纠错检索结果先过安检不是又一种检索器,是检索质量的裁判
Schema 校验用声明式结构约束并验证模型输出先验货再签收约束解码(JSON Mode)只保证形状,不保证内容正确
Self-Critique模型对自己的输出进行评审修订自己给自己改作业无外部锚点时常常越改越错;要接工具反馈才有效
降级(Fallback)主路径失败时切换到次优但可用的路径主厨请假,学徒顶上不是「报错给用户」,降级成功用户无感
熔断(Circuit Breaker)连续失败时短期切断调用,定期探测恢复保险丝:先断电,再试送与重试互补不矛盾:熔断防雪崩,重试抗抖动
幂等键(Idempotency Key)操作的唯一标识,重复执行只生效一次同一订单号提交多次只扣一次款重试 ≠ 幂等:重试是动作,幂等是动作安全的前提
去重表(Dedup Table)用唯一约束记录已处理消息/步骤的表盖过章的票不能再用与幂等键同思想:一个防外部副作用,一个防内部重放
Fencing Token单调递增的授权编号,拒绝过期写入旧钥匙开不了新锁解决「假死复活」,不是解决「并发争抢」
断点续跑(Checkpointing)步骤状态外置持久化,崩溃后从断点恢复游戏存档不是「记住对话历史」,是「记住执行进度」

参考材料


小结

四句话总结这一章:

  1. 幻觉治不了,只能兜——先分清事实性还是忠实性,前者靠检索接地,后者靠引用校验,校验回路(Self-RAG 内化 / CRAG 外挂)在发出前拦最后一道
  2. 输出永远要校验——schema 管形状,带外部锚点的 critique 管内容;修复循环不保证收敛,上限、具体反馈、降级出口一个不能少
  3. 降级是必修课——按错误类型查决策矩阵,超时重试熔断用经验参数起步,降级链不演练等于没有
  4. 长任务先想断怎么办——幂等键防副作用重复,去重表防步骤重放,fencing token 防僵尸复活,三者齐了断点续跑才真的安全

把 LLM 从「神奇的黑盒」降级为「一个不太靠谱的下游依赖」,你的系统反而可靠了——这正是 AI Native 工程师的成熟标志。