第2181篇:LLM系统的灾备容灾——当OpenAI挂了你的系统怎么办
2026/4/30大约 7 分钟
第2181篇:LLM系统的灾备容灾——当OpenAI挂了你的系统怎么办
适读人群:负责AI系统高可用的工程师 | 阅读时长:约16分钟 | 核心价值:设计多供应商LLM容灾架构,让AI系统在单一API宕机时保持可用
2024年3月,OpenAI连续出现两次API中断,每次持续数小时。
我当时接到好几位同行的消息,内容基本一致:"系统炸了,客服电话打爆了,领导在问怎么回事。"
他们的系统是单点依赖OpenAI的——所有AI功能都走GPT-4的API,没有任何备份。OpenAI挂了,他们的AI就完全不可用了。
这是AI系统架构里经常被忽视的可靠性问题。传统软件的高可用有完善的方法论:多活部署、故障转移、降级策略。但AI系统不一样——你不只需要服务器高可用,还需要模型供应商高可用。
LLM系统的故障类型
LLM系统故障分类:
供应商级故障
├── API完全不可用(503/504)
├── API延迟飙升(正常2秒→现在30秒)
├── API限流(429 rate limit)
├── 模型质量下降(API通了但输出变差)
└── 价格突然变化(影响成本预算)
网络级故障
├── 网络分区(无法连接特定供应商)
└── DNS解析问题
应用级故障
├── 自有RAG向量库不可用
├── 上下文窗口超限
└── 模型输出格式变化(模型升级导致)
降级策略优先级(高可用 > 质量 > 成本):
主路径:首选模型(如GPT-4)
降级1:同等级备用模型(如Claude-3)
降级2:更便宜的小模型(如GPT-4o-mini)
降级3:缓存历史回答
降级4:规则引擎兜底
最后:提示用户服务暂时不可用多供应商路由器
/**
* 多LLM供应商路由器
*
* 核心功能:
* 1. 健康检测(实时感知各供应商状态)
* 2. 故障转移(自动切换到备用供应商)
* 3. 熔断器(避免向故障供应商持续发请求)
* 4. 成本控制(在可用的供应商中优先选便宜的)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MultiProviderLLMRouter {
private final List<LLMProvider> providers; // 按优先级排序
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final ProviderHealthMonitor healthMonitor;
private final FallbackResponseCache fallbackCache;
/**
* 带容灾的LLM调用
*/
public LLMResponse callWithFailover(LLMRequest request) {
// 获取当前可用的供应商列表(按优先级)
List<LLMProvider> availableProviders = getAvailableProviders(request.getQuality());
for (LLMProvider provider : availableProviders) {
CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker(
provider.getProviderId());
try {
LLMResponse response = breaker.executeCallable(
() -> provider.call(request));
// 成功,记录供应商使用情况
healthMonitor.recordSuccess(provider.getProviderId(),
response.getLatencyMs());
if (provider.getPriority() > 0) {
// 不是首选供应商,加个标记说明是降级响应
response.setDegradedMode(true);
response.setProviderUsed(provider.getProviderId());
}
return response;
} catch (CallNotPermittedException e) {
// 熔断器打开,跳过这个供应商
log.debug("供应商熔断中,跳过: {}", provider.getProviderId());
} catch (Exception e) {
// 调用失败,记录并尝试下一个
healthMonitor.recordFailure(provider.getProviderId(), e);
log.warn("供应商调用失败,尝试下一个: provider={}, error={}",
provider.getProviderId(), e.getMessage());
}
}
// 所有供应商都不可用,尝试缓存响应
return handleAllProvidersFailed(request);
}
/**
* 根据请求质量要求过滤可用供应商
*/
private List<LLMProvider> getAvailableProviders(QualityRequirement quality) {
return providers.stream()
.filter(p -> !healthMonitor.isUnhealthy(p.getProviderId()))
.filter(p -> p.getQualityLevel().meets(quality))
.sorted(Comparator.comparingInt(LLMProvider::getPriority))
.collect(Collectors.toList());
}
/**
* 所有供应商都不可用时的兜底处理
*/
private LLMResponse handleAllProvidersFailed(LLMRequest request) {
log.error("所有LLM供应商不可用!尝试降级处理");
// 尝试从缓存获取类似问题的历史回答
Optional<String> cachedResponse = fallbackCache.findSimilarResponse(
request.getUserQuery(), 0.85);
if (cachedResponse.isPresent()) {
log.info("使用缓存回答进行降级");
return LLMResponse.fromCache(cachedResponse.get());
}
// 无缓存,返回服务降级消息
return LLMResponse.degraded(
"抱歉,AI服务暂时不可用。请稍后重试,或联系人工客服。");
}
}供应商健康监控
/**
* 供应商实时健康监控
*
* 主动探测而不是被动等失败
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ProviderHealthMonitor {
private final Map<String, ProviderHealthState> healthStates =
new ConcurrentHashMap<>();
private final AlertService alertService;
/**
* 定期主动探测各供应商状态
*/
@Scheduled(fixedDelay = 30000) // 每30秒探测一次
public void probeAllProviders() {
for (LLMProvider provider : registeredProviders) {
CompletableFuture.runAsync(() -> probeProvider(provider));
}
}
/**
* 主动探测单个供应商
*
* 用一个轻量的标准问题来探测(避免浪费token)
*/
private void probeProvider(LLMProvider provider) {
long startTime = System.currentTimeMillis();
try {
// 用最简单的问题探测(只消耗极少token)
String probeResponse = provider.callRaw(
"Respond with exactly: OK",
10, // max tokens
1000); // timeout ms
long latencyMs = System.currentTimeMillis() - startTime;
boolean healthy = probeResponse != null &&
probeResponse.contains("OK");
updateHealthState(provider.getProviderId(), healthy, latencyMs, null);
} catch (Exception e) {
long latencyMs = System.currentTimeMillis() - startTime;
updateHealthState(provider.getProviderId(), false, latencyMs, e.getMessage());
}
}
private void updateHealthState(
String providerId,
boolean healthy,
long latencyMs,
String errorMsg) {
ProviderHealthState prev = healthStates.get(providerId);
ProviderHealthState current = new ProviderHealthState(
providerId, healthy, latencyMs,
healthy ? null : errorMsg, Instant.now());
healthStates.put(providerId, current);
// 状态变化时发告警
if (prev != null && prev.isHealthy() && !current.isHealthy()) {
log.error("LLM供应商变为不可用: {}", providerId);
alertService.sendAlert(
String.format("LLM供应商 %s 不可用: %s", providerId, errorMsg),
AlertLevel.CRITICAL);
} else if (prev != null && !prev.isHealthy() && current.isHealthy()) {
log.info("LLM供应商恢复: {}", providerId);
alertService.sendAlert(
String.format("LLM供应商 %s 已恢复", providerId),
AlertLevel.INFO);
}
// 延迟飙升告警
if (healthy && latencyMs > 5000) {
log.warn("LLM供应商延迟高: provider={}, latencyMs={}",
providerId, latencyMs);
alertService.sendAlert(
String.format("LLM供应商 %s 延迟高: %dms", providerId, latencyMs),
AlertLevel.MEDIUM);
}
}
public boolean isUnhealthy(String providerId) {
ProviderHealthState state = healthStates.get(providerId);
return state != null && !state.isHealthy();
}
}熔断器配置
/**
* 熔断器配置
*
* 防止向已故障的供应商持续发请求(保护系统 + 保护预算)
*/
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
// OpenAI的熔断配置(相对宽松,因为是主供应商)
CircuitBreakerConfig openaiConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50%失败率触发熔断
.waitDurationInOpenState(Duration.ofMinutes(2)) // 熔断2分钟后半开
.slidingWindowSize(20) // 最近20次请求计算失败率
.minimumNumberOfCalls(10) // 至少10次请求才开始计算
.permittedNumberOfCallsInHalfOpenState(3) // 半开状态允许3次探测
.recordExceptions(ConnectException.class,
TimeoutException.class,
HttpServerErrorException.class)
.build();
// 备用供应商的熔断配置(更严格,避免降级时仍失败)
CircuitBreakerConfig backupConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(30) // 30%就熔断
.waitDurationInOpenState(Duration.ofMinutes(5))
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.build();
return CircuitBreakerRegistry.of(Map.of(
"openai", openaiConfig,
"anthropic", backupConfig,
"azure-openai", backupConfig
));
}
}供应商供应商配置管理
/**
* 多供应商LLM客户端工厂
*
* 支持在配置中管理多个供应商,无需修改代码切换
*/
@Configuration
@ConfigurationProperties(prefix = "llm")
@Data
public class LLMProviderConfig {
private List<ProviderConfig> providers;
@Data
public static class ProviderConfig {
private String id;
private String type; // openai/anthropic/azure/local
private String apiKey;
private String baseUrl;
private String modelName;
private int priority; // 数字越小优先级越高
private QualityLevel quality; // HIGH/MEDIUM/LOW
private double costPerToken;
private boolean enabled;
}
}# application.yml
llm:
providers:
- id: openai-gpt4
type: openai
api-key: ${OPENAI_API_KEY}
model-name: gpt-4-turbo
priority: 1
quality: HIGH
cost-per-token: 0.00003
enabled: true
- id: anthropic-claude3
type: anthropic
api-key: ${ANTHROPIC_API_KEY}
model-name: claude-3-5-sonnet-20241022
priority: 2
quality: HIGH
cost-per-token: 0.000015
enabled: true
- id: azure-openai-gpt4
type: azure
api-key: ${AZURE_OPENAI_KEY}
base-url: https://xxx.openai.azure.com
model-name: gpt-4
priority: 3
quality: HIGH
cost-per-token: 0.00003
enabled: true
- id: openai-gpt4o-mini
type: openai
api-key: ${OPENAI_API_KEY}
model-name: gpt-4o-mini
priority: 10 # 降级专用,优先级低
quality: MEDIUM
cost-per-token: 0.0000002
enabled: true降级响应缓存
/**
* 降级响应缓存
*
* 当所有LLM都不可用时,用历史回答兜底
*/
@Component
@RequiredArgsConstructor
public class FallbackResponseCache {
private final VectorStore vectorStore;
/**
* 查找语义相似的历史回答
*/
public Optional<String> findSimilarResponse(String query, double minSimilarity) {
List<Document> similarDocs = vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(1)
.withSimilarityThreshold(minSimilarity));
if (similarDocs.isEmpty()) return Optional.empty();
Document similar = similarDocs.get(0);
// 检查这个缓存回答是否足够新(超过30天的不用)
Instant cachedAt = similar.getMetadata()
.get("cached_at", Instant.class);
if (cachedAt != null &&
cachedAt.isBefore(Instant.now().minus(30, ChronoUnit.DAYS))) {
return Optional.empty();
}
return Optional.of(
"[注意:当前AI服务有限,以下为历史相关回答仅供参考]\n\n" +
similar.getContent());
}
/**
* 后台任务:定期把高质量回答写入缓存
*/
@Scheduled(cron = "0 0 3 * * *")
public void refreshCache() {
// 获取最近7天的高质量回答(评分>0.85)
List<HighQualityResponse> toCache = fetchHighQualityResponses(7, 0.85);
List<Document> documents = toCache.stream()
.map(r -> new Document(
r.getResponse(),
Map.of("query", r.getQuery(),
"cached_at", Instant.now().toString(),
"quality_score", r.getQualityScore())))
.collect(Collectors.toList());
vectorStore.add(documents);
log.info("降级缓存已刷新,新增{}条记录", documents.size());
}
}核心洞察:可靠性不是附加功能,是AI系统的生命线
OpenAI那两次中断,影响了全球数以万计的AI应用。有的团队二十分钟内切换到了备用供应商,有的团队整整停摆了三个小时。
差别就在于有没有预先设计容灾架构。
几个工程上的经验:
不要把容灾当成"以后再做"的事。API中断不是概率事件,是必然事件——只是不知道什么时候发生。从第一天开始就设计多供应商架构,代价很小,但到故障发生时价值极大。
熔断器比重试更重要。发生故障时,盲目重试会让情况更糟(浪费资源,加重下游压力)。熔断器让你快速承认故障,转向备用方案。
降级要有明确的用户体验。当使用的是备用模型或缓存回答时,要告诉用户("当前使用备用AI,可能效果略有不同"),不要假装什么都没发生。
定期演练。在非生产环境模拟主供应商故障,验证容灾切换是否真的工作。制度化的故障演练(Chaos Engineering)能发现很多平时看不到的问题。
