跳到主要内容

Token 预算是产品决策,而非配置值

· 阅读需 12 分钟
Tian Pan
Software Engineer

在你的代码库某处,有一行代码看起来像 retriever.search(query, top_k=8)。某位工程师在某个下午写下了那个 8。它从未被团队以外的任何人评审过,从未出现在规范文档中,也从未被重新审视。这一个整数决定了你的上下文窗口中有多少配额分配给了检索到的文档而非对话历史,决定了每次请求的成本,决定了响应速度的感官体验,并且——由于语言模型在长文本下的实际表现——还决定了答案的准确性。

这是一个产品决策。它却被硬编码在一个 f-string 里。

上下文窗口是一个固定的、稀缺的预算。每一次请求都在相互竞争的项目中支出:系统提示词、对话历史、检索到的文档、累积的工具输出,以及模型仍需生成的响应。一方增多意味着另一方减少。然而,几乎没有人将这种分配视为一项需要负责人的决策。它是由最后触碰提示词组装代码的人隐式做出的,然后就被固化了下来。其结果是形成了一个产品表面——它是 AI 功能中最重要的一个——但却从未有产品经理见过它。

上下文窗口是零和博弈,且没人画过那张饼图

从物理现实开始。模型的上下文窗口是一个固定的 Token 数量。你发送的所有内容都在共享它:指令、少样本示例(few-shot examples)、历史记录、检索到的片段、工具结果,以及模型自身的输出(它必须填入剩下的任何空间)。这不是一个你可以通过工程手段绕过的软限制。它是一个预算,而预算是零和的。

在实践中,这种分配是一场无声的拔河。一个拥有长对话历史的聊天机器人想要保留之前的每一轮对话。一个 RAG 功能想要塞进 10 份相关的文档——每份 1,500 个 Token,在模型读取到实际问题之前就已经花掉了 15,000 个 Token。一个智能体(Agent)想要保留原始的工具输出:一次天气检查耗费 200 个 Token,一次数据库查询耗费 3,000 个,一次 API 调用耗费 5,000 个,如果你轮复一轮地追加每一个原始结果,上下文会在单个会话中迅速膨胀。

这些消费者中的每一个都是由解决不同问题的不同人构建的。历史记录保留逻辑存在于一个模块中,检索调用在另一个模块中,工具结果处理在第三个中。没有任何一个文件会写道:“历史记录占 30%,检索占 40%,工具输出占 20%,响应占 10%。”饼图是客观存在的——它必须存在,因为 Token 是有限的——但从未有人画过它。它是三个独立的局部决策自发形成的总和,而自发形成的预算总是错误的,因为没有人对整体进行优化。

第一步是让这张饼图变得真实。写下每个功能在代表性请求中的 Token 去向。这个数字几乎从来不是团队猜测的那样。团队通常会发现,一个 “RAG 答案” 中 60% 的内容是没人阅读的对话历史,或者一个工具的原始 JSON 输出是它所支撑的推理过程的三倍。

“增加更多片段”是披着工程外衣的产品决策

隐瞒决策的操作通常是这样的:收到一个质量投诉——“助手遗漏了一些它本该知道的信息”。显而易见的修复方法是检索更多内容:将 top_k 从 8 调到 12,保留更多历史,扩大片段大小。这看起来像是在调优。它只是一个配置更改,通过一个小的 PR 发布,评估分数上升了一点。然后每个人都继续后续工作。

但“检索更多”并不是一个中性的旋钮。它同时消耗了三种货币。

它消耗金钱。输入 Token 是计费的。一个被广泛引用的对比是:一个客服机器人每天处理 20,000 个查询,如果采用朴素的 15 万 Token 历史记录方案,每天成本约为 9,000 美元;而同一个机器人,如果将上下文过滤到相关的 4,000 个 Token,成本约为 240 美元。这不只是四舍五入的误差——这是由你修剪上下文的力度所决定的 37 倍的成本波动。

它消耗延迟。更多的输入意味着更多的预填充(prefill)。在极端情况下,最大长度上下文的预填充延迟在当前硬件上可能超过两分钟,这悄无声息地排除了交互式使用的可能性。即使远未达到极限,你增加的每一千个 Token 都是用户在第一个 Token 出现前等待的时间。

它消耗质量——这是最反直觉的部分。更多的上下文并不稳健地意味着更好的答案。它通常意味着更差的答案。

因此,“增加更多片段”是在成本、延迟和质量之间进行权衡。决定如何进行权衡正是产品决策的定义。拥有该功能的单次请求成本、延迟 SLO 和准确性目标的人应该对此负责。相反,决策是由那个碰巧在那周修改检索文件的人做出的,而且它是隐形做出的,因为调高 top_k 看起来不像是一个产品变更。它看起来就像一个配置值。

上下文腐烂:为什么“更多”会主动损害准确性

“直接检索更多”背后的假设是:额外的上下文起码是无害的——也许浪费了钱,但不会导致错误答案。研究表明事实并非如此。

斯坦福大学研究人员记录的“迷失在中间”(lost in the middle)效应表明,模型对输入内容的开头和结尾关注度很高,而对中间部分的关注度较低。在检索约 20 份文档时,与放在开头或结尾相比,放在中间的事实的准确率下降了 15 到 20 个百分点。仅仅是位置——而非相关性,也不是正确性——就让分数产生了如此大的波动。

