用 Java 接入大模型(十一):模型超時或故障時,業務如何降級
用 Java 接入大模型(十一):模型超时或挂掉时,业务怎么降级
這篇 Java 大模型接入教程説明,模型超時或不可用時,應按故障類型採取重試、切換模型、熔斷或明確失敗等降級處理。連線超時只重試 1 次,首字超時可重試或切換備用模型,流式輸出中斷則不重試;總時長超時須提示任務可能已完成。文章亦建議優先使用標記為可降級的快取,並只把上游故障計入熔斷統計。
2026-10-27 8
版权
版权声明:
本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议》和 《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
简介: 本文是「用Java接入大模型」系列第十一篇,聚焦**系统最后一道防线——降级设计**。面对上游故障、限流、网络分区等必然发生的不可用场景,详解超时分级对应策略、熔断机制(含代码实现)、四层降级路径(缓存→换模型→换能力→明确失败)及缓存、重试的降级化运用,强调“出错时给用户什么”比“如何不出错”更重要。
上一篇把并发控制住了。但控制得住不等于不会出事——上游会故障、配额会用完、网络会分区。这一篇讲的是最后一道防线:当"调用成功"这件事做不到时,业务该怎么办。 这也是阶段二的收尾。
一、先接受一件事:模型一定会有不可用的时候
前几篇都在讲"怎么让调用更可靠":分层超时、重试带抖动、限流、连接复用。这些都是提高成功率的手段。但有一类事实必须单独面对:
上游故障不是小概率事件,而是必然事件。
可能的来源有四个,性质完全不同:
- 上游整体故障:服务端 5xx、接口大面积超时。你做什么都没用,只能等。
- 限流:429。你的用量超过了配额,或者上游整体过载。
- 网络分区:连接被重置、DNS 失败、专线抖动。和上游本身状态无关。
- 你自己的配额耗尽:账户欠费、额度用完、并发超限。上游是好的,但对你关门了。
这四种情况的共同点是:它们的发生不取决于你的代码质量。 所以一个有真实流量的系统,如果没有准备降级方案,那就等于把"接口一定会在某个时刻整体不可用"当成了设计前提——而这个前提是错的。
降级要解决的核心问题不是"怎么不出错",而是"出错时给用户什么"。这听起来像妥协,但它其实是产品决策:给一个不完整但正确的结果,也好过给一个错误的结果,更好过什么都不给。
二、超时分级和降级动作要一一对应
第四篇讲过超时要分四层设。这里补上另一半:每一层超时对应什么降级动作。
因为不同层超时的原因不同,能做的补救也不同。
连接超时(5 秒级):网络或 DNS 问题。可能是瞬时抖动,也可能真的断了。
- 动作:重试 1 次,不要多。如果连接都建不起来,重试十次也是白等。
- 不要在这时候换模型——因为可能所有模型都连不上,换了只是换个等待对象。
首字超时(15 秒级):连接建起来了,但上游迟迟不开始返回。说明上游在排队或过载。
- 动作:可以重试,也可以切换到备用模型。这里换模型是有意义的,因为备用模型可能是不同的服务集群,负载情况不一样。
- 这是四层里最适合"换模型"的一层。
块间隔超时(20 秒级):流已经开始,但卡住了。
- 动作:只能按中断处理。内容已经推出去一部分,重试会让用户看到重复或矛盾的内容(第四篇讲过)。
- 这条路径没有"优雅降级"的选项,只能承认失败,并把"这是中断"这个事实准确地传给前端。
总时长超时(兜底):只用来防资源泄漏。
- 动作:按失败处理,但要明确告知用户"任务可能已经完成,请稍后刷新"。因为这种超时下,上游很可能已经算完了,只是你没收到。直接说"失败了"会让用户重复提交,可能产生重复扣费。
这里有个容易搞混的点值得单独说:用户主动断开也要走取消,而不是走降级。 用户点了停止或关了页面,上游的流还在跑、token 还在烧——这是成本问题,不是可用性问题。第四篇提过要留取消的钩子,在这里它是必须的。
三、熔断:别继续打一个已经倒下的上游
重试和降级都是针对单个请求的。但如果上游整体挂了,每个请求都去试一遍、都等一次超时,会带来两个恶果:
- 你的线程和连接全被这些注定失败的请求占住,连正常请求也处理不了;
- 上游本来在恢复,被你持续的高频请求又压垮一次。
所以需要熔断:连续失败到一定比例或次数,就直接跳过调用,走降级路径,不再打上游。
熔断器有三个状态,理解它们就够用了:
- 关闭(正常):请求正常放行,同时统计失败率;
- 打开(熔断):达到阈值后,短时间内所有请求直接走降级,一次都不打上游;
- 半开(探测):熔断一段时间后,放少量请求过去试探。如果成功,回到关闭;如果还失败,回到打开并延长熔断时间。
半开这一步很关键。没有它,熔断器就只是一个定时的开关——时间到了不管上游好没好都全量放行,容易造成二次冲击。
一个极简的实现(生产环境建议直接用 Resilience4j 或 Sentinel,这里是为了看清机制):
public final class SimpleCircuitBreaker {
private enum State {
CLOSED, OPEN, HALF_OPEN }
private final int failureThreshold; // 连续失败多少次触发熔断
private final long openDurationMs; // 熔断保持多久
private final int halfOpenPermits; // 半开状态放行几个探针
private State state = State.CLOSED;
private int consecutiveFailures = 0;
private int halfOpenInFlight = 0;
private long openedAt = 0L;
/** 判断当前是否允许放行 */
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
switch (state) {
case CLOSED:
return true;
case OPEN:
if (now - openedAt >= openDurationMs) {
state = State.HALF_OPEN;
halfOpenInFlight = 1;
return true; // 第一个探针
}
return false; // 熔断中,直接走降级
case HALF_OPEN:
if (halfOpenInFlight < halfOpenPermits) {
halfOpenInFlight++;
return true;
}
return false; // 探针够了,其余走降级
default:
return false;
}
}
public synchronized void onSuccess() {
consecutiveFailures = 0;
halfOpenInFlight = 0;
state = State.CLOSED; // 有一例成功就恢复
}
public synchronized void onFailure() {
switch (state) {
case HALF_OPEN:
state = State.OPEN; // 探针失败,重新熔断
openedAt = System.currentTimeMillis();
halfOpenInFlight = 0;
break;
case CLOSED:
if (++consecutiveFailures >= failureThreshold) {
state = State.OPEN;
openedAt = System.currentTimeMillis();
}
break;
default:
break;
}
}
}
哪些失败应该计入熔断统计,这是最容易做错的一点。
- 要计入:连接超时、首字超时、5xx、429。这些反映上游的真实健康度。
- 不要计入:400 参数错误、401/403 鉴权失败、内容被安全策略拦截。这些是你的请求有问题,不是上游有问题。把它们计入,会出现"因为代码里一个参数写错,导致熔断器打开,全站 AI 功能被降级"这种荒唐事故。
这个区分必须做对,否则熔断器会变成一个故障放大器。
四、降级的四个层次
前面讲的是"什么时候降级",这一节讲"降成什么样"。按代价从低到高,有四层可选。
第一层:换模型。
主用旗舰模型不可用时,切到备用模型。通常是"旗舰 → 轻量",副作用是质量下降和风格变化。
几个要点:
- 备用模型要提前接通并测过。 上线时才第一次调用备用模型,等于把故障现场当测试环境。
- 输出格式要兼容。 如果主模型走的是结构化输出,备用模型也必须支持同样的约束,否则降级后下游解析全崩——这比不降级更糟。
- 降级要能被感知。 用户应该知道"当前用的是快速模式",而不是默默收到质量更差的结果然后开始怀疑系统。
第二层:换能力。
模型这条路走不通,就退到不依赖模型的能力。比如:
- 规则/模板:把常见的几类问题用规则回答(FAQ 命中就用预设答案);
- 历史结果:相同或近似的问题直接返回上次的结果;
- 简化任务:把"生成一段分析"降级为"返回一段统计数据",让代码直接算。
这一层通常是体验下降但不至于不可用,是性价比最高的一层。
第三层:换交互。
把同步等待改成异步处理。用户提交后不等待,完成后通知。
// 降级路径:不阻塞用户,转为异步
if (!circuitBreaker.tryAcquire()) {
taskQueue.submit(new LlmTask(requestId, prompt));
return Response.accepted(requestId, "任务已受理,完成后将通知你");
}
这一层适合那些用户不一定立刻需要结果的场景,比如生成报告、批量处理。但如果是实时对话,改异步就等于功能不可用,要谨慎。
第四层:明确失败。
前面都不行,就老老实实告诉用户"现在用不了,请稍后再试"。
这听起来像放弃,但它是必须保留的一层。因为相比"给一个编造的答案",诚实地说"暂时不可用"是更好的选择。尤其在那些输出会被当作事实依据的场景(法律、医疗、财务),给出错误内容的代价远大于不给。
顺着这一层往下还有一个更细的决策:失败时要不要重试按钮? 这里要区分两种失败:
- 可重试的(超时、限流)→ 提示"请稍后重试",并说明大概多久;
- 不可重试的(内容被拦、参数错误)→ 不要让用户反复重试,因为结果不会变。要提示用户换个说法或改输入。
五、缓存是最容易被忽略的降级手段
降级方案里最常见的漏洞是"没有东西可退"。而缓存恰好能补上这一层。
缓存有两种用法,在降级场景里都有效:
精确缓存:同一个问题、同样的上下文,直接返回上次的答案。
语义缓存:把问题向量化,相似度超过阈值就复用答案。这个在问答类场景里命中率相当可观,因为用户问法千变万化,但问题本身重复度很高。
从降级角度看,缓存的价值在于:它是一个完全不依赖上游的能力。 上游全挂的时候,缓存还能服务相当一部分请求。所以值得把"缓存命中"当成降级路径里优先级最高的一层——它比换模型好(质量不降),比换能力好(结果更贴合)。
但必须处理过期问题,这是缓存作为降级兜底时最容易出事的地方:
- 如果缓存的是会变的信息(库存、价格、状态),降级时返回旧值可能造成实际损失。这种内容就不该进降级兜底。
- 如果缓存的是不会变或变化很慢的信息(产品说明、政策条款、常见问题),拿旧值救急是完全可以接受的。
所以缓存要能区分这两类。一个务实的做法是给缓存项加一个"可降级"标记,只有标了这个的内容才允许在降级路径里使用。
还有一点:缓存命中时不要忘了记录。 因为降级期间命中率会突然升高,如果不记,你会看到上游调用量下降、以为流量掉了,实际是大量请求走了缓存。
六、重试的代价,在降级场景里要重新算一遍
第四篇算过一次重试成本:重试 2 次最坏是请求数的 3 倍。在降级场景里这个账更值得细算,因为故障期间的重试几乎没有产出。
具体来说:
- 如果上游整体故障,你重试三次换来的大概率是三次失败。消耗的额度却是实打实的。
- 重试还会延缓熔断触发。每次重试都算一次尝试,熔断器要花更久才统计到足够的连续失败,这期间所有请求都在白等。
- 熔断打开期间,不要重试。 这一点要落实在代码里:熔断的判断应该在最外层,
tryAcquire()返回 false 就直接走降级,不要进入重试逻辑。
一个合理的组合顺序是:
熔断检查 → 限流检查 → 调用(含重试) → 降级
顺序不能乱。如果先重试再判断熔断,那么熔断就形同虚设——请求已经打了三次上游才被告知"不该打"。
七、小结与决策清单
把这一篇收成一份可以对照执行的清单。
故障类型对应的降级动作:
- 连接超时 → 重试 1 次;不换模型
- 首字超时 → 重试或切换到备用模型
- 块间隔超时 → 按中断处理,明确告知用户内容不完整;不重试
- 总时长超时 → 按失败处理,并提示"可能已完成,请稍后刷新"
- 429 限流 → 退避后重试;若持续则走降级
- 5xx 上游故障 → 触发熔断,走降级
- 401/403 鉴权问题 → 不降级、不重试,立刻告警,这是配置问题
- 400 参数错误 → 不降级,直接暴露,这是代码问题
降级层次的优先级:
- 缓存命中(质量不降,最优)
- 换模型(质量下降但可用)
- 换能力(规则/模板/历史结果)
- 换交互(同步改异步)
- 明确失败(诚实告知,保留重试入口)
必须做对的三件事:
- 熔断只统计上游原因的失败,别把参数错误算进去,否则一个 bug 能触发全站降级
- 备用模型要提前接通并测过,包括输出格式的兼容性
- 熔断打开期间不重试,顺序是"熔断 → 限流 → 调用 → 降级"
最后一句判断标准:如果上游现在全挂,你的接口会返回什么? 能立刻答出这个问题的系统,才算有了降级方案。
下一篇
阶段二到这里收尾:Prompt 会写了、模板能改了、token 算得清、并发控得住、坏掉有退路。
接下来进入阶段三,也就是整个系列里篇幅最大的一块——RAG。开始之前先要回答一个很多人跳过的问题:RAG 到底该不该做。它解决的问题很明确,但被滥用的程度也很高,很多项目上了向量库之后发现"还不如把文档直接贴进提示词"。下一篇就给一套判断标准,以及什么时候该用别的手段。
本文是「用 Java 接入大模型」系列第十一篇,阶段二收尾。前几篇分别讲了 Prompt 工程、提示词模板化、token 计数、并发与限流,建议按顺序看。文中熔断阈值与超时取值为实践起点,实际值请按你的上游 SLA 与压测结果标定;生产环境建议直接使用成熟的熔断组件而非自行实现。
來源:阿里雲開發者社區 AI · developer.aliyun.com