• 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

  • 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

LLM Gateway 容错实战

2026-08-25

做线上 AI 服务的同学,应该都体会过那种半夜被告警炸醒的崩溃。

其实做线上久了就明白一个道理:开发环境能跑不叫稳定,异常场景不崩,才是真正的生产可用。

LLM 服务和普通接口完全不一样,网络抖动、接口限流、模型过载、Token 超限、空返回、节点下线……随便一个小问题,都能直接干掉一整条多轮 Agent 链路。

没有容错的 LLM Gateway,说白了就是裸奔。

这篇文章我就好好复盘一下我们整套容错体系的迭代过程。从最开始参考 OpenClaw 的容错设计思想,到最后自己在 Go 生产服务里重构落地。


一、LLM 调用不是简单单次接口请求

很多新手开发会误以为:LLM 调用就是发一次请求、拿一次结果,和普通 HTTP 接口没区别。

单纯的对话接口确实是这样,一次请求一次响应,结束就完事。

但Agent 链路完全是另一个东西。

用户一句简单的指令,背后可能藏着六七轮的思考、行动、观察循环。多轮迭代、工具调用、自主决策,整条链路是有状态、长流程、可中断、不可重来的。

这就导致一个很致命的问题:整条链路前面十几步都跑成功了,最后一步接口报错,所有上下文全部作废,用户任务直接白跑。

线上 LLM 服务的异常场景真的非常多,我整理了日常生产最常见的几类,基本全覆盖了:

  • 429 限流:单 Key 打满 RPM 配额,高峰期高发,最常见

  • 503 过载:厂商模型集群负载过高,临时拒绝请求

  • 认证失效:Key 过期、密钥轮换,概率低但一出事就是大面积故障

  • 上下文溢出:对话轮次多、工具返回数据量大,Agent 场景重灾区

  • 网络超时:跨区域调用、DNS 抖动,随机出现,防不胜防

  • 模型空返回:模型内部异常,返回空内容,偶发但很坑

  • 模型不可用:版本下线、区域不支持,概率极低但影响极大

如果网关没有分层容错,上面任意一个问题,最终给到用户的都是同一个结果:失败。

而且 Agent 本身有三个特性,会无限放大故障影响:多轮执行、依赖工具、自主决策。

普通接口失败只是单次损失,Agent 失败是「前面所有计算资源、上下文、用户等待时间全部白费」。


二、OpenClaw 七重容错

它的设计思路我非常认同:所有恢复策略,必须代价递增。

就像看病一样,先上最简单、成本最低、副作用最小的方案,能解决就绝不升级。简单重试能搞定就不换 Key,换 Key 能搞定就不降级参数,降级能搞定就不切换模型。

它的核心骨架就是一个重试循环,我把它的核心伪代码贴出来,一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// OpenClaw 核心容错循环伪代码
async function runAgentLoop(context) {
let attemptCount = 0;
// 根据可用密钥数量动态扩容重试上限
const maxAttempts = clamp(24 + keyCount * 8, 32, 160);

while (attemptCount < maxAttempts) {
try {
const result = await runSingleAttempt(context);
// 任务正常完成,直接返回
if (result.finished) return result;
attemptCount++;
} catch (err) {
// 统一错误决策:可重试则继续,不可重试直接抛出
const { shouldRetry } = handleError(err, context);
if (shouldRetry) {
attemptCount++;
continue;
}
throw err;
}
}
// 用尽所有次数,彻底失败
throw new MaxRetryExceededError();
}

固定动态最大重试次数,统一捕获异常、统一决策重试、超限直接终止,从根源避免乱重试、死循环。

基于这套动态上限的循环骨架,OpenClaw 定义了完整的分层容错体系。整套策略从轻到重、代价逐级递增,完全贴合生产容错的核心思路:

整套体系分为七层策略,从轻到重依次升级:

  1. Token 自动刷新:密钥过期自动续,零成本无感恢复

  2. 上下文智能压缩:Token 超限时自动摘要旧对话,保住核心上下文

  3. 工具结果截断:压缩无效时,适度丢弃冗余数据,优先保证任务跑完

  4. Key 配额轮换:单 Key 限流、失效,自动切备用 Key

  5. 推理级别降级:复杂参数不兼容时,降低推理深度保证可用

  6. 指数退避重试:模型过载时,阶梯式等待重试,避免越重试越崩

  7. 模型兜底降级:所有方案失效,最后切换兜底模型保可用性

这里有一个做容错最重要的思维:必须加断路器。

压缩不能无限压、重试不能无限重、Key 耗尽必须停、降级到底必须断。没有断路器的容错,比没有容错更可怕,直接死循环炸集群。

同时它区分了两条完全隔离的故障路径:

