Anthropic 刚发布了一份事后复盘,把过去六周里 Claude Code 用户抱怨质量下降的根因拆得清清楚楚——不是模型变笨了,而是三个产品层改动在同一时段叠加,像三把刀同时扎进同一条神经。API 和模型权重从头到尾没动过,但用户体验硬生生掉了 3% 以上,直到 4 月 20 日才全部修复。
这件事值得每个做 LLM 产品的团队细看:你以为改的是配置,实际改的是模型"能想多深"。
三条故障链,各自致命
1. 推理努力等级被悄悄降档
Claude Code 的产品层有一个"推理努力"(reasoning effort)参数,控制模型在回答前花多少内部思考步骤。某次产品更新把这个参数从默认值往下调了一档——理由可能是节省延迟或成本,但后果是模型在复杂任务上明显"想不够"就急着输出。
用户感知:代码补全变浅、多步推理任务出错率上升。
2. 缓存 Bug:模型越想越少
更诡异的是第二个问题。Claude Code 使用上下文缓存来加速多轮对话,但缓存实现有一个 Bug:在缓存命中时,模型之前生成的内部推理内容(thinking tokens)会被逐步截断甚至抹掉。换句话说——
模型在第一轮想了 2000 tokens,第二轮缓存复用时只剩 500 tokens,第三轮可能只剩 100 tokens。
这不是"忘了之前说了什么",而是"忘了之前怎么想的"。推理链条被物理删除,模型只能基于残缺的思维过程继续工作,质量自然雪崩。
3. 系统提示词长度上限砍掉 3%
第三个改动是对系统提示词加了一个 verbosity limit(冗长度上限),超过上限的内容会被截断。这个截断直接导致约 3% 的质量下降——因为被砍掉的不是废话,而是对模型行为有约束力的关键指令。
三个问题各自造成的质量损失可能都不算灾难级,但它们在同一时段同时生效,用户感受到的是叠加效应。
为什么六周才定位?
复盘里最值得警惕的不是 Bug 本身,而是定位时间:
- 产品层改动与 API/模型解耦——模型权重没变,API 行为没变,所以常规的模型评估全部通过,没人报警。
- 缓存 Bug 是渐进式的——不是"突然坏了",而是"越用越差",用户抱怨分散在六周里,没有单一故障时间点。
- 3% 的质量下降在统计噪声里几乎不可见——除非你跑大规模 A/B 测试或专门针对复杂任务做评测,日常指标很难捕捉。
这三条合在一起,就是经典的"温水煮青蛙"场景。
实战:如何在自己的 LLM 产品里防住这类问题
下面给一个可运行的 Python 脚本,演示如何用结构化评测检测"推理深度退化"和"系统提示词截断"两类问题。思路是:用同一套测试题,分别在不同配置下跑,对比结果差异。
"""
quality_drift_detector.py
检测 LLM 产品层配置变化是否导致质量漂移
依赖:anthropic Python SDK (pip install anthropic)
运行前设置环境变量:export ANTHROPIC_API_KEY=your_key
"""
import os
import json
from anthropic import Anthropic
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
# 测试题:需要多步推理的编程任务,对推理深度敏感
TEST_PROMPTS = [
{
"id": "multi_step_debug",
"prompt": "给定这段 Python 代码,找出所有 bug 并逐一解释原因和修复方案:\n"
"def merge_dicts(a, b):\n return {k: a[k] for k in a if k not in b} | b",
"expected_depth": 3, # 期望至少识别 3 个独立问题
},
{
"id": "refactor_plan",
"prompt": "把一个 500 行的单文件 Flask 应用重构为模块化结构,"
"给出目录布局、文件职责划分和迁移步骤。",
"expected_depth": 4, # 期望至少 4 个步骤
},
]
# 两种系统提示词:完整版 vs 截断版(模拟 verbosity limit)
SYSTEM_FULL = (
"你是一位资深软件工程师。回答时必须:"
"1. 先分析问题结构再给出方案;"
"2. 对每个建议说明理由和潜在风险;"
"3. 如果有多种方案,比较优劣;"
"4. 给出可直接运行的代码示例。"
)
SYSTEM_TRUNCATED = "你是一位资深软件工程师。回答时要分析问题再给方案。" # 模拟截断后只剩一句话
def count_reasoning_steps(response_text: str) -> int:
"""粗略统计回答中独立推理步骤的数量"""
# 按编号列表、分号分隔的句子等启发式计数
steps = 0
for line in response_text.split("\n"):
line = line.strip()
if line and (line[0].isdigit() or line.startswith("-") or line.startswith("*")):
steps += 1
# 也按"首先/其次/然后/最后"等连接词计数
connector_words = ["首先", "其次", "然后", "接着", "最后", "另外", "同时"]
for word in connector_words:
steps += response_text.count(word)
return max(steps, 1)
def run_test(prompt: str, system: str, thinking_budget: int = 2000) -> str:
"""调用 Claude API,控制 thinking budget 和系统提示词"""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=system,
messages=[{"role": "user", "content": prompt}],
# 使用 extended thinking 控制推理深度
thinking={
"type": "enabled",
"budget_tokens": thinking_budget,
},
)
# 提取文本内容
text_blocks = [b.text for b in response.content if b.type == "text"]
return "".join(text_blocks)
def detect_drift():
"""对比不同配置下的质量差异"""
results = {}
configs = {
"baseline": {"system": SYSTEM_FULL, "thinking_budget": 2000},
"low_effort": {"system": SYSTEM_FULL, "thinking_budget": 500}, # 模拟推理降档
"truncated_sys": {"system": SYSTEM_TRUNCATED, "thinking_budget": 2000}, # 模拟提示词截断
"both_bad": {"system": SYSTEM_TRUNCATED, "thinking_budget": 500}, # 模拟叠加
}
for test in TEST_PROMPTS:
print(f"\n=== 测试题: {test['id']} ===")
for config_name, config in configs.items():
text = run_test(
test["prompt"],
config["system"],
config["thinking_budget"],
)
steps = count_reasoning_steps(text)
drift = test["expected_depth"] - steps
label = "✓" if drift <= 0 else f"⚠ 缺 {drift} 步"
print(f" {config_name:15s} | 推理步骤: {steps:3d} | 期望: {test['expected_depth']} | {label}")
results.setdefault(test["id"], {})[config_name] = {
"steps": steps,
"expected": test["expected_depth"],
"drift": drift,
}
# 输出汇总
print("\n=== 漂移汇总 ===")
for test_id, configs_result in results.items():
baseline_steps = configs_result["baseline"]["steps"]
for name, r in configs_result.items():
delta = r["steps"] - baseline_steps
print(f" {test_id} | {name:15s} | 相对基线变化: {delta:+d} 步")
return results
if __name__ == "__main__":
detect_drift()
运行方式:
pip install anthropic
export ANTHROPIC_API_KEY=your_key_here
python quality_drift_detector.py
这个脚本的核心逻辑:用同一套对推理深度敏感的测试题,在不同配置下跑对比。如果你看到 low_effort 或 truncated_sys 的推理步骤数相对基线明显下降,就说明产品层改动正在侵蚀质量——哪怕模型权重完全没变。
缓存截断思维链:最隐蔽的杀手
三个 Bug 里,缓存抹掉 thinking tokens 是最值得单独深挖的。它的危害模式是:
| 轮次 | 缓存行为 | thinking tokens 保留 | 模型状态 |
|---|---|---|---|
| 1 | 无缓存,完整生成 | 2000 | 完整推理 |
| 2 | 缓存命中,但 thinking 被截断 | 500 | 推理链断裂 |
| 3 | 再次缓存,thinking 进一步丢失 | 100 | 几乎无推理 |
这不是"上下文窗口不够"那种显式错误——模型照样输出,只是输出的质量在沉默中退化。用户不会看到报错,只会觉得"最近 Claude 变笨了"。
防住这类问题的实操建议:
# 如果你用 Claude Code 的缓存功能,加一条监控:
# 每轮对话后检查 thinking tokens 数量是否异常衰减
# 伪代码逻辑(嵌入你的对话管线):
# round_1_thinking = 2000
# round_2_thinking = extract_thinking_length(response_2)
# if round_2_thinking < round_1_thinking * 0.5:
# log_warning("thinking tokens 衰减超过 50%,可能缓存截断")
# disable_cache_for_next_round()
如果你自己搭建了类似的缓存层,核心原则是:缓存可以复用用户输入和模型输出,但绝对不能截断模型的内部推理记录。thinking tokens 是模型的"工作记忆",砍掉它等于让模型带着脑损伤继续工作。
给 LLM 产品团队的检查清单
Anthropic 这次复盘的教训,浓缩成一条就是:产品层参数和模型推理深度是同一个系统的不同面,改任何一面都要跑完整评测。具体落地:
- 推理努力参数(reasoning effort / thinking budget):任何下调都必须跑针对复杂任务的专项评测,不能只看平均指标。3% 的整体下降在简单任务上会被噪声淹没,但在多步推理任务上可能是 15% 的灾难。
- 系统提示词长度上限:截断前先做重要性排序,把行为约束类指令放在前面,描述性内容放后面。或者干脆不截断——token 成本远低于质量事故成本。
- 缓存实现:缓存命中时必须完整保留 thinking tokens,不能只缓存最终输出。如果缓存空间有限,优先缓存 thinking 而不是缓存 system prompt。
- 监控:部署结构化质量评测作为 CI 管线的一部分,每次产品层配置变更都自动跑对比。不要依赖用户抱怨来发现问题——六周的延迟就是代价。
- 变更窗口:多个产品层改动不要在同一时段上线。Anthropic 的教训是三个"小改动"叠加成了大事故。如果必须同期上线,至少每个改动单独跑一轮评测确认无影响后再叠加。
这次事故的根因不是模型能力退化,而是产品层对模型推理过程的三个无意伤害。模型没变,但模型"能想多深"被配置悄悄限制了。对任何在模型之上搭产品层的团队来说,这是同一个陷阱的不同版本——你的配置就是模型的能力边界。