模型宣称的200K上下文窗口实际质量在130K左右就开始严重衰减,中间部分内容最容易被遗忘(lost in the middle问题),这是注意力机制的固有局限而非Prompt问题。
模型文档中标注的上下文窗口大小是一个容量规格,而不是一个承诺。宣称支持 200K tokens 的模型会欣然接受 200K tokens,但模型处理这些内容的质量在你达到上限之前很久就已经开始下滑了。如果你在生产环境中构建功能时假设整个窗口的表现都一样好,你就会在长输入场景下遇到只有在生产环境才会暴露的 bug。下面我们来解析 token 预算的实际行为,以及如何避免上下文窗口悄悄对你撒谎。
标称的上下文窗口描述的是容量,而非可用的质量。关于长上下文行为的研究表明,模型对输入开头和结尾的内容关注度高得多,而埋在中间的内容即使远未达到标称上限也会被忽略。人们称之为 "lost in the middle"(中间丢失)问题,这源于注意力机制的工作原理,而非可以通过 prompt 绕过的 bug。
这个差距比大多数团队预期的要大。声称支持 200K 窗口的模型在实践中约 130K tokens 时就会出现可测量的质量下降。这不是模型拒绝回答,而是模型对你花钱发送的 tokens 的利用能力在悄悄变差。如果一条关键指令位于一个巨大 prompt 的中间,请把它当作"可能已读"来处理,而不是"一定已读"。
最大的误解是上下文窗口只用于你的输入。其实不是这样。这个限制适用于输入和输出 tokens 的总和。你的 system prompt、对话历史、任何检索到的文档、用户查询,以及模型自己的回复,都从同一个池子里消耗 token。
这有一个人们经常遇到的后果。一个慷慨的 system prompt 加上很长的对话历史可能几乎没有留下回答的空间。模型不会先警告你。它在思考中途耗尽预算,回复被截断,或者 API 直接拒绝请求。一旦你接受了输出与输入竞争同一空间这一事实,你就不会对最长会话中出现的截断回答感到惊讶了。
长 prompt 不仅有质量风险。每次调用都会消耗时间和金钱。注意力步骤将每个 token 与所有其他 token 进行比较,所以核心计算量随输入长度的平方增长。QK^T 矩阵是 n×n 的,这意味着将上下文翻倍大约会使模型的工作量翻两番。一项研究测量到在 15000 词上下文时延迟增加 7 倍。这就是快速回复和用户放弃的加载Spinner之间的差别。
成本遵循同样的曲线,因为 LLM API 对输入和输出 tokens 都收费。历史或检索上下文的每个额外 token 都是你每次请求都在花的钱,不管它是否物有所值。如果你的账单不断上涨,过大的 prompt 通常是原因之一,而精简它们是降低推理成本最快的方法之一,无需更换模型。
你无法管理从未衡量的预算。在发起请求之前,统计 prompt 各部分的实际成本。这个习惯可以揭示出悄悄吞噬你窗口空间的 system prompt 膨胀和无限制增长的历史记录。
import { encoding_for_model } from "tiktoken";
const enc = encoding_for_model("gpt-4o");
const count = (text) => enc.encode(text).length;
const parts = {
system: count(systemPrompt),
history: count(conversation.map((m) => m.content).join("\n")),
docs: count(retrievedChunks.join("\n\n")),
query: count(userQuery),
};
const inputTokens = Object.values(parts).reduce((a, b) => a + b, 0);
console.log(parts, "input total:", inputTokens);
对真实会话运行一次,你通常会发现某一部分占用了预算。十有八九要么是几个月没人精简过的膨胀 system prompt,要么是每轮对话都在增长的无限制历史记录。
本地计数只是第一步。在生产环境中,你希望在热路径上检查预算,并在撞墙之前而非之后发出警报。有效的规则是:每次调用时记录 token 使用量,当使用量超过上下文限制的 80% 时触发警报。这样你就有反应的时间,而不会等到请求开始失败。
const CONTEXT_LIMIT = 128000; // set this to your model's real limit
function checkBudget({ inputTokens, maxOutputTokens }, requestId) {
const projected = inputTokens + maxOutputTokens;
const usage = projected / CONTEXT_LIMIT;
console.log(JSON.stringify({
requestId,
inputTokens,
maxOutputTokens,
usagePct: Math.round(usage * 100),
}));
if (usage > 0.8) {
notifyOncall(`Context at ${Math.round(usage * 100)}% on ${requestId}`);
}
return usage <= 1;
}
当你越线时,解决方案很少是换更大的模型,而是减少发送的内容。对旧对话进行摘要总结、丢弃过时的检索片段,并依靠检索来只获取查询需要的段落而不是把所有东西都塞进 prompt。如果你正在搭建检索层,一个可靠的 RAG 来管理上下文设置可以帮你完成大部分的精简工作。
今天就记录你的 system prompt 的 token 计数。如果它超过几千个 tokens,其中可能携带了你不再需要的指令。
把 prompt 的开头和末尾用起来。将最重要的指令移到最顶部或最底部,绝不要放在中间。
在下一次部署之前添加 80% 使用量警报,让窗口在即将满的时候告诉你,而不是静默失败。
这些都不需要花哨的平台。一个 token 计数器、一次预算检查和一个 80% 警报就能在你用户发现之前捕获大多数上下文问题。如果你想了解超大的 prompt 对你账单的影响,用 LLM 管道成本计算器跑一下数字,然后根据真实定价来规划预算。
如果你想更深入地了解如何削减模型账单,我在我网站上更详细地介绍了这个话题。
如果你想在你的网站上端到端地搭建这套体系,这正是我承接的工作类型。
如果你的配置有所不同,欢迎留言。我很好奇大家在生产环境中实际运行的 token 预算是多少。