跳到主要内容

Lab 01 · Loop 恢复:副作用只执行一次

在外部副作用已经提交、结果事件尚未落盘时注入退出,验证 operation_id、事件日志与恢复检查点。

本章任务

要回答的问题

进程死在副作用提交与结果落盘之间时,恢复怎样避免重复执行?

读完你能

  • 识别工具执行前、执行后与验证前三个持久化边界
  • 用 operation_id 查询已提交状态并修复缺失事件
  • 证明重复恢复不会增加副作用次数
适合现在读
正在实现 Agent Loop、后台任务、支付、发信、发布或其他有副作用工具的工程师
先修知识
理解 JSONL 与 append-only event log;知道幂等键与至少一次投递的基本问题
实践产物
一份 Loop 恢复事件日志、效果账本和零重复副作用验收报告
证据边界
本实验用本地账本模拟外部系统;真实服务仍需提供幂等键、提交查询或补偿动作

一次 publish_report 已经把结果发到团队知识库,但 Harness 还没来得及把 tool_result 写进事件日志,进程被杀。重启后只看对话历史,Agent 会再次调用工具。

本实验把事实拆成两份:

  • effect-ledger.json:模拟外部服务,记录副作用是否已提交;
  • events.jsonl:Harness 自己的可重放时间线。

你要故意制造“两份事实短暂不一致”,然后证明恢复器会查外部账本、补写事件,而不是重复发布。

状态已知事实恢复动作
planned模型决定调用工具,副作用还没发生允许按同一 operation_id 执行一次
外部账本已有记录,日志没有 effect_committed副作用发生,Harness 在落盘前崩溃查询提交状态,跳过执行,补写事件
result_persisted工具结果已经耐久化不再执行工具,只继续验证
verified副作用数、结果和完成条件已核对重复 Resume 应成为无操作

关键不是让所有文件永远同步,而是让恢复器知道谁是真相源,以及怎样从不一致状态收敛。

从项目根目录运行:

Terminal window
python3 public/lab-assets/loop-recovery.py --reset --crash-at after_effect

第一次会输出:

INJECTED_CRASH boundary=after_effect

退出码是 75。此时外部效果账本已有记录,事件日志只有 planned。继续恢复:

Terminal window
python3 public/lab-assets/loop-recovery.py
python3 public/lab-assets/loop-recovery.py --report

合格结果应包含:

PASS duplicate_effects=0

再把 --crash-at 改成 before_effectafter_result,分别验证执行一次和只重跑 Verifier。

打开 .agent-mechanics-lab/loop-recovery/events.jsonl。正常恢复后的顺序应为:

planned
effect_committed # recovered_from_ledger=true
result_persisted
verified

recovered_from_ledger=true 是本实验最重要的证据。它说明恢复器不是根据最后一条模型消息猜测,而是查询了副作用真相源。

然后检查 effect-ledger.json:同一个 op_publish_report_v1 只能出现一次。连续执行恢复命令两次,事件数和副作用数都不应增加。

本地 JSON 账本可以替换成:

  • 支付 Provider 的 idempotency key 与 payment intent 查询;
  • 邮件服务的 message id 与发送状态查询;
  • GitHub / GitLab 的 deployment id;
  • 数据库事务表中的唯一 operation_id
  • 无法查询时的补偿动作与 needs_review 状态。

不要只给工具函数加重试。工具契约至少要返回 operation_id、提交状态、可查询标识和结果持久化状态。

验收:恢复必须证明零重复副作用

Section titled “验收:恢复必须证明零重复副作用”

提交报告时记录四个数字:

  1. 每个故障点的恢复耗时;
  2. 同一 operation_id 的副作用次数;
  3. 恢复后补写的事件数量;
  4. 需要人工介入的次数与原因。

通过标准是:三个边界都能解释,副作用次数始终为 1,重复 Resume 不改变状态。无法查询外部提交状态时,正确结果是 needs_review,不是静默重做。

展开实验复盘与加分项
  • 为什么 events.jsonl 不能单独证明副作用是否执行?
  • operation_id 应由模型、Harness 还是业务系统生成?
  • 外部服务只有“至少一次”接口时,怎样设计补偿?
  • 把事件日志最后一行截断后,恢复器应该怎样处理?
  • 加分项:为 effect-ledger.json 增加并发写入测试,并解释文件锁与数据库唯一约束的差别。