ContextChef (2):Janitor——把触发逻辑和压缩策略彻底分离
6 Mar 2026
7 min read
English version: ContextChef (2): Janitor — Separating Trigger Logic from Compression Policy
历史压缩是所有长程 Agent 都要解决的问题,但大多数框架的解法是:替你做了。框架内置一个压缩策略,你调一调参数,然后接受它的行为。
Janitor 的设计角度不是这个。它的核心分工是:“何时触发”是基础设施,由库负责;“怎么压缩”是业务决策,由开发者负责。这两件事被彻底分开。
Anthropic 在 Effective Context Engineering for AI Agents 里把这个操作称为 compaction,并强调它的核心难点不是压缩本身,而是选择保留什么——“overly aggressive compaction can result in the loss of subtle but critical context whose importance only becomes apparent later.” 这恰好说明了为什么压缩策略不能内置在库里:什么是”后来才重要的细节”,只有了解业务的开发者才能判断。
设计角度:机制,不做决策
Janitor 做的事情很窄:监控 token 用量,在合适的时机触发一个你提供的回调,把压缩后的历史写回。触发逻辑是库的职责,压缩逻辑是你的职责。
这个分工带来了几个具体的好处:
你选模型,你控成本。 compressionModel 是一个 async 函数 slot,接收待压缩的消息数组,返回一个字符串摘要。你决定用 gpt-4o-mini 还是 claude-haiku,你写 prompt,你决定摘要详细程度。Janitor 不知道也不在意你怎么实现这个函数——它只负责在合适的时机调用它。
干预时机在你手里。 onBeforeCompress 钩子在自动压缩之前触发,让你有机会先做无损的预处理——详见 ContextChef (8):编译管道里的五个扩展点。这个钩子的存在意味着摘要压缩是最后手段,不是第一反应。
保留边界你来定。 preserveRatio 和 preserveRecentMessages 控制压缩时保留多少近期历史,但这只是边界参数——边界之内保留什么、怎么摘要边界之外,还是你的 compressionModel 说了算。
一个重要的设计细节:压缩是基于**轮次(turn)**操作的,不是基于单条消息。一个”轮次”是一个原子单元——单条消息,或者一条带 tool_calls 的 assistant 消息加上它后续的所有 tool result 消息。这意味着 preserveRecentMessages: 1 保留的是最后一个完整轮次,可能是一条消息,也可能是五条(如果 assistant 调用了四个工具)。原因是:如果把工具调用和它的结果拆开,模型会看到一个没有响应的调用——一个破损的状态,既干扰模型理解,也干扰压缩摘要的质量。基于轮次的分组从结构上消除了这类错误。
两条路径:为不同阶段的项目设计
Janitor 有两条触发路径,不是因为有两个不同的使用场景,而是因为不同阶段的项目对接入成本的接受程度不同。
feedTokenUsage 路径是起步方式。LLM API 的响应里通常直接带有 usage.prompt_tokens,把这个值喂给 Janitor,零额外依赖,三行代码。代价是有一轮延迟——压缩在超限之后的下一次 compile() 才触发。大多数情况下这完全够用,因为模型通常能容忍接近上限的 token 数,超一轮不会立即崩溃。
Tokenizer 路径是精细化之后的选择。当你发现 feedTokenUsage 的一轮延迟在某些边缘情况下会导致请求超限失败,或者你想在压缩触发前做更精确的预算估算时,切换到 tokenizer 路径——传入一个 token 计数函数,Janitor 在每次 compile() 时预先计算,提前干预。
这两条路径可以并存:如果同时提供了 tokenizer 和 feedTokenUsage,Janitor 取两者中的较大值,保守触发。实践上,很多项目从 feedTokenUsage 起步,在遇到边界问题后再引入 tokenizer,两次改动都只是配置变更,不影响压缩逻辑本身。
直接丢弃还是低成本摘要
最简单的压缩方式是不压缩——超限时直接丢掉旧消息。原型阶段这样做完全没问题,但生产环境里直接丢弃意味着模型会突然”失忆”。失忆会让 Agent 在长程任务里绕路甚至产生前后矛盾的操作,代价远不止几分钱的 API 费用。实际上,低成本摘要模型(gpt-4o-mini、claude-haiku)的开销通常远低于因失忆导致的任务失败成本。
还有一条更轻量的路径:Anthropic 提到的 tool result clearing——不摘要内容,只清除历史中深层工具调用的原始输出,保留工具名和调用 ID。工具执行完之后,它的原始输出对模型的价值已经大幅降低;模型只需要知道”这个工具被调用过”,不需要再看完整的 15000 字 JSON。ContextChef 通过 compact() 方法提供了这个能力——这是一个纯机械操作,零 LLM 成本,可以剥离 thinking 块和旧的工具结果,不需要任何摘要。也可以和 onBeforeCompress 钩子结合实现两阶段策略——详见 ContextChef (8):编译管道里的五个扩展点。
下一篇:Pruner 和工具幻觉的根因——为什么 Manus 说”不要在运行时删工具”。