一句话定义:Graceful Degradation 不是"失败时报错",是"失败时给用户一个质量递减但仍可用的答案"。
为什么你现在就需要想清楚降级
凌晨 2:17,你的 LLM 服务商宣布维护窗口,预计 40 分钟。
你的 AI 写作助手有 6,000 个在线用户。你的选择是:
- 返回 500,用户一脸懵
- 显示"服务暂时不可用,请稍后再试"
- 继续给用户提供一个质量稍差、但仍然有用的响应
第三条路就是 Graceful Degradation。
但它不是一个简单的 if-else。做不好,你会面临三个陷阱:
- 降级质量太差:用户宁愿等,也不要一个明显是模板的答案
- 降级成本比主路径更贵:某些 fallback 方案在大流量下比主模型更烧钱
- 降级监控缺失:你根本不知道你在降级,也不知道降了多少
这篇文章给你一个可落地的 5 层降级架构,从最高质量到最保底的静态响应,每一层都有实际代码和触发条件。
第一层:模型切换(Model Fallback)
触发条件
- 主模型 HTTP 5xx / 超时(>= 10s)
- 主模型 rate limit 429
- 主模型 context length 超限
架构设计
typescript
// fallback-chain.ts
interface ModelConfig {
provider: string;
model: string;
maxTokens: number;
timeoutMs: number;
costPerKToken: number;
}
const FALLBACK_CHAIN: ModelConfig[] = [
{
provider: "deepseek",
model: "deepseek-chat",
maxTokens: 8192,
timeoutMs: 30000,
costPerKToken: 0.14,
},
{
provider: "qwen",
model: "qwen-plus",
maxTokens: 4096,
timeoutMs: 15000,
costPerKToken: 0.08,
},
{
provider: "zhipu",
model: "glm-4-flash",
maxTokens: 4096,
timeoutMs: 20000,
costPerKToken: 0.05,
},
];
async function callWithFallback(
messages: Message[],
options: CallOptions = {}
): Promise<LLMResponse> {
const errors: Error[] = [];
for (const config of FALLBACK_CHAIN) {
try {
const result = await callModel(config, messages, options);
// 记录降级事件
if (config !== FALLBACK_CHAIN[0]) {
metrics.increment("llm.fallback.used", {
from: FALLBACK_CHAIN[0].model,
to: config.model,
reason: errors[0]?.constructor.name,
});
}
return { ...result, degradedTo: config.model };
} catch (err) {
errors.push(err as Error);
// 非降级性错误直接抛出(如认证失败)
if (isNonRetryableError(err)) throw err;
logger.warn(`Model ${config.model} failed, trying next`, {
error: (err as Error).message,
attempt: errors.length,
});
}
}
throw new AllModelsFailedError(errors);
}
function isNonRetryableError(err: unknown): boolean {
const status = (err as any)?.status;
// 401/403 不重试,是配置问题
return status === 401 || status === 403;
}
关键细节:同步 vs 异步 fallback
同步 fallback(串行尝试) 适合单次请求场景,延迟叠加但实现简单。
异步 hedge(并行发出,取最快的) 适合对延迟极度敏感的场景,但成本加倍:
typescript
async function hedgedCall(
messages: Message[],
hedgeAfterMs = 2000
): Promise<LLMResponse> {
// 先发主模型
const primaryPromise = callModel(FALLBACK_CHAIN[0], messages);
// 2 秒后如果主模型还没回,同时发备用
const hedgePromise = sleep(hedgeAfterMs).then(() =>
callModel(FALLBACK_CHAIN[1], messages)
);
const result = await Promise.any([primaryPromise, hedgePromise]);
// 赢者取全,输者的请求继续跑完但结果被丢弃
// 注意:这里有成本浪费,需要评估值不值
return result;
}
实测数据(基于我们自己的生产系统,p99 延迟改善):
| 策略 | p50 延迟 | p99 延迟 | 成本倍数 |
|---|---|---|---|
| 串行 fallback(超时触发) | +0ms | +12s(超时叠加) | 1.0x |
| 并行 hedge(2s 触发) | +0ms | +2.1s | 1.6x(压力下) |
| 并行 hedge(500ms 触发) | +0ms | +0.5s | 2.1x |
结论:hedge 不是免费的,只在 p99 > 5s 已经影响业务的情况下才值得。
第二层:Semantic Cache 命中(缓存降级)
当所有模型都挂了,或者你不想再花 API 费用,可以尝试从语义缓存里找一个"足够接近"的历史响应。
工作原理
markdown
用户请求 → Embedding → 向量搜索 → 相似度 > 阈值 → 返回缓存结果
↓ 否
调用 LLM(正常路径)
实现
python
# semantic_cache.py
import numpy as np
from typing import Optional
class SemanticCache:
def __init__(
self,
embedding_model: EmbeddingModel,
vector_store: VectorStore,
similarity_threshold: float = 0.92, # 关键参数
max_age_hours: int = 24,
):
self.embedding_model = embedding_model
self.vector_store = vector_store
self.similarity_threshold = similarity_threshold
self.max_age_hours = max_age_hours
async def get(self, query: str) -> Optional[CachedResponse]:
query_embedding = await self.embedding_model.embed(query)
results = await self.vector_store.search(
vector=query_embedding,
top_k=3,
filter={"created_at": {"$gte": self._cutoff_timestamp()}},
)
if not results:
return None
best = results[0]
# 相似度阈值是核心调参点
if best.score < self.similarity_threshold:
metrics.increment("semantic_cache.miss.below_threshold", {
"score": round(best.score, 2)
})
return None
metrics.increment("semantic_cache.hit", {"score": round(best.score, 2)})
return CachedResponse(
content=best.payload["response"],
cache_score=best.score,
original_query=best.payload["query"],
is_degraded=True, # 标记:这是降级响应
)
async def set(self, query: str, response: str, metadata: dict = {}):
embedding = await self.embedding_model.embed(query)
await self.vector_store.upsert(
vector=embedding,
payload={
"query": query,
"response": response,
"created_at": time.time(),
**metadata,
},
)
阈值怎么选?
这是语义缓存的核心调参问题。太高:命中率低,降级价值有限。太低:用户收到牛头不对马嘴的答案。
python
# 建议的阈值校准方法:用你的历史 QA 对做测试
def calibrate_threshold(qa_pairs: list[tuple[str, str]], embedding_model) -> float:
"""
对 1000 对历史 QA,计算「换了说法的同一问题」的 embedding 相似度分布。
选取 P10 作为阈值(90% 的同语义问题能命中)。
"""
same_intent_scores = []
for q1, q2 in generate_paraphrases(qa_pairs):
e1 = embedding_model.embed(q1)
e2 = embedding_model.embed(q2)
score = cosine_similarity(e1, e2)
same_intent_scores.append(score)
# 使用 P10 作为阈值
threshold = np.percentile(same_intent_scores, 10)
print(f"建议阈值:{threshold:.3f}")
return threshold
我们的实测结果(5 万条生产 query 校准):
- 相似度分布呈双峰:同语义问题集中在 0.93-0.99,不同问题集中在 0.5-0.75
- 选 0.92 作为阈值,误命中率 < 0.3%,命中率约 18%(纯降级流量下)
第三层:规则引擎降级(Deterministic Fallback)
有一类问题不需要 LLM 也能回答好:有固定答案的问题。
typescript
// rule-engine.ts
interface Rule {
pattern: RegExp | string[];
response: string | ((match: RegExpMatchArray) => string);
confidence: number; // 0-1,用于记录日志和告警
}
const RULES: Rule[] = [
{
// 问候类
pattern: /^(你好|hello|hi|嗨|早上好|下午好|晚上好)/i,
response: "你好!我是 AI 助手,有什么可以帮你的?",
confidence: 0.99,
},
{
// 产品价格(从数据库/配置读取,不依赖 LLM)
pattern: /价格|多少钱|收费/,
response: async () => {
const pricing = await pricingService.getCurrent();
return `当前价格:基础版 ¥${pricing.basic}/月,专业版 ¥${pricing.pro}/月。`;
},
confidence: 0.95,
},
{
// 用户账户状态(直接查数据库)
pattern: /我的账户|余额|到期/,
response: async (_, userId: string) => {
const account = await accountService.get(userId);
return `您的账户余额:¥${account.balance},有效期至 ${account.expiresAt}。`;
},
confidence: 0.98,
},
];
async function tryRuleEngine(
query: string,
context: RequestContext
): Promise<RuleResponse | null> {
for (const rule of RULES) {
const match = matchRule(query, rule.pattern);
if (!match) continue;
const response =
typeof rule.response === "function"
? await rule.response(match, context.userId)
: rule.response;
metrics.increment("rule_engine.matched", {
rule_id: rule.pattern.toString().slice(0, 20),
});
return {
content: response,
is_degraded: true,
degradation_tier: "rule_engine",
confidence: rule.confidence,
};
}
return null;
}
规则引擎的适用场景:
- FAQ 类问题(高频 + 固定答案)
- 涉及实时数据的查询(账户、订单、价格)
- 安全边界检查(某些问题永远不应该走 LLM)
不适用:
- 开放式创作
- 需要上下文推理的问题
- 答案随时间快速变化的领域
第四层:模板响应(Template Degradation)
规则引擎匹配不上,但你仍然知道用户在问什么类别的问题------这时可以返回一个带占位符的模板响应。
python
# template_degradation.py
from enum import Enum
from dataclasses import dataclass
class IntentCategory(Enum):
WRITING_HELP = "writing_help"
CODE_REVIEW = "code_review"
TRANSLATION = "translation"
SUMMARIZATION = "summarization"
UNKNOWN = "unknown"
TEMPLATES = {
IntentCategory.WRITING_HELP: """
抱歉,AI 写作助手暂时无法提供完整服务。
**当前可用的临时方案:**
1. 您可以先写出草稿框架,稍后我们恢复后帮您润色
2. 参考我们的写作指南:[链接]
3. 预计恢复时间:{eta}
我们已记录您的需求,服务恢复后将优先处理。
""",
IntentCategory.CODE_REVIEW: """
代码审查服务暂时受限。建议:
- 使用 ESLint / SonarQube 进行自动化检查
- 参考 {language} 最佳实践文档
- 预计 {eta} 内恢复完整服务
""",
IntentCategory.SUMMARIZATION: """
文本摘要服务暂时不可用。
您可以:
1. 手动提取关键段落(通常文章前 1/4 包含主要观点)
2. 使用关键词搜索定位重要内容
3. {eta} 后重试
""",
IntentCategory.UNKNOWN: """
AI 服务暂时受限,预计 {eta} 恢复。
对于您的请求,建议:
- 先保存您的输入内容
- 稍后重试,或联系客服获取帮助
"""
}
class IntentClassifier:
"""轻量分类器,不依赖 LLM,用关键词 + TF-IDF"""
def __init__(self):
self.keyword_map = {
IntentCategory.WRITING_HELP: ["写", "文章", "润色", "改写", "创作"],
IntentCategory.CODE_REVIEW: ["代码", "bug", "review", "审查", "调试"],
IntentCategory.TRANSLATION: ["翻译", "translate", "英文", "中文"],
IntentCategory.SUMMARIZATION: ["总结", "摘要", "summarize", "概括"],
}
def classify(self, text: str) -> IntentCategory:
scores = {}
for intent, keywords in self.keyword_map.items():
score = sum(1 for kw in keywords if kw in text)
scores[intent] = score
best = max(scores, key=scores.get)
return best if scores[best] > 0 else IntentCategory.UNKNOWN
def get_template_response(query: str, eta: str = "30 分钟") -> str:
classifier = IntentClassifier()
intent = classifier.classify(query)
template = TEMPLATES.get(intent, TEMPLATES[IntentCategory.UNKNOWN])
return template.format(
eta=eta,
language=detect_language(query),
)
模板响应的原则:
- 承认降级,不要伪装成正常响应
- 给用户一个 ETA(即使是估算的)
- 提供可操作的替代方案
- 记录用户的原始请求,服务恢复后主动推送结果
第五层:完全静态响应(Static Fallback)
最后一道防线。所有 LLM、缓存、规则都挂了。
typescript
// static-fallback.ts
interface StaticFallback {
message: string;
retryAfterSeconds?: number;
statusPageUrl?: string;
supportContact?: string;
}
const STATIC_FALLBACK: StaticFallback = {
message:
"AI 服务当前不可用,我们正在紧急修复。您的请求已记录,服务恢复后会通知您。",
retryAfterSeconds: 300,
statusPageUrl: "https://status.yourapp.com",
supportContact: "support@yourapp.com",
};
async function staticFallback(
request: LLMRequest,
context: RequestContext
): Promise<Response> {
// 尝试将请求入队(非阻塞)
try {
await offlineQueue.enqueue(
{
request,
userId: context.userId,
timestamp: Date.now(),
},
{ timeout: 500 }
);
} catch {
metrics.increment("static_fallback.queue_also_down");
}
return {
status: 503,
body: STATIC_FALLBACK,
headers: {
"Retry-After": String(STATIC_FALLBACK.retryAfterSeconds),
"X-Degradation-Tier": "static",
},
};
}
把 5 层串起来:统一降级编排器
typescript
// degradation-orchestrator.ts
type DegradationTier = "primary" | "model_fallback" | "semantic_cache" | "rule_engine" | "template" | "static";
interface DegradedResponse {
content: string;
tier: DegradationTier;
latencyMs: number;
confidence?: number;
cacheScore?: number;
}
class DegradationOrchestrator {
constructor(
private readonly semanticCache: SemanticCache,
private readonly ruleEngine: RuleEngine,
private readonly modelFallback: ModelFallbackChain,
private readonly config: OrchestratorConfig
) {}
async handle(
request: LLMRequest,
context: RequestContext
): Promise<DegradedResponse> {
const startTime = Date.now();
const {
enableSemanticCache = true,
enableRuleEngine = true,
enableTemplates = true,
primaryTimeoutMs = 30000,
} = this.config;
// 第 1 层:正常路径 + 模型 fallback
try {
const result = await this.modelFallback.call(
request.messages,
{ timeoutMs: primaryTimeoutMs }
);
return {
content: result.content,
tier: result.degradedTo ? "model_fallback" : "primary",
latencyMs: Date.now() - startTime,
};
} catch (err) {
logger.error("All models failed", { error: err });
metrics.increment("degradation.all_models_failed");
}
// 第 2 层:语义缓存
if (enableSemanticCache) {
try {
const cached = await this.semanticCache.get(
extractLastUserMessage(request.messages)
);
if (cached) {
return {
content: this.wrapDegradedContent(cached.content, "缓存响应"),
tier: "semantic_cache",
latencyMs: Date.now() - startTime,
cacheScore: cached.cacheScore,
};
}
} catch (err) {
logger.warn("Semantic cache failed", { error: err });
}
}
// 第 3 层:规则引擎
if (enableRuleEngine) {
const query = extractLastUserMessage(request.messages);
const ruleResult = await this.ruleEngine.match(query, context);
if (ruleResult) {
return {
content: ruleResult.content,
tier: "rule_engine",
latencyMs: Date.now() - startTime,
confidence: ruleResult.confidence,
};
}
}
// 第 4 层:模板响应
if (enableTemplates) {
const query = extractLastUserMessage(request.messages);
const eta = await this.estimateRecoveryEta();
return {
content: getTemplateResponse(query, eta),
tier: "template",
latencyMs: Date.now() - startTime,
};
}
// 第 5 层:静态降级
const staticResult = await staticFallback(request, context);
return {
content: staticResult.body.message,
tier: "static",
latencyMs: Date.now() - startTime,
};
}
private wrapDegradedContent(content: string, label: string): string {
return content;
}
private async estimateRecoveryEta(): Promise<string> {
try {
const status = await statusMonitor.getIncidentEta();
return status?.eta ?? "30 分钟";
} catch {
return "30 分钟";
}
}
}
降级监控:你必须知道你在降级
降级监控是最容易被忽视的部分。我见过不少团队,降级逻辑写了,但完全不知道降级在生产中有多频繁。
必须追踪的指标
typescript
// 1. 降级率(按层分)
metrics.gauge("degradation.rate_by_tier", {
tier: response.tier,
value: 1,
});
// 2. 降级持续时长
metrics.histogram("degradation.duration_minutes", {
tier: response.tier,
value: incidentDurationMinutes,
});
// 3. 语义缓存命中率和相似度分布
metrics.histogram("semantic_cache.hit_score", {
value: cached.cacheScore,
buckets: [0.88, 0.90, 0.92, 0.95, 0.98, 1.0],
});
// 4. 用户行为指标(降级期间的留存 vs 离开)
metrics.track("user.action_during_degradation", {
tier: response.tier,
action: userAction, // "retry" | "exit" | "continue"
});
告警阈值建议
| 指标 | 告警阈值 | 级别 |
|---|---|---|
| 主模型失败率 | > 1% (5min 内) | Warning |
| 进入 semantic_cache 层 | > 5% (5min 内) | Warning |
| 进入 template 层 | > 1% (5min 内) | Critical |
| 进入 static 层 | 任何 1 次 | Critical |
| 语义缓存命中率跌破 10% | - | Info(缓存冷却) |
Dashboard 设计
一个好的降级 Dashboard 应该在一屏内回答:
- 现在在第几层降级?(大字显示当前 p95 降级层)
- 降级影响多少用户?(受影响请求数/总请求数)
- 什么时候开始的?(降级事件时间轴)
- 预期什么时候结束?(ETA from 服务状态页)
python
# 伪代码:Grafana Dashboard 数据源查询
SELECT
tier,
count(*) as requests,
count(*) / sum(count(*)) OVER () as percentage
FROM degradation_events
WHERE timestamp > now() - interval '5 minutes'
GROUP BY tier
ORDER BY
CASE tier
WHEN 'primary' THEN 1
WHEN 'model_fallback' THEN 2
WHEN 'semantic_cache' THEN 3
WHEN 'rule_engine' THEN 4
WHEN 'template' THEN 5
WHEN 'static' THEN 6
END
5 个真实踩坑
坑 1:没有给降级响应打标记
现象:语义缓存命中的响应被用户截图传播,但内容是 3 天前的旧答案,引发舆情。
修复 :所有降级响应必须带 is_degraded: true 的 flag,UI 层根据这个 flag 决定是否显示"此响应来自缓存"的角标。
typescript
headers: {
"X-Response-Source": response.tier,
"X-Cache-Score": response.cacheScore?.toFixed(2),
"X-Degraded": response.tier !== "primary" ? "1" : "0",
}
坑 2:Semantic Cache 阈值太低导致"幻觉缓存"
现象:用户问"明天的天气怎么样",缓存命中了"今天的天气"的历史响应(相似度 0.89)。
修复:对时间敏感类问题,在 embedding 之前过滤,或强制绕过缓存:
python
TIME_SENSITIVE_PATTERNS = [
r"今天|明天|现在|最新|当前|今年|本月|昨天",
r"today|tomorrow|now|current|latest|yesterday",
r"最近|近期|刚刚|刚才",
]
def should_bypass_cache(query: str) -> bool:
return any(
re.search(pattern, query, re.IGNORECASE)
for pattern in TIME_SENSITIVE_PATTERNS
)
坑 3:Fallback 模型成本比主模型更贵
现象:主模型超时,fallback 到一个调用更频繁的备用模型,在流量高峰期的 fallback 成本是主模型的 3 倍。
根因:Fallback 链设计时只考虑了可用性,没考虑成本。每次 fallback 的成本 × fallback 频率才是真实成本。
修复:在 fallback 链中设置每日成本上限:
typescript
class CostAwareFallback {
private dailyFallbackCost = 0;
private readonly maxDailyFallbackCost = 100; // USD
async callWithCostGuard(messages: Message[]): Promise<LLMResponse> {
if (this.dailyFallbackCost > this.maxDailyFallbackCost) {
throw new CostLimitExceededError();
}
const result = await this.call(messages);
this.trackCost(result);
return result;
}
}
坑 4:规则引擎覆盖了 LLM 应该回答的问题
现象:规则引擎匹配到"价格"关键词,返回了固定价格,但用户实际上在问竞品的价格对比分析。
修复:规则引擎只在降级路径激活,不作为主路径的前置过滤器。并且规则匹配条件要足够精确:
python
# 错误:过于宽泛
pattern: "价格"
# 正确:精确匹配
pattern: r"你们(的)?价格|产品定价|收费标准|费用是多少"
坑 5:没有"降级退出"机制
现象:主模型恢复了,但流量仍然因为旧的超时 flag 停留在 semantic_cache 层,持续 20 分钟。
修复:降级状态必须有自动恢复机制,不能靠手动切换:
typescript
class DegradationStateManager {
private currentTier: DegradationTier = "primary";
private lastHealthCheckAt = 0;
async getCurrentTier(): Promise<DegradationTier> {
const now = Date.now();
// 每 30 秒做一次主路径健康探测
if (now - this.lastHealthCheckAt > 30_000) {
this.lastHealthCheckAt = now;
const isHealthy = await this.probe();
if (isHealthy && this.currentTier !== "primary") {
logger.info("Primary recovered, exiting degraded mode", {
from: this.currentTier,
});
this.currentTier = "primary";
metrics.increment("degradation.auto_recovery");
}
}
return this.currentTier;
}
private async probe(): Promise<boolean> {
try {
await callModel(FALLBACK_CHAIN[0], [
{ role: "user", content: "ping" }
], { timeoutMs: 5000 });
return true;
} catch {
return false;
}
}
}
架构总结:5 层降级速查表
| 层级 | 触发条件 | 典型延迟 | 质量 | 成本 |
|---|---|---|---|---|
| 主模型 | 正常 | p50: 2-5s | ★★★★★ | 基准 |
| 模型 fallback | 主模型 5xx/超时/429 | p50: 3-8s | ★★★★☆ | 0.5-2x |
| Semantic Cache | 所有模型失败 / 成本熔断 | p50: 50-200ms | ★★★☆☆ | 极低 |
| 规则引擎 | 缓存未命中 + 规则可匹配 | p50: 1-10ms | ★★★★☆(特定场景) | 极低 |
| 模板响应 | 规则未匹配 | p50: <1ms | ★★☆☆☆ | 无 |
| 静态响应 | 全部失败 | p50: <1ms | ★☆☆☆☆ | 无 |
最后
Graceful Degradation 本质上是一个工程品位问题:你对用户体验的尊重程度,决定了你在系统故障时愿意付出多少额外的工程成本来维持一个"还能用"的体验。
三个核心原则:
- 每层降级都要有监控:不知道自己在第几层,就是裸奔
- 降级要有出口:30 秒健康探测,自动恢复,不要靠人工切换
- 语义缓存是双刃剑:阈值设对了是救命药,设错了是毒药
从这 5 层里选你现在最缺的那一层先做,比什么都不做强。
本文代码示例基于生产系统提炼,TypeScript 和 Python 片段均可直接集成。如有问题,评论区见。