Resume 是”在同一份会话档案上继续往下写”:沿用原始 session ID、消息按时间顺序追加到原 .jsonl 文件,副作用是改写原会话。Fork 是”在岔路口把历史复制一份再另开炉灶”:分配全新 session ID、复制分叉点前的消息链到独立 .jsonl,原始文件保持不变,副作用是数据双开。两个动作都吃”历史”,但一个改源、一个复制,对后续能否多端并行、回滚、审计的影响截然不同。
一、先把”Resume”和”Fork”在概念上拉开
会话管理里,”Resume”(恢复/续接)把进程重新挂回某个已存在的会话,让对话从断点继续推进;”Fork”(分叉/派生)则是在现有会话的某个时间点做一次”快照复制”,从那一刻起走出第二条独立分支。前者像打开一份文档继续编辑,后者像把文档复制一份另存为新文件再改。
很多 Agent 客户端把”续接”和”分叉”做成一组互斥选项:在调用 --resume 时把 fork_session 设为 false(默认)即续接,设为 true 即分叉。这两套动作的差别不是”用哪个都行”,而是”对历史数据做什么”。
二、核心数据副作用:一条表就能讲清
理解 Resume 与 Fork 的差异,最直接的办法是看它们对”原文件”做了什么。下表把关键维度并列对照。
| 维度 | Resume(续接) | Fork(分叉) |
|---|---|---|
| 原始 session ID | 沿用原 ID 不变 | 沿用原 ID 不变(仅作为来源) |
| 当前 session ID | 沿用原 ID | 分配全新 UUID |
| 历史记录文件 | 在原 .jsonl 末尾追加消息 | 复制分叉点前的消息到新 .jsonl,后续各自追加 |
| 原会话文件 | 被修改(线性追加) | 保持原状,零改动 |
| 后续消息归属 | 全部写入原 ID | 写入新 ID;原 ID 不再变化 |
| 适用场景 | 同一任务继续推进 | 并行试验、A/B、回滚保护 |
| 风险点 | 多端同时 Resume 会写入串行 | 分叉无法合并回主分支 |
也就是说,”对原始数据的修改”是 Resume 唯一的副作用;Fork 唯一的副作用是”在磁盘上多出一份历史副本 + 一个新 ID”。
三、Session ID 的处理逻辑
Resume 在 ID 上的处理最简单:调用 --resume <session_id> 后,所有新消息的 sessionId 字段继续指向传入的同一个 UUID,文件系统也只看到同一个 .jsonl 被不断 append。”ID 没变”是 Resume 成立的前提——下游审计、计费、记忆外挂都靠它做去重。
Fork 的处理则分两层:父 ID 仍保留在历史里被引用,但当前活跃进程拿到的是 SDK/CLI 自动生成的新 UUID。从外部看,原本一个会话变成两个会话:一个”父会话”(冻结在分叉点)、一个”子会话”(从分叉点继续)。在 Agent Client Protocol(ACP)等开放协议里,Fork 通常通过 unstable_forkSession({ sessionId }) 调用,调用方拿到的是全新的 sessionId,与原 ID 无任何继承关系。
四、历史记录的处理逻辑
Resume 走的是”线性追加”模型:从存储里读出截止上一次的全部消息,重新塞进上下文窗口,新消息按时间顺序 append 到同一份 .jsonl。这种模式最便宜,也最危险——多端同时 Resume 同一 session,两边的输入会交错写入同一份文件,事后回看会出现”消息乱序”。
Fork 走的是”复制分叉点 + 之后各自发展”模型:客户端在分叉瞬间把整段历史(分叉点之前的所有消息、工具调用、工具结果)复制到一份新文件,分配新 ID;之后的对话只追加到新文件。父文件从此静止,连分叉那一刻都不会被改。这种”父静子动”的设计让 Fork 在并发场景天然安全:两个终端分别跑不同的 Fork,互不污染。
工程上还有一个常被忽略的细节:Fork 复制的是”父会话分叉点前”的整段上下文,而不只是”我看到的那一句”。也就是说,Fork 出去的新分支一开始就和父会话”知识完全对齐”,但从此各走各路——这点是 A/B 测试和方案对比的命根子。
五、典型落地步骤
Resume 与 Fork 的实际操作通常分四步推进,区别只在第二步与第三步。
- 拿到要操作的 session ID(来自上一次启动的返回值或
/sessions列表); - Resume:把 session ID 作为
--resume传入;Fork:把 session ID 作为--fork-from或fork_session: true传入; - Resume 直接进入”继续对话”;Fork 则会先复制历史、生成新 ID,再用新 ID 开始对话;
- 写入文件:Resume 追加到原 .jsonl;Fork 创建新 .jsonl 并只往里写。
六、代码示例:两套调用对比
下面这段伪代码展示 Resume 与 Fork 在调用层面的最小差异。Resume 不带分叉参数,Fork 需要显式打开 fork_session 开关并接收新 ID。
# Resume:续接原 session
client = AgentClient.resume(session_id="sess-abc-123")
reply = client.send("继续完成上次的重构")
# 写入 sess-abc-123.jsonl 的末尾
# Fork:在分叉点复制历史并另起新 session
fork = AgentClient.resume(
session_id="sess-abc-123",
fork_session=True, # 关键:开启分叉
)
new_id = fork.session_id # 与 sess-abc-123 不同的全新 ID
fork.send("这次改用另一种方案试试")
# 写入 <new_id>.jsonl;原 sess-abc-123.jsonl 不再变动
七、常见误用与避坑
把 --resume 当 Fork 用:同一 session 在两个终端同时续接,事后翻历史会发现”对话交错”——这是 Resume 模型下最常见的脏数据问题,规避办法是要么用 Fork 给每个终端独立 ID,要么明确只用单端续接。
把 Fork 当”轻量级”操作:Fork 复制的是”截止分叉点”的完整历史,分叉点越靠后,磁盘与内存开销越大。长任务中途频繁 Fork,会留下多份大体相似却独立的小版本,需要定期清理。
混淆”记忆外挂”与”session 自身历史”:很多 Agent 把跨 session 的”长期记忆”写在外部存储。Resume 不会自动把 Fork 出去的新分支接到这些外挂上,需要在 Fork 之后显式继承或重写外挂 ID,否则会出现”两个分支共用同一份记忆”的串台现象。
常见问题(FAQ)
Q1:Resume 后原 session 的工具调用结果还在上下文里吗?
在,Resume 会把整段历史重新加载到上下文,包括所有工具结果;如果文件已变更,可能引发陈旧上下文问题。
Q2:Fork 出来的子分支能合并回父会话吗?
不能,主流 Agent 协议都没有”merge fork”语义;如需把成果合并,靠 git commit 等外部机制。
Q3:怎么判断一段历史是来自 Resume 还是 Fork?
看 session ID 与首条消息的来源标记;Fork 复制历史后写入的是新 ID,Resume 永远沿用原 ID。