Appearance
历史执行怎样重放和分叉:Replay、Determinism、Fork 与 Time Travel
前置:09-06 Checkpoint/Resume;理解 deterministic execution record
核心问题:重放历史时哪些步骤可以重新计算,哪些外部副作用绝不能再次执行,Replay、Resume 和 Fork 的边界在哪里?
“Replay”这个词最容易让人误解
假设一个 Run 已经执行到:
text
read repository
→ model decides patch
→ write file
→ run tests
→ WAITING_APPROVAL审批后恢复时,系统绝不能简单理解成“从函数开头再跑一遍”。write file、外部 API、随机数、当前时间、模型调用都可能产生新的事实。
因此先区分三件事:
text
State reconstruction
= 用 checkpoint/event 恢复已提交事实
Deterministic workflow replay
= 重新走控制代码,但复用历史 task/result,使同一个执行回到原路径
Live re-execution
= 真正重新调用 Model / Tool / External System这三种行为的风险完全不同。
恢复的第一原则:已经提交的事实不要靠“再做一次”证明
如果 Node test 已经有 durable completion record:
text
node_id = test
attempt = 2
status = SUCCEEDED
exit_code = 0Resume 应读取这个事实,而不是重新跑测试来“确认它还是成功”。重新执行属于新的运行行为,可能受到代码、环境、时间或外部服务变化影响。
真正需要重新执行的通常是:
- 尚未形成 completion record 的 attempt;
- 明确声明可重新计算的纯步骤;
- 用户主动创建的新 Fork;
- reconciliation 后确认必须补做的操作。
Determinism 约束的是“恢复路径”,不是要求模型本身确定
Durable workflow 常要求恢复时能够再次走到同一个逻辑位置。危险代码包括:
python
if random.random() > 0.5:
...
if datetime.now() > deadline:
...
result = requests.get(url) # 直接嵌在 replay control flow如果这些值没有被记录,resume 时可能走另一条分支。
更可靠的模型是:
text
non-deterministic operation
→ 作为显式 Task/Node 执行
→ result durable commit
→ control flow 只消费已记录结果模型调用也属于 non-deterministic operation。不要要求 LLM 在 replay 时再次生成完全一样的文本;要持久化足以恢复控制流的规范化结果或 execution record。
不同 runtime 的具体实现不同:有的从 node boundary 恢复,有的会 replay workflow body。设计 Shadow Harness 时应明确自己的恢复粒度,而不是模糊使用“replay”一词。
Side Effect 必须从 Replay 边界中剥离
外部副作用不能因为“历史控制流重新走到这里”就自动再次执行:
text
Replay reaches send_email
↓
lookup operation_id
↓
already committed? ─ yes → reuse fact
│
no
↓
execute with idempotency / reconciliation对于无法可靠去重的高风险操作,恢复策略宁可进入 NEEDS_RECONCILIATION,也不要猜测第一次是否成功。
🔍 源码对照:Approval identity 让“同一次副作用”有可验证身份
完整源码与通用分析见 11-05 HITL / Approval / Resume。这里不重复代码;这里关注 Replay / Fork:重放必须保留或显式重建 approval 与 side-effect identity,历史批准不能自动授权新的参数或新的动作。
源码说明
transcript mirror、initial transcript load 和 storage 能提供“历史如何被重新投影进运行环境”的案例,但它们不等价于 Event Sourcing。本文读源码时最重要的训练,是区分 transcript replay(重建模型可见历史)、state reconstruction(重建 Runtime facts)和 side-effect replay(通常不能简单重做)。
工程边界
不要因为存在 transcript/history 就宣称可以 deterministic time travel。模型 provider、tool world state、prompt/tool schema version 都可能让重放偏离原执行。
Shadow Harness 对照
- Replay 输入绑定 RunManifest:model/prompt/tool schema/retrieval/code versions。
- Fork 创建新 lineage,不修改原 execution history;external side effects 默认不 replay。
验证
对一个固定 ScriptedModel Run 做 replay,再改变 Tool schema version 做第二次 replay;第一条应相同,第二条必须被 manifest compatibility check 拒绝或显式迁移。
Fork / Time Travel 是新执行,不是修改旧历史
Fork 从历史 checkpoint 创建新的分支:
text
original run: r-100
checkpoint: cp-7
↓ fork
new run/branch: r-101
parent = r-100@cp-7新分支可以换 Plan、Context、Model 或人工修正状态,但必须:
- 拥有新的 branch/run identity;
- 保存 parent lineage;
- 不覆盖原 trace/checkpoint;
- 对外部副作用重新评估 operation identity,避免错误复用旧幂等键。
Time Travel 的价值主要是调试、对比策略和人工恢复,不应该把历史事实改造成“仿佛从未发生”。
Shadow Harness 的 Replay Policy
建议把“是否执行”显式化:
python
class ReplayPolicy(Protocol):
def decide(self, execution: NodeExecution) -> Literal[
"REUSE_COMMITTED_RESULT",
"REEXECUTE",
"RECONCILE_FIRST",
"REJECT",
]: ...Decision 依赖:
- node 是否纯计算;
- 是否已有 durable completion;
- side-effect classification;
- tool schema / runtime version 是否兼容;
- operation id 是否仍有效;
- 当前是 resume 还是用户主动 fork。
这样“replay”不会退化成一个危险的 run_from(checkpoint) 开关。
Failure Modes
Resume 时重新调用所有非确定性步骤
后果: 模型结果、时间、随机数或远程数据变化,控制流偏离原 Run。
处理: 已完成步骤复用 durable result;非确定性操作放入可记录的 Task/Node boundary。
Replay 把外部副作用当普通函数
后果: 邮件、支付、部署、写文件等再次发生。
处理: operation identity + idempotency/reconciliation;无法确认时 fail closed。
如何验证
| 场景 | 必须成立 |
|---|---|
| checkpoint 前生成随机值,之后 crash | resume 使用第一次已提交值,不改变分支 |
| Model Node 已完成后 resume | 不再次调用 Provider |
| side effect 成功但本地 completion 未确认 | 先 reconciliation,不直接重复执行 |
| 从旧 checkpoint fork | 新 branch 有独立 identity,原历史不可变 |
关联源码
自测
- 历史 Run 中包含真实外部写操作。Replay 为什么不能默认再次执行这些副作用?哪些步骤需要 deterministic substitute 或 recorded result?
- Fork 从某个历史点创建新分支时,哪些旧状态可以继承,哪些 environment facts 必须重新验证?