从主循环 fork 出去
从主循环 fork 一个临时分支,以完整上下文和前缀缓存处理总结、提炼、自查与评估,同时不污染主历史。
思路
Agent 跑长任务时,经常需要干一些“任务之外”的活:总结对话、提炼经验、生成标题、检查方向。这些活有个共同点——它们需要主循环的完整上下文,但它们的产出不属于任务本身。
处理这类活,最顺的办法是从主循环 fork 一个临时分支:
const result = await session.fork({
prompt: DISTILL_PROMPT, // append 一条指令
toolPolicy: { allow: [] }, // 执行层拦截工具调用
outputSchema: resultSchema, // 结构化返回
});
语义和进程 fork 一样:分支完整继承主循环此刻的一切——system prompt、对话历史、工具轨迹、已加载的 skill——在末尾 append 一条指令,跑完把结果作为返回值交回,分支丢弃,不回写主历史。
这个做法同时拿到四样东西:
完整上下文,零构造成本。 分支的上下文不是“构造”出来的,就是主循环本身。不需要从数据库读事件日志、过滤、拼 transcript——这类代码本质上是主循环上下文逻辑的手工副本,主循环一进化(新的消息类型、新的附件处理),副本静默过时,不报错,只是悄悄漏信息。fork 把唯一真相源定为主循环自己。
缓存全复用。 分支前缀和主循环逐字节一致,前缀缓存(供应商对相同上下文前缀的打折,通常便宜一个数量级)全额命中,只为 append 的指令和输出付费。几十万 token 的会话,fork 一次的边际成本几乎只有输出那几千 token。
不污染主历史。 元操作的输入输出留在分支里,随分支丢弃。主循环的任务叙事保持干净,下次 compact 不会把总结的总结搅进摘要。
可以高频跑。 前三条合起来的结果:每轮、每 N 轮、任务结束时触发都行,没有累积成本,也不需要状态机和轮询——它就是一次函数调用。
一个容易做错的细节:禁用工具不能靠把工具移出 schema。工具定义是上下文前缀的一部分,通常还排在最前面——schema 一变,前缀从第一个字节起就和主循环分叉,缓存全灭。正确做法是 schema 原样保留,在执行层拦截:模型仍然看到全套工具,白名单之外的调用一律拒绝执行。同理,裁剪历史(contextTransform)也会动前缀,不动缓存的默认姿态是全量继承。
能拿它干什么
判据一句话:凡是“以主循环当前状态为输入、产出不属于任务本身”的活,都是它的活。
- 提炼沉淀:记忆提炼(用户偏好、纠正过的规则 → 长期记忆)、skill 蒸馏、踩坑归档、决策日志
- 自省纠偏:方向自查(每 N 轮问“还在朝目标走吗”,答案回注主循环)、约束审计(对照用户最初的要求逐条检查产出)、卡死检测
- 轻量元数据:标题/摘要、进度报告、移交摘要、commit message
- 评估决策:多视角评审(同一状态 fork 多份、注入不同视角)、交付前置信度评估、投机探索(fork 推演 A 方案几步,主循环零成本观望)
这些活现在大多被做成两种形态:要么塞进主循环(低频可以,高频堆垃圾),要么各自手搓一条独立链路——读库、重建上下文、新 session、状态表、轮询、解析输出,一个功能几百行,且每个功能重复一遍。有了 fork,它们共享同一个执行原语,每个用途只剩一条 prompt 和一个 schema。
Claude Code 已经这么干了
Claude Code 的 session memory compact 就是这个模式的生产实例。从行为和成本特征看,它的后台摘要走的正是这条路:以当前会话为前缀 fork 出压缩任务,前缀命中缓存,只为摘要指令和输出付费——这也是它敢在后台持续做增量压缩、而不是等上下文爆掉才一次性硬压的经济学前提。