用小脚本检验长上下文是否拖累回答
文章以客服机器人遗漏早期关键信息为例,说明上下文容量充足并不代表模型能有效利用全部内容。文中提供两个测试脚本及上下文裁剪思路,用于衡量长输入对应用表现的影响。
AI 深度解读
客服机器人第 3 条消息就知道用户按年付费,到了第 40 条,却拿月付退款政策回答问题。信息一直留在提示词里,模型仍然用错了。原文讨论的「上下文腐化」(Context Rot),就是输入变长后,模型利用已有信息的能力下降。
作者的结论很实用:别把上下文窗口容量当成可靠性承诺。先记录输入长度,再用检索测试和业务评测确认问题,最后精简模型每次要读的内容。对维护客服机器人、知识库问答或编程 Agent 的团队,这套排查顺序比直接换模型更有参考价值。
装得下,为什么还是会答错
上下文包含本次调用提供的全部 token:系统提示词、聊天记录、工具结果和文档。原文以 Messages API 为例,每轮请求重新携带完整历史;第 40 轮就需要带上之前的 39 条消息。对采用这种调用方式的应用,历史越长,每轮输入通常越大。
作者称,现代模型接受数百万 token 已经很常见。但窗口容量只回答「能否接收」,没有承诺「能否准确使用」。
原文引用了两组研究。Chroma 在 2025 年 7 月评估 18 个模型,保持任务复杂度不变,只增加输入长度,发现连简单任务也会退化,而且退化并不均匀。在聊天历史问答中,只保留相关信息的提示词平均约 300 tokens,完整提示词平均约 113k tokens;前者在所有受测模型上表现都明显更好。
NoLiMa 则测试了 13 个宣称支持至少 128K 上下文的模型。在 32K tokens 时,其中 11 个模型的表现降到了各自短输入基线的 50% 以下。这里比较的是相对基线,不能理解成准确率统一低于 50%。
为什么会这样?一个合理解释是:完整历史让模型同时承担「找出相关信息」和「回答问题」两项任务。作者还借全注意力的 n² 关系说明负担:token 翻倍,两两关系约变成四倍。不过原文也明确限定,有些模型使用更便宜的注意力变体,这不是所有模型统一遵循的成本定律。
信息还在上下文里,不等于模型一定能正确使用它。
怎么确认自己的应用也有这个问题
作者给出的第一步是记录每次调用的输入 token 数,并按轮次画图。示例使用 TypeScript、@anthropic-ai/sdk 和 client.messages.countTokens,读取返回的 input_tokens;密钥来自 ANTHROPIC_API_KEY,模型 ID 来自 MODEL。脚本使用顶层 await,需要按 ES module 运行。
原文称 Anthropic 的 token counting endpoint 免费,但受速率限制。这个检查只能告诉你输入堆了多高,不能单凭曲线判断回答是否退化。
第二步才是测试能力:埋入一个必须找到的事实,在不同长度和位置提同一个问题。原文采用 2_000、20_000、100_000 tokens 三档目标长度,以及 0、0.5、1 三个位置,共九个提示词。
关键是避免直接匹配。事实写成「每年扣一次费」,问题却问「用户是月付还是年付」。传统大海捞针测试容易因词语高度重合拿到接近满分,未必测得出真实业务里的理解困难。
示例通过 client.messages.create 请求答案,设置 max_tokens: 2000,再用 answer.includes("annual") 判对错。这个评分比较粗:答案即使包含否定,也可能被误判。填充文本又是重复生成的,最终长度还会超过目标值,因此结果应视为初筛。完整九格会发送几十万输入 tokens,需要考虑费用和速率限制;有随机性时,每格还要重复测试,并尽量换成真实日志片段。
位置也不能凭经验断言。2023 年的 Lost in the Middle 研究发现,答案放在长文档中间时,表现常明显下降;Chroma 的简单事实测试却没有发现明显的位置差异。三个位置都测,是为了让当前模型和任务给出答案。
第三步是在业务评测集上比较完整历史与相关片段。下面这段代码解决的就是同题对照:
type Msg = { role: "user" | "assistant"; content: string };
type EvalQuestion = { question: string; relevantMessages: Msg[]; allMessages: Msg[] };
async function runEval(
questions: EvalQuestion[],
askFn: (messages: Msg[], question: string) => Promise<string>,
scoreFn: (q: EvalQuestion, answer: string) => number,
) {
let focusedWins = 0;
for (const q of questions) {
const focused = scoreFn(q, await askFn(q.relevantMessages, q.question));
const full = scoreFn(q, await askFn(q.allMessages, q.question));
if (focused > full) focusedWins++;
}
return { focusedWins, total: questions.length };
}
要看清 focused > full:它只统计相关片段胜出的题数,没有记录分差、平局和反向胜出的情况。askFn 与 scoreFn 仍需接入自己的模型调用和业务评分。
精简历史,优先处理已经过期的事实
原文强调的干扰项,不只是无关闲聊,还包括「看起来相关,却不能回答当前问题」的内容。用户已经更换套餐,旧套餐仍在历史里;订单已经取消,旧订单状态仍被反复带入。
Chroma 的测试发现,加入一个干扰项就会降低表现,四个干扰项会进一步叠加影响,而且输入越长,影响越明显。模型可能拒答,也可能自信地答错。
作者提出五种处理方式:运行时检索所需资料,清理旧工具结果,维护结构化笔记,接近窗口上限时压缩历史,以及让子 Agent 承担大范围调查。子 Agent 可以消耗数万 tokens,再返回通常为 1,000 到 2,000 tokens 的摘要。
作者最倾向结构化笔记,因为它能明确替换过期状态:
type Msg = { role: "user" | "assistant"; content: string };
const notes: Record<string, string> = { plan: "annual", lastOrder: "blue headphones" };
function buildMessages(notes: Record<string, string>, recentTurns: Msg[], question: string): Msg[] {
const pinned = "Key facts:\n" + Object.entries(notes).map(([k, v]) => `- ${k}: ${v}`).join("\n");
return [...recentTurns, { role: "user", content: `${pinned}\n\n${question}` }];
}
notes.plan = "monthly"; // 状态变化时覆盖,旧值不再保留
重点是最后一行覆盖旧值。不过它只清除了笔记中的旧值;如果 recentTurns 仍包含旧套餐信息,冲突依然可能存在。这个示例展示了消息组装方式,没有实现笔记的外部持久化,也没有解决事实由谁确认的问题。
布局也能辅助。原文引用 Anthropic 指南:对 20k+ tokens 的输入,把长文档放在前面,查询、指令和示例放在后面;查询置于末尾,在其测试中可让回答质量最多提升 30%。还可以先要求模型引用相关原文,再回答。压缩则有另一种风险:摘要可能恰好删掉下一轮需要的细节,因此作者建议同时保留关键事实笔记。
我的判断
我赞同它的排查顺序:先测真实输入长度和业务结果,再决定怎样裁剪。相比一遇到错误就升级模型,这更容易定位问题;相比直接截断历史,也更有机会保住必要信息。
但原文没有提供作者自己应用的完整实验结果、模型版本和对照数据。开头的退款场景是说明问题的例子,不能当成实测案例。「只能选一个就选笔记」也属于作者的经验判断,缺少与检索、摘要方案的直接比较。
还有一个容易忽略的前提:评测时人工选出的相关片段,可能比生产检索更准确。它赢过完整历史,说明精简有潜力;真正上线后,还要检查检索是否漏掉决定答案的消息。笔记同样可能写错,摘要同样可能遗漏。
裁剪有没有价值,要看答案是否更可靠,不能只看 token 是否减少。
原文对所有模型都会出现腐化的表述也比较宽。研究支持在受测模型和任务上观察到退化,不足以预告每个模型在每种业务中的退化幅度。「最多提升 30%」同样是特定测试结果,不是调整顺序就能兑现的收益。
能拿来做什么
- 在 Node 服务的模型调用入口记录轮次和
input_tokens,结合现有错误样本检查长会话,先确认问题是否集中在输入增长之后。 - 从自己的客服或问答日志里挑出必须找回的事实,用不同措辞提问,测试 2_000、20_000、100_000 tokens 和三个位置,并记录实际输入长度。
- 为一组业务问题准备相关片段与完整历史两份输入,用同一评分规则比较;采用检索后,再评估检索遗漏造成的损失。
- 给持续运行的 Agent 保存可更新的关键事实笔记,套餐、订单等状态变化时覆盖旧值,同时清理失效工具结果;用同一评测集验证修改是否减少了错误。