MyPrototypeWhat

ContextChef (8):编译管道里的五个扩展点

English version: ContextChef (8): Five Extension Points in the Compile Pipeline

ContextChef 默认是一个封闭的编译管道:你把状态喂进去,它输出 payload,内部怎么排列、怎么压缩、怎么格式化都由各个模块决定。大多数情况下这正是你想要的——不需要关心细节,直接拿结果。

但有些需求处于模块边界之外:想在压缩之前先做一轮无损清理;想在每次生成前注入实时检索结果;想记录每次记忆写入的审计日志;想在最后一刻对消息数组做一次兜底修改。这些都不需要 fork 库,也不需要绕开管道——它们对应的是管道里五个精确的干预窗口。

compile() 的执行顺序

compile() 调用

1. Janitor:检查 token 预算
   ├─ onBudgetExceeded(超限时,压缩前)
   └─ onCompress(压缩完成后)

2. onBeforeCompile(Janitor 之后,拼装之前)

3. Memory 注入

4. Sandwich 拼装(Assembler + Guardrail)

5. transformContext(拼装完成后,Adapter 之前)

6. Adapter 格式化

输出 payload

onMemoryUpdate 不在 compile() 的流程里,它在任何一次 Memory 写入时单独触发。

onBudgetExceeded:压缩前的干预窗口

触发时机:Janitor 检测到 token 用量超出 contextWindow,在走摘要压缩之前。

用途:在”有损压缩”之前先做”无损预处理”。最常见的模式是:先把历史里的大型工具结果卸载到 VFS,看能否仅靠这步把 token 数降到预算内,避免摘要压缩丢失内容。返回修改后的历史数组,Janitor 用这份历史继续;返回 null 跳过预处理,直接走压缩。

这个 hook 的存在让”lossless first”策略可以显式实现——摘要压缩是最后手段,而不是超限时的第一反应。

onCompress:压缩完成的通知

触发时机:压缩实际发生之后,接收 summary 消息和被压缩的消息数量。

用途:通知类的副作用——在 UI 上显示进度指示器、把压缩摘要保存到日志、触发外部状态更新。不用于修改压缩结果。

onBeforeCompile:just-in-time 上下文注入

触发时机:Janitor 处理完毕后,拼装三明治之前。接收当前 topLayerrollingHistorydynamicStaterawDynamicXml 的只读快照。

用途:连接 ContextChef 和外部上下文源的标准接口。在这里做 RAG 检索、MCP 查询、代码 AST 提取——任何”每次生成前按需加载”的上下文都属于这里。返回字符串,ContextChef 把它包裹在 <implicit_context> 标签里,注入到和 dynamic state 相同的位置(last_usersystem),不影响 KV-cache 稳定性。

onBeforeCompile: async ({ rawDynamicXml }) => {
  // rawDynamicXml 是当前任务状态的 XML 序列化,直接用作检索 query
  const results = await vectorDB.search(rawDynamicXml, { topK: 3 });
  return results.map(r => r.content).join('\n');
}

rawDynamicXml 作为检索 query 是一个值得注意的设计细节:它是结构化的任务状态,语义比随机 user 消息更稳定,检索精度通常更高。这个 hook 和 Anthropic 提到的 just-in-time context 理念直接对应——维护轻量标识符,在真正需要时才加载完整数据。

Memory 的三个钩子

Memory 模块有三个钩子,职责不同,需要分开理解。

onMemoryUpdate:模型写入的否决钩子。 在模型通过工具调用触发 createMemory / updateMemory / deleteMemory 时触发,对开发者直接调用 chef.memory().set() 不生效。返回 false 阻止这次写入。典型用途:限制模型只能写入特定 key、对模型写入做内容安全检查。

onMemoryChanged:全量变更通知。 任何一次记忆变更(set、delete、TTL 过期)都会触发,接收结构化的 MemoryChangeEvent{ type: 'set' | 'delete' | 'expire', key, value, oldValue }。不能否决写入,只能观察。典型用途:审计日志、外部同步(数据库、Redis)。

onMemoryExpired:TTL 过期通知。 compile() 扫描到过期条目时触发,接收完整的 MemoryEntry(含 importanceupdateCount 等元数据)。不能否决,只能观察。典型用途:记录哪些信息因 TTL 被清理、触发清理后的副作用。

三者的关系:onMemoryUpdate 是写入路径上唯一的”守门员”,但只管模型写入;onMemoryChanged 是全量观察者,覆盖所有变更来源;onMemoryExpiredonMemoryChanged(type: ‘expire’)的专项版本,提供更丰富的条目元数据。allowedKeys 校验在 onMemoryUpdate 之前发生——被过滤掉的写入不会触发任何 hook。

transformContext:终极逃生舱

触发时机:三明治完整拼装之后,Adapter 格式化之前。接收完整的 Message[],返回修改后的数组。

用途:以上四个 Hook 都不能覆盖的情况。理论上可以在这里做任何消息层面的操作——删除、插入、修改内容、重排顺序。实践上,如果你发现自己频繁用到 transformContext,通常意味着某个模块没有覆盖你的场景,或者接入方式有可以优化的地方。把它当做”确实有一个边缘情况”的工具,而不是常规管道的一部分。


五个干预窗口,每个在管道里的位置都是精确的。选择哪个 Hook 的判断标准只有一条:它触发的时机是否和你的干预需求匹配