拆.
多 Agent——AI 怎么'协作' · 第 07

Session Fork

让 AI 同一段对话'分叉'

4500读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:探索性 AI 工作的版本控制需求

用 AI Agent 做事时经常遇到这种情况——用户做了一段工作,AI 改了文件、跑了测试、有了初步结论。然后用户想"试试另一种方案"——但又不想丢掉当前进度。

直觉的需求是——让当前 session "分叉"

  • 保留当前状态(已改的文件、对话历史、AI 的思考)
  • 创建一个"平行的 session"——从某个时间点开始走不同方向
  • 两条分支独立——一条尝试 A 方案、一条尝试 B 方案
  • 之后可以决定保留哪条、丢掉哪条

这跟 git 的 branch 概念很像。事实上git 是 fork 概念的灵感来源——AI Agent 也需要"版本控制"。

为什么需要?因为 AI 工作经常是探索性的——很多时候你不知道哪条路是对的。

具体场景:

场景 1:方案对比

用户让 AI "重构 auth 模块"——可能有 3 个方案:

  • A:用现有 framework
  • B:换个 framework
  • C:从头自己写

用户想看每个方案的样子——但不能"先做 A 再做 B 再做 C"——A 已经改了文件,做 B 时回滚再改、做 C 时又回滚——太累。

理想:fork 3 个分支同时探索——最后看哪个最好。

场景 2:A/B 测试

用户让 AI 写一段文案——两个不同风格。用户想看每个风格的样子分别什么样——fork 两个分支生成。

场景 3:撤销决策

AI 在某个 turn 做了不对的决策——用户想"回到那个 turn 之前重新走"。

fork 让用户能在任意 turn fork——从那个点开始走新路径,旧的决策保留作记录。

场景 4:实验

用户想"试试看 AI 会不会做 X"——但不想真的执行。fork 一个分支让 AI 试——如果不对就丢、对就保留。

这 4 个场景都需要"session 级版本控制"。AI Agent 产品要支持这种 fork——给用户探索的自由

但 fork 不是简单的"复制 session"——有很多产品判断:

  • 复制什么?(messages、文件状态、snapshot……)
  • 不复制什么?
  • fork 之间是否完全独立?
  • 文件改动如何处理?
  • snapshot 如何处理?

opencode 实现了 fork——我们看具体怎么做。

二、案例:opencode 的 fork 复制消息但不复制 snapshot

R6-05 / R6-13 阶段的研究告诉我们——opencode 在 packages/opencode/src/session/session.ts 里有 fork() 函数。它的逻辑大致是:

async function forkSession(sourceSessionId: string, atMessageId?: string) {
  const newSessionId = generateSessionId()
  
  // 复制 messages 历史(截到 atMessageId 处,如果指定了)
  const messages = await getMessages(sourceSessionId)
  const cutMessages = atMessageId 
    ? messages.slice(0, messages.findIndex(m => m.id === atMessageId) + 1)
    : messages
  
  await saveMessages(newSessionId, cutMessages)
  
  // 复制 session metadata(title、agent type 等)
  const metadata = await getSessionMetadata(sourceSessionId)
  await saveSessionMetadata(newSessionId, { ...metadata, parent: sourceSessionId })
  
  // **注意:snapshot 不复制**——新 session 从空 snapshot 开始
  // 文件状态保留——两个 session 共享相同的文件系统
  
  return newSessionId
}

注意几个关键设计。

关键设计 1:复制 messages

fork 后的新 session 拥有原 session 的对话历史——一直到 fork 点。

这让 LLM 看到"我之前做过 X、Y、Z"——保持上下文连贯。

如果不复制 messages——新 session 是空的——LLM 不知道之前的工作,得从头开始。

关键设计 2:可在中间点 fork

atMessageId 参数让 fork 在任意 message 点发生。不必从 session 末尾——可以从 turn 5、turn 10、turn 任意位置 fork。

这让用户能"回到某个时间点"再走新路径——非常灵活。

关键设计 3:metadata 关联但独立

新 session 知道自己的 parent——通过 parent: sourceSessionId 字段。但状态独立——新 session 后续的工作不影响原 session。

这种"关联但独立"让 UI 能展示 fork 关系(树状),但工作上各跑各的。

关键设计 4:snapshot 不复制

这是最有意思的设计——snapshot 不复制

什么是 snapshot?opencode 的 snapshot 是文件状态的"快照"——某个时间点项目里所有文件的内容。每次 tool 调用(特别是 edit / write)后都会创建新 snapshot——形成时间线。

如果 fork 时复制 snapshot 历史——新 session 拥有原 session 的所有 snapshot 记录。理论上能"回滚到原 session 任意时间点"。

但 opencode 这么做。新 session 的 snapshot 历史是空的——从 fork 那一刻重新开始

为什么不复制?

