今天(2026-10-15)DeepSeek 服务挂了大约四个小时。我手头依赖工具集体罢工,翻了一下社交媒体,不少开发者和产品团队都在吐槽。
这件事让我想聊几个更深的问题,以及对应的技术解法。
一、世界是个草台班子,但专业公司在对抗熵增
先说一个直觉:任何规模足够大的系统,出故障都是常态,不是例外。
这不针对 DeepSeek。OpenAI 2023 年 11 月那次全线宕机持续了数小时,Hacker News 上有几百条评论在复盘当时的影响 1。Google SRE 手册里有一章专门讲「级联失败」——一个组件过载导致相邻组件也挂掉,像多米诺骨牌一样扩散 2。
所以「世界是草台班子」这个感受是对的。但反过来看:正因为系统天然会坏,才需要专业的工程团队、商业公司和制度设计去做一件事——对抗熵增。你的手机信号大多数时候是通的、你刷的网站大多数时候能打开,不是因为它们不会坏,而是因为有人在设计冗余、监控、恢复流程。
当然也希望“文科程序员”们意识到,有了AI确实可以一句话开发出软件了,但持续稳定的运行才能称之为服务,而这依赖于无数人的努力和坚守。
落到我们写的 AI 应用上,道理一样:你不能假设第三方 API 永远在线。你需要为故障做设计。
二、SaaS 的依赖转移:从自建模型到调用 API
大模型火了之后,一个明显的趋势是:大部分 SaaS 公司不会自己训练和部署模型。
原因很直接:训练一个大模型的成本是千万甚至亿级,推理部署需要 GPU 集群和运维团队。绝大多数公司选择直接调用 OpenAI、Anthropic、DeepSeek 这些厂商的 API 3。
这意味着什么?意味着 SaaS 产品的核心功能——以前可能是自己写的业务逻辑——现在有相当一部分依赖外部服务。你的 app 能不能用,不只取决于你的代码,还取决于第三方 API 能不能正常响应。
三、鲁棒性:系统在故障下还能跑吗?
鲁棒性(robustness)的意思很朴素:系统在遇到问题时,是直接崩掉,还是能降级运行?
放到 AI 调用的场景里,鲁棒性至少要回答这几个问题:
- API 偶尔超时或返回 5xx,你会重试吗?重试几次?
- API 完全挂了,你能切到另一家厂商吗?切换对业务代码透明吗?
- 所有厂商都挂了,你的产品能给出一个可预期的降级结果,而不是抛异常吗?
这些问题的答案不应该散落在每个调用点,而应该有统一的机制。
3.1 错误分类学:先搞清楚「坏」有几种坏
第一步不是写重试逻辑,而是先把错误分类。因为不同的错误,处理方式完全不同。
flowchart LR
A[API 调用失败] --> B{什么类型的错误?}
B -->|瞬时故障<br/>5xx/429/断网| C[TransportError<br/>可重试]
B -->|确定性失败<br/>401/402/参数错| D[SemanticError<br/>不可重试]
B -->|响应格式不对<br/>缺字段/维度错| E[ContractError<br/>不可重试]
B -->|输出无法解析<br/>JSON 坏/枚举越界| F[ParseError<br/>业务层重试]
C --> G[传输层指数退避重试]
D --> H[直接报错,找根因]
E --> H
F --> I[带错误提示再试一次]四种基本错误类型:
- TransportError:网络抖动、服务端 5xx、限流 429。这类错误可以重试——过一会儿再发可能就好了。
- SemanticError:鉴权失败(401)、余额不足(402)、请求参数错误(400/422)。重试没有意义,再试也是同样的结果。
- ContractError:返回的响应不符合约定——比如你请求了 10 条 embedding,结果只回来了 8 条。这说明要么你请求写错了,要么服务端版本不匹配,重试没用。
- ParseError:模型返回的内容解析失败(比如让它输出 JSON 结果格式不对)。这种错误的重试不该在传输层做,而应该在业务层——把解析错误的信息一起发回去,让模型修正。
分类的价值在于:谁负责重试,重试几次,策略是什么——一次性说清楚。不会出现每个调用点自己写 while 循环、自己判断状态码的混乱局面。
Vercel AI SDK 的设计就是把 isRetryable 这种判断放在错误类的构造期推导,而不是让调用方自己写字符串判断 4。
3.2 重试的纪律
重试不是越多越好。Google SRE 书里专门警告过「重试叠乘」的问题:如果你的服务有 4 层,每层都重试 4 次,一次失败就会变成 4³ = 64 次尝试,反而会加重故障 2。
几条基本规则:
- 重试只在传输层做,业务层不要自己写重试循环。
- 指数退避 + 抖动(jitter):第一次等 1 秒,第二次 2 秒,第三次 4 秒,并且每次加一个随机偏移,避免所有客户端同时重试(惊群效应)。
- 尊重 Retry-After 头:服务端说 30 秒后再试,就老老实实等 30 秒。
- 重试有上限:通常 2-3 次就够了,再多就是浪费时间和配额。
3.3 故障转移:一家挂了切另一家
重试只能解决瞬时故障。如果一家厂商彻底挂了几小时,你需要能切到另一家。
这就是「故障转移」(failover)——维护一个有序的候选列表,第一个失败了试第二个,第二个失败了试第三个。
sequenceDiagram
participant App as 业务代码
participant Router as 选源层
participant P1 as 厂商 A (主)
participant P2 as 厂商 B (备)
App->>Router: chat("你好")
Router->>P1: 发送请求
P1-->>Router: 503 Service Unavailable
Router->>Router: 标记 P1 冷却 60s
Router->>P2: 发送请求
P2-->>Router: 200 OK + 响应
Router-->>App: 返回响应但故障转移有几个容易踩的坑:
坑一:什么错误都切换。 参数错误(400)换哪家厂商都会失败,白付延迟。LiteLLM 和 Portkey 的实现都有明确的「切换白名单」——只有特定类型的错误才触发切换 56。
坑二:冷却唯一候选源。 如果你只有一家厂商,就不该有熔断/冷却逻辑——单源的情况下,故障转移没有意义,快速失败走降级就好。
坑三:流式输出已经开始了还切换。 流式响应如果已经吐出了一半内容,切换到另一家厂商只会产生重复计费和拼接残流。主流实现都是「首内容块后不切换」5。
四、可扩展性:模型换代这么快,你改得起吗?
鲁棒性解决的是「服务挂了怎么办」。可扩展性解决的是「出了新模型 / 换了厂商,你要改多少代码」。
一个典型的场景:你一开始只用 OpenAI 的 gpt-4o-mini,后来想加一个 DeepSeek 的便宜档,再后来又想加 Anthropic 的高质量档。每加一家,你要改多少代码?
如果你的代码里到处都是 openai.ChatCompletion.create(...),那每加一家厂商都是灾难——每个调用点都要改。
好的架构应该做到:
| 变化 | 目标成本 |
|---|---|
| 换同协议的另一家厂商 | 改配置,0 行代码 |
| 同厂商加一个新模型 | 注册表加一行 |
| 接入一种新协议(比如决策问答) | 写一个协议族子类 |
| 接入一种新能力(比如向量化) | 复制一套已知模式 |
4.1 分层架构:把变化关在自己的层里
核心思路是分层。每一层只管自己的事,变化只在自己那一层发生。
graph TB
L3["L3 业务层<br/>写业务逻辑,选档位"]
L2["L2 能力域层<br/>统一接口 + 协议族 + 厂商实现"]
L1["L1 传输脊<br/>HTTP 客户端 · 重试 · 错误分类 · 计量"]
L0["L0 横切面<br/>配置 · 版本键"]
L3 -->|"只调用统一接口"| L2
L2 -->|"只用传输端口"| L1
L0 -.-> L3
L0 -.-> L2从上到下解释:
L3 业务层:你的业务代码。它只知道「我要一个对话能力」或「我要一个向量化能力」,不知道具体是哪家厂商、什么模型。它调用统一的接口,传业务参数。
L2 能力域层:按能力划分(对话、向量化、重排……),每种能力有一个统一的抽象基类(ABC)。同一种能力下,不同的协议族(比如 OpenAI 兼容协议、厂商自有协议)各有一个基类,具体厂商再继承协议族基类。
理想情况下,厂商子类应该只剩配置——base_url、api_key、模型名。如果一个厂商子类里有大量业务逻辑,说明层次没切对。
L1 传输脊:所有能力共享同一套 HTTP 客户端、重试逻辑、错误分类、计量。代码库里不应该出现第二份重试循环。
L0 横切面:配置读取、版本键管理这种全局的东西,放在最底层,所有人都能用,但传输脊和能力域不直接读环境变量——配置从上层传进来。
Vercel AI SDK 就是按能力切接口的典型:languageModel、embeddingModel、rerankModel 各是各的接口,厂商分别实现 7。
4.2 注册表模式:加新厂商不用改调用代码
「注册表」(registry)是一个很简单但很有用的模式。你维护一个字典,名字映射到实现类。业务代码用名字来选 provider,不用 import 具体的类。
用 Python 写出来大概是这样:
# chat/registry.py
_chat_providers: dict[str, type[ChatProvider]] = {}
def register(name: str):
"""类装饰器:把厂商实现注册进来"""
def decorator(cls):
_chat_providers[name] = cls
return cls
return decorator
def get_provider(name: str) -> ChatProvider:
if name not in _chat_providers:
raise ValueError(f"未知 provider: {name}")
return _chat_providers[name]()
# chat/deepseek.py
from .registry import register
from .openai_family import OpenAIFamilyChat
@register("deepseek")
class DeepSeekChat(OpenAIFamilyChat):
base_url = "https://api.deepseek.com/v1"
default_model = "deepseek-chat"
# 业务代码
from externals.chat.registry import get_provider
provider = get_provider("deepseek")
result = provider.chat("你好")
加新厂商时,你只需要写一个新文件,用装饰器注册一下。业务代码完全不用改。simonw/llm 这个 CLI 工具的插件机制用的就是类似的思路 8。
4.3 版本键:换了模型,旧结果不能混用
这是一个容易被忽略但后果严重的问题。
假设你用 embedding 模型 A 生成了一批向量,存在向量数据库里做检索。后来你换成了模型 B,向量维度或者向量空间分布不一样了——这时候新旧向量混在一起,检索质量会无声地下降,而且你可能很久都发现不了。
Qdrant 的官方迁移文档专门讲这个问题:embedding 模型的结果不可互换,换模型必须带版本标识 9。
解决方法很简单:所有「换了生成源就不可比」的产物——向量、缓存、提示词模板输出——都带一个 model_version 字段。版本不匹配视同缺失,逐步用新模型重新生成。
class ItemVector(Base):
__table_args__ = (UniqueConstraint("item_id", "model_version"),)
model_version: Mapped[str] # 比如 "bge-m3" 或 "text-embedding-3-small"
vector: Mapped[list[float]]
这个版本槽位必须第一天就留。等你以后想换模型了才发现没留,就只能全量重嵌——数据量大的话,成本和时间都很可观。
五、回到今天的宕机事件
如果今天你的应用因为 DeepSeek 挂了而完全不可用,说明什么?
- 你可能没有错误分类——超时和参数错误混在一起处理
- 你可能没有重试机制——一次失败就直接报错给用户
- 你可能只有一家厂商——没有备选方案
- 你可能把厂商 API 调用散落在业务代码里——想加备选也不知道从哪改
这些都不是今天改明天就能好的事。但好消息是,你可以从小处开始:
- 先把错误分类做了:把所有外部调用的异常统一归到几类里
- 再加一个统一的 HTTP 客户端:重试和脱敏只写一份
- 再抽一个统一的对话接口:业务代码不再直接调用厂商 SDK
- 最后再加第二家厂商做备选:配置里切一下就能用
Google SRE 手册里有一句话说得好:「不用的代码路径 = 不工作的代码路径」10。故障转移这种东西,只有真正演练过,你才知道它能不能用。
参考引用
- Hacker News: OpenAI is down (2023-11-08). https://news.ycombinator.com/item?id=38190401 ↩︎
- Google SRE Book: Addressing Cascading Failures. https://sre.google/sre-book/addressing-cascading-failures/ ↩︎
- Anthropic Engineering: How we built our multi-agent research system (2025-06-13). https://www.anthropic.com/engineering/multi-agent-research-system ↩︎
- Vercel AI SDK: AI SDK Errors – AIAPICallError. https://ai-sdk.dev/docs/reference/ai-sdk-errors#ai-api-call-error ↩︎
- LiteLLM Router source code (commit 2211fa5f3f27). https://github.com/BerriAI/litellm/blob/2211fa5f3f27/litellm/router.py ↩︎
- Portkey AI Gateway source code (commit 669825cb). https://github.com/Portkey-AI/gateway/blob/669825cbe89ee51569918b8f78a9db486fd69dd4/src/handlers/handlerUtils.ts ↩︎
- Vercel AI SDK: Provider Management. https://ai-sdk.dev/docs/ai-sdk-core/provider-management ↩︎
- Simon Willison: llm, a CLI utility and Python library for interacting with Large Language Models. https://simonwillison.net/2023/Jul/12/llm/ ↩︎
- Qdrant Documentation: Migrate to a New Embedding Model with Zero Downtime. https://qdrant.tech/documentation/tutorials-operations/embedding-model-migration ↩︎
- Google SRE Workbook: Managing Load. https://sre.google/workbook/managing-load/ ↩︎




发表回复