上下文溢出问题,只走压缩、截断逻辑;接口故障、限流问题,只走重试、轮换、降级逻辑。

两条路径互不干扰,不会出现「上下文满了还瞎换模型」这种问题,逻辑清晰。


三、生产需重构

我们老项目是多年迭代堆出来的超级大函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LLM请求
│
├── 自定义模型配置构建
├── 敏感词检测
├── 构建请求体
│
├── 请求模型函数(ctx, req)
│ └── 请求模型内部(llm 层)
│ ├── [for A] 降级链轮询 主模型 → fallback1 → fallback2
│ └── [for B] Provider 轮询 同逻辑模型不同厂商
│ └── 真正的请求模型 → 协议转化 → 各 provider
│ └── 请求失败(错误决策)
│ ├── [for C] 限流重试
│ └── [for D] RetryableError 重试
│
│
├── 9种连接错误 if-else
│
└── 流式处理 闭包
├── [for E] RetryableError → 重新调请求模型函数
├── [for F] 空返回 → 重新调请求模型函数
└── [for G] Fallback 换模型 → 重新调请求模型函数

各种逻辑层层嵌套、重试套重试、错误处理散落各处,当初一点点加功能,最后彻底失控。

老代码最致命的四个问题,也是很多迭代型老项目的通病:

1. 重试嵌套,完全不可控

底层 llm 层有一套重试,上层业务闭包又嵌套一套重试。一个限流错误,可能触发多层循环反复重试,重试多少次、什么时候终止,没人能一眼看明白。线上问题最怕这种「黑盒逻辑」,出问题根本没法快速定位。

2. 相同逻辑重复写两遍

同样的错误判断、同样的恢复逻辑,连接阶段写一次,流式输出阶段又写一次,两套逻辑还不完全一致。后期改 bug、加功能必须改两处,极其容易漏改、出兼容问题。

3. 超大闭包,变量乱共享

600 多行的闭包逻辑,捕获十几个外部变量,多套结果状态互相覆盖,代码可读性、可测试性基本为零。除了最初开发的人,没人敢轻易改动,迭代效率极低。

4. 决策层级混乱

最核心的问题就是权责错乱。底层 llm 层的响应处理逻辑,直接私自判定是否需要模型降级、重试兜底。上层 service 业务层完全不知情,没法统一管控降级链路、重试策略,也做不了统一的监控上报。

简单说:该做决策的上层没权限,不该决策的底层乱兜底,整套容错体系完全没有章法。

也是因为这些历史遗留问题,线上偶尔出现的诡异重试、莫名报错、重复输出问题,根本没法快速根因定位。


四、全新架构重构

这次重构的核心目标很明确:解耦分层、统一决策、显性循环、强制兜底。彻底干掉嵌套重试、散落逻辑、混乱闭包,把所有容错逻辑规范化、标准化。

重构后的 架构,不再是一堆杂乱的业务堆砌,而是清晰的四阶段执行流程,每一层职责都极度单一:

流程:参数构建 → 前置校验 → 请求组装 → 统一容错执行

最核心的改动,就是抽离出了通用的 executeLLM 容错引擎,所有模型调用、重试、降级、兜底逻辑,全部收敛到这一层。

1. 双层循环:彻底理清重试与降级关系

新架构采用「外层模型降级循环 + 内层同模型重试循环」的正交设计,彻底解决之前嵌套混乱的问题:

  • 内层循环:单一模型范围内的重试兜底。遇到限流、临时过载、网络抖动等可重试错误,走指数退避重试,用尽当前模型的容错潜力

  • 外层循环:全量降级链兜底。当前模型重试耗尽、彻底失败后,再切换下一个 fallback 兜底模型

逻辑:**能重试就不换模型,换模型是最后不得已的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 重构后:LLM 容错双层循环核心伪代码
func executeLLM(ctx, req) error {
// 1. 预构建完整降级链路:主模型 + 所有兜底模型
fallbackModels := buildFallbackChain(req)

// 外层:模型降级循环(彻底失败才换模型)
for _, model := range fallbackModels {
// 内层:同一模型内多次重试
maxRetry := max(req.RetryTimes, 3)
for retryCnt := 0; retryCnt <= maxRetry; retryCnt++ {
res, err := tryOnce(ctx, model) //模型建连和流式处理
// 统一决策器:四种结果
decision := errorHandler.Decide(err, res.hasChunk)

switch decision {
case Success:
return nil
case NeedRetry:
// 集群防雪崩 + 指数退避
if !quotaAcquire() { goto Fallback }
backoff.Wait(retryCnt)
case NeedFallback:
goto Fallback
case Fatal:
return err
}
}
Fallback:
}
// 所有模型、所有重试全部耗尽
return ErrAllFailed
}

