Token 预算是调度问题,而非提示词问题
当一个智能体(agent)给出的回答比上周差时,第一直觉往往是归咎于提示词(prompt)。有人会重新编写系统指令,删减几句话,增加一个示例,然后发布。有时这会有所帮助。但通常这毫无作用,因为提示词从来就不是问题所在。问题在于,一个冗长的工具调用结果静默地消耗了 18,000 个 token,将实际的任务指令推到了上下文窗口中关注度较低的中部位置,让模型在一个 70% 都是噪音的记录上进行推理。
这不仅是措辞问题,更是资源分配问题。资源分配在系统工程中有一个专属名称:调度(scheduling)。上下文窗口是一种固定大小的资源,多个消费者在其中竞争,而目前大多数智能体技术栈“调度”它的方式就像 1960 年代的批处理系统调度内存一样——先到先得,直到用完为止。
这种视角的转变至关重要,因为它改变了你构建产品的方式。如果 token 使用量是一个提示词问题,解决方法是编辑。如果它是一个调度问题,解决方法则是一个分配器(allocator):一个为每个消费者分配预算、强制执行上限、决定在压力下驱逐哪些内容并使整个过程可观测的组件。操作系 统在几十年前就为内存解决了这个问题,而智能体框架正在以艰难的方式重新发现它。
上下文窗口就是 RAM,而没人写过内存管理器
思考一下在单次推理调用中,究竟有哪些内容在竞争空间:系统提示词、工具和函数定义(schemas)、从向量数据库检索到的片段、正在进行的对话历史、之前步骤的工具输出、少样本(few-shot)示例、子智能体的草稿本(scratchpad)推理。每一个都是一个在申请内存的进程,而上下文窗口就是它们共享的 RAM。
在真实的操作系统中,没有进程可以无限制地调用 malloc()。有一个内存管理器负责跟踪分配、强制执行限制,并在压力增大时将内容换出(page out)。在大多数智能体代码中,并没有对应的机制。检索器根据 top_k 获取尽可能多的片段。工具返回 API 返回的所有内容。历史记录每轮增加一轮。没有任何机制在它们之间进行仲裁。“分配策略”仅仅取决于这些字符串被拼接在一起的顺序。
这种方式在出问题之前一直有效。出问题的转折点很少是硬性的上下文限制错误——现代窗口足够大,通常都有空间。转折点在于质量。Chroma 的上下文腐烂研究测试了 18 个前沿模型,发现随着输入长度的增长,每个模型的表现都会下降,即使相关信息在技术上确实存在。性能遵循 U 型曲线:窗口开头和结尾的内容准确率最高,而困在中间的内容准确率会下降 30% 以上。上下文窗口并非均匀 资源。Token 具有位置价值,忽略位置的分配器正在白白丢掉正确性。
因此,这种失败是无声的。你不会收到异常报错。你只会得到一个稍差的答案,接着又是一个,而仪表盘仍然显示绿色,因为技术上没有任何东西崩溃。这就是资源未受管理的典型特征。
各组件的预算上限
调度器做的第一件事就是限制每个消费者的上限。你需要提前决定每个组件可以占用的窗口最大份额,并强制执行。
对于一个智能体,一个可行的初始分配方案大致如下:系统指令占用 10–15%,工具定义占用 15–20%,检索到的知识占用 30–40%,其余部分预留给对话历史和模型自身的推理。具体的数字并不如上限的存在本身重要。上限将无限制的 malloc() 转换为固定分配,这意味着一个组件再也不会因意外而让其他组件“饿死”。
最紧迫需要上限的组件是工具输出。在某项已发布的上下文工程基准测试中,48,400 个总 token 中有 30,400 个仅来自工具结果——而且其中 40% 到 60% 的 token 可以在不产生可衡量性能损失的情况下移除。工具输出是智能体系统的内存泄漏。API 返回了一个包含 200 行的 JSON 块,智能体只需要其中三个字段,而剩下的 197 行现在留在窗口中,降低了后续每一步的质量。生产环境的工具链已经开始直接处理这个问题:Claude Code 会根据预算计算每个工具结果的 token 数,并将超大的结果持久化到磁盘而不是内联,专门捕捉那些虽然未超过字节限制但 token 密度极高的输出。
工具输出的上限不应该只是个礼貌的建议。它应该是一个硬性的截断或转移(truncate-or-offload)边界,在结果进入窗口之前就应用。任何超过上限的内容都会被存入磁盘、生成摘要或进入后续的检索调用——默认情况下绝不会直接放入提示词。
优先级与驱逐:压力之下哪些内容被换出
上限处理稳态,驱逐(eviction)处理危机。当所有需求的总和超过窗口时,必须有一些内容离开,而这种“离开”应该是一个决策,而不是字符串顺序导致的意外。
为每个组件分配一个优先级,反映其对当前步骤的重要性。系统指令和活动任务描述是不可驱逐的——它们是内核;如果它们消失了,一切都无法运行。智能体正在积极使用的工具定义具有高优先级。最近的对话轮次是中等优先级。旧的工具输出和陈旧的检索片段是低优先级——它们在三步之前很有用,现在大多是噪音。
驱逐随后从底部开始。生产环境工具链中的滑动窗口策略正是这样做的:当使用量超过阈值时,丢弃最旧的 30% 消息,然后以 10% 的增量继续丢弃,直到回到预算之内。这就是换名后的页面置换算法。值得改进的一点是,新鲜度只是价值的一个粗略代理。一个确立了关键事实的旧工具结果可能比最近一轮的客套话更重要。一个好的驱逐器会根据优先级和相关性进行评分,而不仅仅是根据时间——这更接近带有提示的 LRU(最近最少使用),而非纯粹的 FIFO(先进先出)。
至关重要的一点是,驱逐不是删除。被换出的内存 帧会存入磁盘,并可以被重新调入(faulted back in)。智能体对应的做法是总结一个被驱逐的块,将完整版本带上句柄存储在外部,并在后续步骤需要时允许智能体重新检索。智能体应该能够重新调入自己的上下文。这种特性——将内容驱逐到可恢复的地方,而不是虚无中——正是调度器与断头台的区别。
饥饿故障模式
每种调度系统都有其特征性的故障,对于 Token 预算来说,这种故障就是“饥饿”:一个贪婪的消费者占用了过多的资源,导致另一个消费者无法取得进展。
具体表现为:冗长的工具结果挤占了推理预算。智能体调用了一个工具,该工具返回了 18,000 个 Token。从技术上讲,这些 Token 都在“上下文”中,所以不会报错。但模型现在对实际推理步骤剩下的有效注意力(Attention)要少得多,任务指令也已经漂移到了 U 型曲线中低注意力的中间部分。模型并不比昨天笨,它只是被“饿坏了”。它被要求在工具结果未消耗掉的区区几百个 Token 的注意力空间内进行思考。
这正是调度器存在的意义——防止这种 Bug。操作系统不会允许单个进程消耗掉所有 RAM 并卡死其他进程,它拥有各种限制以及一套根据优先级强制执行公平性的调度器。在智能体领域,对应的方案是推理预算预留:在上下文窗口中划出一块保证不受检索和工具输出干扰的区域,无论它们多么想要这块空间。你预留它的方式就像实时系统为关键任务预留 CPU 一样。这种预留是确保模型始终有思考空间的保证。
