Session Fork
让 AI 同一段对话'分叉'
一、原理:探索性 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 无限召唤自己的设计。