ContextChef (2):Janitor——把触发逻辑和压缩策略彻底分离
6 Mar 2026
1 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 不知道也不在意你怎么实现这个函数——它只负责在合适的时机调用它。
干预时机在你手里。 onBudgetExceeded 钩子在自动压缩之前触发,让你有机会先做无损的预处理——比如把大型工具结果卸载到 VFS,看能否在不摘要的情况下把 token 数降下来。你返回修改后的历史,Janitor 用这份历史继续;你返回 null,Janitor 走默认压缩流程。这个钩子的存在意味着摘要压缩是最后手段,不是第一反应。
保留边界你来定。 preserveRatio 和 preserveRecentMessages 控制压缩时保留多少近期历史,但这只是边界参数——边界之内保留什么、怎么摘要边界之外,还是你的 compressionModel 说了算。
两条路径:为不同阶段的项目设计
Janitor 有两条触发路径,不是因为有两个不同的使用场景,而是因为不同阶段的项目对接入成本的接受程度不同。
feedTokenUsage 路径是起步方式。LLM API 的响应里通常直接带有 usage.prompt_tokens,把这个值喂给 Janitor,零额外依赖,三行代码。代价是有一轮延迟——压缩在超限之后的下一次 compile() 才触发。大多数情况下这完全够用,因为模型通常能容忍接近上限的 token 数,超一轮不会立即崩溃。
Tokenizer 路径是精细化之后的选择。当你发现 feedTokenUsage 的一轮延迟在某些边缘情况下会导致请求超限失败,或者你想在压缩触发前做更精确的预算估算时,切换到 tokenizer 路径——传入一个 token 计数函数,Janitor 在每次 compile() 时预先计算,提前干预。
这两条路径可以并存:如果同时提供了 tokenizer 和 feedTokenUsage,Janitor 取两者中的较大值,保守触发。实践上,很多项目从 feedTokenUsage 起步,在遇到边界问题后再引入 tokenizer,两次改动都只是配置变更,不影响压缩逻辑本身。
不提供 compressionModel 的后果
如果不提供 compressionModel,超限时旧消息会被直接丢弃,没有摘要。ContextChef 会在构造时打印警告,但不阻止这样做。
原型阶段可以接受这个行为,但生产环境里直接丢弃意味着模型会突然”失忆”。低成本摘要模型(gpt-4o-mini、claude-haiku)的 API 费用通常远低于因失忆导致的任务失败成本——失忆会让 Agent 在长程任务里绕路甚至产生前后矛盾的操作,代价远不止几分钱的 API 费用。
Anthropic 提到了一种轻量替代方案:tool result clearing——不摘要内容,只清除历史中深层的工具调用原始输出,保留工具名和调用 ID。工具执行完之后,它的原始输出对模型的价值已经大幅降低;模型只需要知道”这个工具被调用过”,不需要再看完整的 15000 字 JSON。这可以通过 onBudgetExceeded 钩子实现,在走摘要压缩之前先尝试这种无损清理。
下一篇:Pruner 和工具幻觉的根因——为什么 Manus 说”不要在运行时删工具”。