这种情况变得更加严峻。Chroma 在 2025 年的一项研究测试了 18 个前沿模型(包括来自主要实验室的最新模型),发现随着输入长度的增加,每一个模型的表现都会下降,即使上下文窗口远未填满。他们将其命名为上下文腐烂(context rot):输入的 Token 越多,输出的 Token 质量越差。而 2025 年底的一篇 arXiv 论文,题目尖锐地定为《尽管检索完美,仅上下文长度就会损害 LLM 性能》(Context Length Alone Hurts LLM Performance Despite Perfect Retrieval),发现即使检索是 100% 完美的——每个相关的 Token 都存在,不相关的被无害的空格替换——性能仍会随着长度的增加而下降,根据任务的不同,下降幅度在 14% 到 85% 之间。

思考一下。即使检索是完美的,长度本身就是对准确性征收的税。因此,为了修复某个质量投诉而调高 top_k 的工程师,可能会让其他每一个查询的质量都变得更糟——稀释了注意力,将关键事实埋在中间,并向模型喂入语义相似但会产生误导的干扰项。被“改进”的评估衡量的是引发投诉的那类查询。而性能倒退则稀疏地分布在所有其他内容中,永远不会表现为单个损坏的请求。

这正是为什么决策不能存在于配置文件中。一个配置旋钮内置了“越多越安全”的直觉。Token 预算则有着相反的形状:它存在一个峰值,过少或过多都会导致表现下降。只有思考整个功能的人才能找到那个峰值。

衡量最后 1,000 个 Token 的边际价值

如果 Token 预算是一个产品决策,它就需要每个产品决策都需要的东西:衡量。不是“更多的上下文好吗?”,而是“最后这 1,000 个 Token 给我们带来了什么?”

这是一个边际价值问题,你可以通过经验来回答。固定一个功能的评估集(eval set)并扫过(sweep)一个插槽(slot)。在 top_k 为 4、6、8、10、12 时运行检索,并绘制每个点的准确率、成本和 p95 延迟。曲线很少是直线。一个生产团队发现,每检索一个段落的收益在超过四个段落后就开始变平;他们将 top_k 从 6 削减到 4,在保持质量稳定的同时,将检索成本降低了 22%。独立评估往往发现,检索块(chunks)的有效范围在 4 到 10 之间,超过这个范围,噪声和稀释效应就会占主导地位。

对每个插槽进行同样的扫描。多少历史记录实际上改变了答案 —— 第 10 轮对话是否还在赚回它的 Token 成本,还是你可以总结前三轮之前的所有内容?模型需要每个工具的多少原始输出 —— 你能否返回一个 200 Token 的摘要,而不是 5,000 Token 的 JSON?你甚至可以将此应用于响应:关于具备 Token 预算意识的推理研究表明,仅仅告诉模型使用合理的预算,就将思维链(chain-of-thought)输出从 258 个 Token 减少到 86 个(减少了 67%),同时保留了正确答案。

每一次尝试都是经过衡量的权衡,而不是猜测。一旦你拥有了这些曲线,分配就不再是一个审美偏好问题。你可以说:这个功能的历史记录插槽在超过三轮后边际价值接近于零,所以我们封顶它,并将释放出的预算花在检索上,因为检索的曲线仍在攀升。这就是预算会议。这是在编写 f-string 之前就应该召开的预算会议。

给产品负责人一个他们能看懂的旋钮

Token 预算之所以被隐藏,是因为它的表达单位不对。“top_k=8、4,000 Token 的历史窗口、原始工具输出”是工程师的词汇。产品负责人无法对其进行推理,所以他们不参与,默认由工程团队负责。

翻译它。用产品经理已经理解的三种“货币”重新表达每个功能的预算:单次请求成本p95 延迟以及评估集准确率。现在,这就是产品负责人真正能够参与的对话了。“这个功能成本 11 美分,响应时间 3 秒,准确率 84%。通过修剪历史记录,我们可以将成本降至 6 美分,时间降至 1.8 秒,而评估结果几乎没有变化。或者我们可以通过扩大检索范围,以 15 美分和 4 秒的代价将准确率提高到 88%。这属于哪种功能定位?”

客服机器人和财务分析助手对这个问题的回答会截然不同,事实也应如此 —— 一个在大业务量下对延迟和成本敏感,另一个则愿意为准确率付费。这种分歧正是重点所在。这是关于功能用途的产品判断,当你交给产品负责人一个贴着他们语言标签的旋钮,而不是一个贴着你语言标签的配置文件时,他们能做出很好的判断。

具体来说:像设置延迟 SLO 一样,为每个功能设置明确的 Token 预算。为上下文窗口的每个消费者(历史记录、检索、工具输出)提供一个命名的分配额度。将上下文预算项作为任何新 AI 功能设计评审的一部分,与已有的延迟和成本项并列。并对其进行回归检查,这样未来某个悄悄将成本翻倍或引发上下文腐烂(context rot)的 top_k 提升,就会显示为一个可评审的变更,而不是不可见的变更。

f-string 始终是一份规格说明

这一切都不需要新的基础设施。预算已经存在 —— 无论你是否承认,上下文窗口都是有限的,而且每一次请求都在消耗它。唯一的问题是,这种支出是否是有意为之。

目前,对于大多数团队来说,事实并非如此。它是三个工程师解决三个局部问题的残留物,固化为一个神奇数字,并因其看起来像配置而逃避了评审。解决方案不是一个框架。而是要意识到,分配上下文窗口的那行代码其实是一个被写进 f-string 的产品规格(spec),并将决策权移交给负责功能成本、速度和质量的人。

画出饼图。衡量边际 Token。用成本、延迟和准确率标记旋钮。然后召开预算会议 —— 在写下 f-string 之前,而不是在事故发生之后。

References:Let's stay in touch and Follow me for more thoughts and updates