9 月 3 日,ChatGPT、Claude 和 Grok 几乎在同一时间窗口内出现故障。Grok 约在美东时间上午 9:30 开始异常,ChatGPT 在上午 11 点左右开始返回错误,Claude 则经历了一次持续 3 小时 6 分钟的部分中断。
这件事值得关注的地方,不只是三家 AI 服务都曾经宕机,而是它们的故障时间高度重叠。三家公司给出的官方解释也并不相同:OpenAI 对外称问题与“路由错误”有关,其他服务则有各自的事故描述。现有信息并不足以证明三起事件由同一个根因触发,但时间上的相关性已经足以提醒工程团队重新审视 AI 服务的可用性假设。
同时出问题,不等于同一个根因
当多个独立服务在同一时间发生故障时,人们很容易直接推断“它们共享了某个基础设施”。这种推断可能成立,但也可能过早。
几个解释都值得保留:
- 三家服务可能依赖同一批云厂商、网络运营商、DNS 服务或上游安全组件。
- 某个区域性的网络事件可能让不同公司的请求同时失败,但各自的内部系统仍然正常。
- 不同服务可能分别发生了发布、路由、容量或流量管理问题,只是故障窗口恰好重叠。
- 用户侧的网络、浏览器、企业代理或身份认证系统也可能放大了“多家服务同时不可用”的感受。
因此,官方状态页只能回答“服务是否知道发生了问题”,未必能回答“整个依赖链为什么同时变得不稳定”。在没有公开完整时间线、影响范围和依赖关系之前,应该把“时间相关性”和“因果关系”分开记录。
对应用团队的直接影响
很多应用已经把大模型调用放在核心路径上:客服回复、代码生成、内容审核、搜索摘要,甚至登录后的关键操作都可能依赖模型接口。一旦开发团队只配置一个模型供应商,服务本身的可用性就会被上游单点放大。
更麻烦的是,简单地把请求切换到另一家供应商并不能自动解决问题。多家供应商可能共享网络、云区域或身份认证依赖;不同模型的上下文窗口、工具调用格式、审核策略和输出结构也不一致。故障转移必须包含协议适配、超时控制和结果降级,而不只是改一个 URL。
一个实用的最低要求是:
- 对每个上游请求设置明确的连接超时、读取超时和总超时。
- 区分可重试错误与不可重试错误,避免在上游故障时制造重试风暴。
- 保留最近一次可接受结果,或切换到模板、规则和人工处理队列。
- 记录供应商、模型、区域、HTTP 状态码、请求耗时和 trace ID。
- 定期演练“主供应商不可用”而不是等到真实事故时才验证备用链路。
先把多家服务的状态记录下来
可以用一个很小的脚本并行检查多个健康检查接口或内部代理接口。下面的示例不假设具体供应商的状态页地址:把环境变量替换成团队实际使用的健康检查 URL 即可。
运行前设置接口地址:
export OPENAI_HEALTH_URL='https://your-gateway.example.com/health/openai'
export ANTHROPIC_HEALTH_URL='https://your-gateway.example.com/health/anthropic'
export GROK_HEALTH_URL='https://your-gateway.example.com/health/grok'
保存为 check-llm-dependencies.sh 并运行:
#!/usr/bin/env bash
set -u
urls=(
"openai|${OPENAI_HEALTH_URL:?missing OPENAI_HEALTH_URL}"
"anthropic|${ANTHROPIC_HEALTH_URL:?missing ANTHROPIC_HEALTH_URL}"
"grok|${GROK_HEALTH_URL:?missing GROK_HEALTH_URL}"
)
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
for item in "${urls[@]}"; do
name="${item%%|*}"
url="${item#*|}"
start="$(date +%s%3N)"
status="$(curl --silent --show-error --location \
--connect-timeout 2 --max-time 8 \
--output /dev/null --write-out '%{http_code}' \
"$url" 2>/dev/null || printf '000')"
end="$(date +%s%3N)"
latency_ms=$((end - start))
printf '%s provider=%s http_status=%s latency_ms=%s\n' \
"$timestamp" "$name" "$status" "$latency_ms"
done
这个检查器只能说明“从某个观测点访问接口是否成功”,不能代替真实的模型调用。生产环境还应增加带有最小提示词的合成测试,并从多个区域运行,避免把本地网络故障误判为供应商故障。
故障转移要保证输出可用
备用模型并不一定能直接替换主模型。一个更稳妥的服务层可以把业务请求先转换为内部统一格式,再由适配器负责调用不同供应商。故障时按照错误类型选择重试、切换或降级:
from dataclasses import dataclass
from typing import Callable
@dataclass
class ModelReply:
text: str
provider: str
degraded: bool = False
def generate(prompt: str, providers: list[tuple[str, Callable[[str], str]]]) -> ModelReply:
last_error = None
for name, call in providers:
try:
text = call(prompt)
if text and text.strip():
return ModelReply(text=text, provider=name)
except TimeoutError as exc:
last_error = exc
continue
except ConnectionError as exc:
last_error = exc
continue
# 业务允许时返回可解释的降级结果,而不是让整个请求无限等待。
if last_error is not None:
return ModelReply(
text="当前无法生成智能回复,请稍后重试或转人工处理。",
provider="fallback",
degraded=True,
)
raise RuntimeError("all providers returned an empty response")
示例中的 providers 可以绑定到不同供应商的 SDK 或内部网关。真正接入时,还需要限制每个请求的总耗时、避免对同一个请求重复产生副作用,并为降级响应设置监控指标。对于有工具调用、写数据库或发送消息的工作流,故障转移尤其需要幂等键,否则同一个任务可能被两个供应商重复执行。
状态页之外,还需要自己的证据
这次事件也说明了状态页的局限性。状态页更新时间可能滞后,描述往往只覆盖供应商确认的范围,而且不同公司的事故分类方式不同。应用团队应该保存自己的观测数据:
- 每分钟成功率和按供应商拆分的错误率。
- DNS、TLS、连接建立、首字节和完整响应的耗时。
- 不同地区、可用区和出口网络的结果。
- 从故障开始到恢复的完整时间线。
- 重试次数、切换次数、降级请求数以及用户可见影响。
这些数据能帮助团队判断问题究竟发生在客户端、企业网络、公共互联网、供应商网关,还是模型服务内部。它们也能避免事故复盘只剩下一句“当时模型不可用”。
采用建议
不需要因为一次同时宕机就立刻维护三套完整模型栈,但应该明确自己的可用性目标。如果 AI 只是辅助功能,缓存和人工兜底通常已经足够;如果 AI 位于交易、客服或运营主流程,就应至少具备超时、降级、可观测性和经过演练的备用路径。
可以用下面的清单做一次检查:
- 是否能在几秒内停止等待失效的上游?
- 是否能区分供应商错误与本地网络错误?
- 备用供应商是否真的经过真实业务请求验证?
- 降级后用户是否仍能完成核心任务?
- 是否能从日志中还原多家服务故障的时间线?
- 是否设置了重试上限、熔断和告警?
三家服务在同一时间出问题,未必意味着存在一个统一的幕后故障。但它足以说明:把多个品牌名称当成完全独立的可用性保障,并不是可靠的架构策略。真正的韧性来自清晰的依赖边界、有限的等待时间、可观测的切换逻辑,以及在上游失效时仍然可用的业务降级方案。