理由 a:snapshot 是文件系统级

snapshot 不是抽象数据——它存的是真实文件内容。复制 snapshot 意味着复制 N 倍的文件数据——成本高。

理由 b:文件系统是共享的

两个 session 都在同一个项目目录工作。它们看到的是同一份文件——文件系统层面无法隔离(除非真的复制目录,但那是巨大开销)。

既然文件共享——复制 snapshot 没意义。两个 session 的 snapshot 即使复制了,实际文件还是同一份——snapshot 历史也只是引用。

理由 c:fork 后的工作通常会改文件

fork 出来就是为了"试新方向"——通常会改文件。新 session 改文件——会产生新的 snapshot。

老 snapshot 历史在新 session 里没意义——因为新 session 是要走不同方向、产生不同 snapshot。

不复制反而清晰——新 session 从空 snapshot 开始、自己的工作自己留 snapshot。

关键设计 5:文件状态共享

这是 fork 设计最重要的认知——两个 session 共享同一份文件系统。

具体说,如果 session A 在 fork 时改了文件 foo.ts——文件真的被改了。Fork 出 session B——B 看到的 foo.ts 是已经改过的版本。

B 想再改 foo.ts——会覆盖 A 的改动。B 改了之后 A 再继续——A 看到 B 的版本。

这是个有意思的"半隔离"——session 间逻辑独立,但文件系统物理共享。

这种设计的优点是简单——不需要复杂的 file overlay、不需要 sandboxing。缺点是用户必须意识到——两个 session 同时改文件会冲突。

opencode 的解决方案:不鼓励"两个 session 同时活跃改文件"。Fork 后通常是"切到新 session 继续工作、老 session 暂时不动"。

或者反过来——用 session fork 做"决策树探索",但每个分支都先 commit 到 git——这样切回老分支能 git checkout 回去。

这是个产品判断——优先简单的实现,依赖用户配合而不是技术防护。

对于"我想真正隔离两个 session 的文件改动"——用户应该用git worktree 或者复制整个项目目录。opencode 不提供这种隔离。

三、设计启示:什么该共享、什么该隔离

这一章的核心论点:Fork 是个"半隔离"的产品概念——逻辑独立但物理共享。用户必须理解这个边界

如果你做 AI 产品考虑 fork 功能,下面几条原则有用:

1. 想清楚 fork 的"目的"

是为了对比方案?回到时间点?做实验?

不同目的对应不同实现:

  • 对比方案 → 可能需要文件级隔离
  • 回到时间点 → snapshot 回滚机制
  • 做实验 → 临时分支,废弃容易

2. 默认共享文件系统

除非用户明确要求隔离——文件系统共享。

完全隔离(每个 session 独立目录)成本太高、用户用起来累。

让用户决定要不要 git branch 真隔离。

3. messages 必须复制

LLM 需要看到 fork 前的对话历史——否则 fork 后 LLM 像失忆。

复制 messages 到 fork 点——LLM 有连续性。

4. snapshot 不复制

snapshot 是文件状态的引用——既然文件共享,snapshot 复制没意义。

新 session 从空 snapshot 开始——自己工作自己留 snapshot。

5. 让 fork 操作可见

UI 上让用户看到 fork 关系——树状结构或者列表。

让用户知道"这个 session 是从那个 session fork 来的"——便于管理。

6. 不鼓励"两个 session 同时改文件"

明确告诉用户——同时活跃的 fork 会有文件冲突风险。

提供工具帮用户 commit 当前进度——让 fork 间的切换有清晰边界。

7. 给 fork 一个"清理"机制

fork 多了——session 列表混乱。

让用户能 archive / delete 不要的 fork——保持列表整洁。

8. 不要假装 fork 是"魔法 isolation"

技术上 fork 不是真隔离——是"逻辑独立 + 物理共享"。

不要 marketing 它为"完美 isolation"——用户用了发现冲突会失望。

诚实告诉用户边界。

最后一个观察。Fork 这种"session 级版本控制"是 AI 产品慢慢成为主流的功能

早期 AI Chatbot 没有这个概念——session 就是 session、改了就改了、想回到原样要重开。

现在主流 AI Agent 都在加 fork 功能——Cursor 有 conversation fork、ChatGPT 有 branching、Claude.ai 有 conversation tree。用户开始期望"AI 对话也是可分叉的"——就像 git 那样。

opencode 的 fork 实现是个不错的范本——简单(不做过度隔离)、清晰(共享什么独立什么明确)、灵活(任意点 fork)。

如果你做 AI 产品要加 fork——抄这个模板就行。不要过度设计——简单的 fork 已经能满足 90% 用户需求。

下一章 5.8 是第 5 篇的收尾——我们看 multi-agent 的一个潜在风险递归 spawn——避免 AI 无限召唤自己的设计。