做线上 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 | // OpenClaw 核心容错循环伪代码 |
固定动态最大重试次数,统一捕获异常、统一决策重试、超限直接终止,从根源避免乱重试、死循环。
基于这套动态上限的循环骨架,OpenClaw 定义了完整的分层容错体系。整套策略从轻到重、代价逐级递增,完全贴合生产容错的核心思路:
整套体系分为七层策略,从轻到重依次升级:
Token 自动刷新:密钥过期自动续,零成本无感恢复
上下文智能压缩:Token 超限时自动摘要旧对话,保住核心上下文
工具结果截断:压缩无效时,适度丢弃冗余数据,优先保证任务跑完
Key 配额轮换:单 Key 限流、失效,自动切备用 Key
推理级别降级:复杂参数不兼容时,降低推理深度保证可用
指数退避重试:模型过载时,阶梯式等待重试,避免越重试越崩
模型兜底降级:所有方案失效,最后切换兜底模型保可用性
这里有一个做容错最重要的思维:必须加断路器。
压缩不能无限压、重试不能无限重、Key 耗尽必须停、降级到底必须断。没有断路器的容错,比没有容错更可怕,直接死循环炸集群。
同时它区分了两条完全隔离的故障路径:
上下文溢出问题,只走压缩、截断逻辑;接口故障、限流问题,只走重试、轮换、降级逻辑。
两条路径互不干扰,不会出现「上下文满了还瞎换模型」这种问题,逻辑清晰。
三、生产需重构
我们老项目是多年迭代堆出来的超级大函数。
1 | LLM请求 |
各种逻辑层层嵌套、重试套重试、错误处理散落各处,当初一点点加功能,最后彻底失控。
老代码最致命的四个问题,也是很多迭代型老项目的通病:
1. 重试嵌套,完全不可控
底层 llm 层有一套重试,上层业务闭包又嵌套一套重试。一个限流错误,可能触发多层循环反复重试,重试多少次、什么时候终止,没人能一眼看明白。线上问题最怕这种「黑盒逻辑」,出问题根本没法快速定位。
2. 相同逻辑重复写两遍
同样的错误判断、同样的恢复逻辑,连接阶段写一次,流式输出阶段又写一次,两套逻辑还不完全一致。后期改 bug、加功能必须改两处,极其容易漏改、出兼容问题。
3. 超大闭包,变量乱共享
600 多行的闭包逻辑,捕获十几个外部变量,多套结果状态互相覆盖,代码可读性、可测试性基本为零。除了最初开发的人,没人敢轻易改动,迭代效率极低。
4. 决策层级混乱
最核心的问题就是权责错乱。底层 llm 层的响应处理逻辑,直接私自判定是否需要模型降级、重试兜底。上层 service 业务层完全不知情,没法统一管控降级链路、重试策略,也做不了统一的监控上报。
简单说:该做决策的上层没权限,不该决策的底层乱兜底,整套容错体系完全没有章法。
也是因为这些历史遗留问题,线上偶尔出现的诡异重试、莫名报错、重复输出问题,根本没法快速根因定位。
四、全新架构重构
这次重构的核心目标很明确:解耦分层、统一决策、显性循环、强制兜底。彻底干掉嵌套重试、散落逻辑、混乱闭包,把所有容错逻辑规范化、标准化。
重构后的 架构,不再是一堆杂乱的业务堆砌,而是清晰的四阶段执行流程,每一层职责都极度单一:
流程:参数构建 → 前置校验 → 请求组装 → 统一容错执行
最核心的改动,就是抽离出了通用的 executeLLM 容错引擎,所有模型调用、重试、降级、兜底逻辑,全部收敛到这一层。
1. 双层循环:彻底理清重试与降级关系
新架构采用「外层模型降级循环 + 内层同模型重试循环」的正交设计,彻底解决之前嵌套混乱的问题:
内层循环:单一模型范围内的重试兜底。遇到限流、临时过载、网络抖动等可重试错误,走指数退避重试,用尽当前模型的容错潜力
外层循环:全量降级链兜底。当前模型重试耗尽、彻底失败后,再切换下一个 fallback 兜底模型
逻辑:**能重试就不换模型,换模型是最后不得已的手段。
1 | // 重构后:LLM 容错双层循环核心伪代码 |
两套循环完全解耦,重试只管单模型容错,降级只管切换模型兜底,彻底解决老代码嵌套混乱的问题。
2. 统一错误决策器:消灭散落的 if-else
我单独抽离了 ErrorHandler 统一决策模块,把所有错误的处理逻辑收敛到一处。不再是各个阶段零散写判断,所有异常只会输出四种结果:正常结束、重试、切换模型、致命终止。
这里加了一个 关键逻辑:只要已经流出部分数据流给用户(hasChunk=true),直接判定为致命错误,禁止重试。
很多人容易忽略这点:流式场景下,用户已经看到部分输出了,如果后台悄悄重试、重新推送内容,会导致内容错乱、重复输出、上下文混乱,体验崩坏。这种情况下,宁可直接失败,也不能盲目容错。
3. 硬上限断路器 + 集群防雪崩
容错最怕的不是失败,是无限容错引发的集群雪崩。
新架构做了双重兜底保护:
固定硬上限:无论配置如何,单模型重试次数最低保底3次,杜绝配置失误导致零容错,同时设置最大上限,避免无限循环
集群重试配额控制:新增 TryAcquireRetryQuota 限流逻辑,当整个集群重试压力过大时,直接拒绝新增重试请求,防止故障扩散、拖垮全链路
指数退避+随机抖动:重试间隔阶梯递增,同时加入随机时间,避免大量请求同时重试引发的惊群效应
彻底杜绝了容错本身变成故障源头的问题。
五、上下文防护
前面的重试、降级体系,解决的是「接口故障、网络异常、限流过载」的问题。而 LLM 场景最特殊的上下文溢出问题,需要单独一套防护体系,严格遵循路径隔离原则。这个是在上游做的,按照OpenClaw的设计
前置窗口校验:请求组装阶段提前校验最大 Token 数量,提前拦截超限请求
客户端分层裁剪:完整对话历史由客户端持有,负责日常上下文摘要、历史裁剪、冗余清理,减轻服务端压力
服务端兜底截断:针对工具返回的超大结果,服务端做强制截断,避免单轮数据打爆上下文
模型降级兜底:常规裁剪无效时,自动切换更大窗口模型,保证任务不中断
始终坚守核心规则:上下文溢出只做压缩、裁剪、换大窗口模型,绝不做无效的接口重试;接口故障绝不乱改上下文,两条防线互不干扰、各司其职。
六、新旧架构核心对比
这次重构我梳理了最核心的几点差异:
1. 重试拓扑:从嵌套黑盒到正交可控
旧架构两层重试互相嵌套、互相触发,重试次数不可预估,线上行为不可控;新架构内外层职责清晰,重试、降级逻辑显性化,任何故障的处理路径都可预判、可追溯。
2. 错误决策:从散落全局到统一收口
旧架构错误判断散落在十多处代码,重复冗余、逻辑不一;新架构全部收敛到 ErrorHandler,改一处即全局生效,迭代维护成本直接减半。
3. 流处理与决策解耦
旧架构超大闭包混合数据消费和故障决策,逻辑纠缠;新架构流式消费只做数据搬运,不做任何业务决策,彻底实现关注点分离。
4. 可观测性全面升级
旧架构监控指标零散,没法统计重试、降级、失败占比;新架构在每一个决策点统一埋点,重试、Fallback、致命错误全部有独立指标,线上问题一眼就能定位。
5. 彻底杜绝死循环与雪崩风险
显性断路器、硬上限兜底、集群配额限流三重防护,让容错从「可能出问题」变成「绝对可控」。
七、总结
真正的生产级稳定,从来不是依赖接口不故障、网络不抖动,而是预判所有异常,并且优雅兜底所有异常。有三条核心理念:
第一,代价递增,绝不过度容错。
优先用最轻量、零代价的恢复方案,重试、换密钥、降级参数、切换模型逐级升级,能用低成本解决的问题,绝不牺牲用户体验和系统性能。
第二,路径隔离,逻辑彻底干净。
上下文溢出、数据超限属于业务数据问题,走压缩裁剪路径;限流、过载、网络报错属于服务通信问题,走重试降级路径。两条路径完全隔离,不交叉、不混乱。
第三,凡事留闸,所有逻辑必有终止。
容错最怕失控。任何重试、压缩、降级逻辑,必须有硬上限、有断路器、有终止条件。没有兜底限制的容错,远比没有容错更危险。
很多人评判服务稳定性,只看正常场景的响应速度、吞吐量。但在我看来:一个 LLM 网关的健壮度,从来不取决于它顺境跑得有多快,只取决于它逆境恢复得有多优雅。