两套循环完全解耦,重试只管单模型容错,降级只管切换模型兜底,彻底解决老代码嵌套混乱的问题。

2. 统一错误决策器:消灭散落的 if-else

我单独抽离了 ErrorHandler 统一决策模块,把所有错误的处理逻辑收敛到一处。不再是各个阶段零散写判断,所有异常只会输出四种结果:正常结束、重试、切换模型、致命终止。

这里加了一个 关键逻辑:只要已经流出部分数据流给用户(hasChunk=true),直接判定为致命错误,禁止重试。

很多人容易忽略这点:流式场景下,用户已经看到部分输出了,如果后台悄悄重试、重新推送内容,会导致内容错乱、重复输出、上下文混乱,体验崩坏。这种情况下,宁可直接失败,也不能盲目容错。

3. 硬上限断路器 + 集群防雪崩

容错最怕的不是失败,是无限容错引发的集群雪崩。

新架构做了双重兜底保护:

  • 固定硬上限:无论配置如何,单模型重试次数最低保底3次,杜绝配置失误导致零容错,同时设置最大上限,避免无限循环

  • 集群重试配额控制:新增 TryAcquireRetryQuota 限流逻辑,当整个集群重试压力过大时,直接拒绝新增重试请求,防止故障扩散、拖垮全链路

  • 指数退避+随机抖动:重试间隔阶梯递增,同时加入随机时间,避免大量请求同时重试引发的惊群效应

彻底杜绝了容错本身变成故障源头的问题。


五、上下文防护

前面的重试、降级体系,解决的是「接口故障、网络异常、限流过载」的问题。而 LLM 场景最特殊的上下文溢出问题,需要单独一套防护体系,严格遵循路径隔离原则。这个是在上游做的,按照OpenClaw的设计

  • 前置窗口校验:请求组装阶段提前校验最大 Token 数量,提前拦截超限请求

  • 客户端分层裁剪:完整对话历史由客户端持有,负责日常上下文摘要、历史裁剪、冗余清理,减轻服务端压力

  • 服务端兜底截断:针对工具返回的超大结果,服务端做强制截断,避免单轮数据打爆上下文

  • 模型降级兜底:常规裁剪无效时,自动切换更大窗口模型,保证任务不中断

始终坚守核心规则:上下文溢出只做压缩、裁剪、换大窗口模型,绝不做无效的接口重试;接口故障绝不乱改上下文,两条防线互不干扰、各司其职。


六、新旧架构核心对比

这次重构我梳理了最核心的几点差异:

1. 重试拓扑:从嵌套黑盒到正交可控

旧架构两层重试互相嵌套、互相触发,重试次数不可预估,线上行为不可控;新架构内外层职责清晰,重试、降级逻辑显性化,任何故障的处理路径都可预判、可追溯。

2. 错误决策:从散落全局到统一收口

旧架构错误判断散落在十多处代码,重复冗余、逻辑不一;新架构全部收敛到 ErrorHandler,改一处即全局生效,迭代维护成本直接减半。

3. 流处理与决策解耦

旧架构超大闭包混合数据消费和故障决策,逻辑纠缠;新架构流式消费只做数据搬运,不做任何业务决策,彻底实现关注点分离。

4. 可观测性全面升级

旧架构监控指标零散,没法统计重试、降级、失败占比;新架构在每一个决策点统一埋点,重试、Fallback、致命错误全部有独立指标,线上问题一眼就能定位。

5. 彻底杜绝死循环与雪崩风险

显性断路器、硬上限兜底、集群配额限流三重防护,让容错从「可能出问题」变成「绝对可控」。


七、总结

真正的生产级稳定,从来不是依赖接口不故障、网络不抖动,而是预判所有异常,并且优雅兜底所有异常。有三条核心理念:

第一,代价递增,绝不过度容错。

优先用最轻量、零代价的恢复方案,重试、换密钥、降级参数、切换模型逐级升级,能用低成本解决的问题,绝不牺牲用户体验和系统性能。

第二,路径隔离,逻辑彻底干净。

上下文溢出、数据超限属于业务数据问题,走压缩裁剪路径;限流、过载、网络报错属于服务通信问题,走重试降级路径。两条路径完全隔离,不交叉、不混乱。

第三,凡事留闸,所有逻辑必有终止。

容错最怕失控。任何重试、压缩、降级逻辑,必须有硬上限、有断路器、有终止条件。没有兜底限制的容错,远比没有容错更危险。

很多人评判服务稳定性,只看正常场景的响应速度、吞吐量。但在我看来:一个 LLM 网关的健壮度,从来不取决于它顺境跑得有多快,只取决于它逆境恢复得有多优雅。

扫一扫,分享到微信

微信分享二维码
反曲弓射箭笔记
© 2026 John Doe
Hexo Theme Yilia by Litten