08|Harness内部:决策、执行、持久化与崩溃恢复
目标:把概率性建议包在可观察、可恢复的执行系统中;知道何时不需要框架。实验:
journal.py、capstone.py。
本章目录
沿着对象与状态理解本章
崩溃恢复必须保留外部动作与本地记录之间的缺口,不能靠补一句摘要解决。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
超时告诉你没收到,不告诉你没发生
为什么自动重试有时会把一次工作做两遍?
外部服务可能已经完成动作,只是成功消息没回到调用方。此时“未知”是精确状态,不是系统不够聪明。对于会产生新记录、发送消息等副作用的动作,恢复需要稳定操作标识、结果核对或服务支持的幂等语义。只读查询的重试通常不需要同样复杂的设计。
输入与适用条件:构造的外部写入场景;不承诺跨数据库、文件系统和远程API天然具备“恰好一次”事务。
1 / 6 · 请求 op-1
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
创建一条记录;客户端等待回执
教学状态默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
在不可靠网络下维持正确副作用次数。
怎样实现
先判断操作是否可重复,再决定重试、查状态、对账或人工确认。
可能的代价与误解
只在本地记一个成功标志,不能消除“远端已提交、本地未记录”的窗口。
如何判断是否做对
故意丢回执并重试,核对最终仅存在一条逻辑记录。
1. 最小Agent循环其实有三种状态
对话状态描述模型看到了什么;任务状态描述目标、已完成动作与未决问题;环境状态描述文件、数据库、服务的真实内容。三者可能不一致。模型“记得已运行测试”,不等于测试对应当前代码;数据库里记着“正在提交”,不等于外部提交没有发生。
本书的最小循环是:构造上下文,获取模型动作候选,校验,执行,记录结果,更新状态,再决定继续或结束。无需默认引入规划Agent、审查Agent、记忆Agent和评分Agent。只有具体失败模式证明某种分工有收益,再增加它。[N01][N30]
为了便于诊断,内部事件可区分proposed、authorized、started、succeeded、failed和unknown。这些是副作用操作的局部状态,不要求每个普通读文件都建立复杂状态数据库。任务规模和动作风险决定持久化粒度。[N87]
2. 为什么必须有unknown,而不能只有成功与失败
时间线:写入请求已经到达远程服务,远程服务执行成功,但网络连接在返回结果前断开。客户端看到超时,无法仅凭本地信息判断是否执行。若把超时当失败重发,就可能重复创建订单或发送邮件;若当成功,则可能虚假报告。
正确恢复依赖外部系统的能力:支持幂等键时,用同一业务操作键查询或重试;能读取最终状态时进行对账;两者都没有时,保持未知并转人工确认。重试次数不是解决问题的核心,语义才是。
本地文件也有类似窗口:已原子替换文件,但进程在写“完成”日志前崩溃。下次执行不能只看日志started就再次修改。必须比较当前工件是否已经是期望结果。
3. 本书动作日志的实现
journal.py使用SQLite记录task_id/op_id、相对路径、修改前哈希、修改后哈希和状态。写入前先比较当前内容与expected。若操作ID已存在但参数不同,拒绝复用;若文件已等于目标内容,恢复为已对账,而不重复写入。
记录意图(SQLite提交)
→ 检查文件前置版本
→ 同目录临时文件写入并fsync
→ 原子replace
→ 故障注入:进程立即退出
→ 重启读取日志和文件
→ 当前哈希等于after:标记reconciled
测试不是修改一个布尔变量模拟崩溃,而是启动子进程,在文件替换后调用os._exit(75)。父进程随后恢复并检查文件内容与日志。因此它验证了一个真实本地崩溃窗口的恢复机制。它没有验证断电、目录fsync、并发写者、跨数据库事务或远程Exactly-once。
SQLite事务能够约束自身数据库的更新,但不能自动覆盖外部文件替换或远程API;两个系统没有共享事务时,需要对账、幂等或补偿,而不是在表里写一个transaction_id就获得原子性。[N31]
4. Compare-and-swap为何比“直接应用模型补丁”可靠
模型根据版本A生成补丁,用户此时改成版本B。执行器若不检查就写入,会覆盖用户修改。CAS要求“当前内容仍等于我读到的expected”,否则返回stale并重新读取。版本可用commit、ETag或内容指纹,选择取决于工件边界。
内容哈希用于这里的并发前置条件是有实际语义的;但把每篇技能文档的全文哈希写死在测试里,仅因文字改动就失败,通常只是脆弱快照。相同技术在不同位置价值不同,不能一概反对或一概滥用。
本书实现是单写者参考,没有对恶意进程的TOCTOU防御。路径检查和随后的打开之间,文件系统仍可能改变;生产隔离需要目录句柄、安全打开标志、容器/沙箱或权限限制。不要把教学CAS包装成安全产品。
5. 完成判定应该绑定当前工件
capstone.Harness记录测试开始和结束时的工件版本。若测试通过后文件又改变,原绿色结果失效,finish拒绝把它作为当前证据。修改后的注释也会使本教学哈希失效,这是保守的工件新鲜度选择;大型仓库可使用依赖感知策略,但必须证明遗漏风险可接受。
同时,测试结果不等于用户目标全部达成。一个功能可能通过测试却缺少迁移、文档或部署步骤。完成核验应按任务原始要求检查必要事项,而不是固定要求所有项目都提交一套重型材料。
错误恢复也需要停止条件。模型连续两次相同工具失败、没有新证据、动作未变化时,应切换定位方式或明确阻塞,而不是循环直到额度耗尽。对于可恢复的临时限流,可以退避;对于权限拒绝,不应通过改命令绕过。
6. 单Agent与确定性控制的边界
哪些动作交给模型?不确定的定位、候选方案、证据关联和取舍。哪些交给程序?解析、授权、文件版本比较、幂等键、超时和计费预算。并非所有业务判断都能写成确定性规则,但能可靠验证的部分不应只要求模型“务必认真”。
规划可以是一小段任务状态,不一定另开Agent。评审可以在当前上下文中核对diff与测试,不一定每次重新读取整个仓库。数据库可以是一份本地文件,不一定首先设计事件总线。这里的目标是减少真实失败,而不是最大化架构组件数。
外部文件写入与本地数据库提交之间的崩溃窗口
输入与条件。流程先os.replace写v8,再把数据库动作c7标成功;在两步之间进程崩溃。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1崩溃前 | 文件已v8 | 数据库仍running。 |
| 2重启 | 若只看数据库重做写操作 | 可能产生重复副作用或覆盖新版本。 |
| 3核对 | 对比当前文件内容、预期工件和动作身份 | 确认c7的外部结果是否已发生。 |
| 4修复状态 | 证据一致时补记成功 | 不是重新盲目执行。 |
| 5跨系统限制 | 若外部动作不可查询 | 本地事务无法神奇覆盖它,需幂等接口、补偿或人工核查。 |
把过程连起来。两个各自原子的操作并不自动形成一个跨资源原子事务。
边界与反例。这描述常见失败窗口,不能给一般外部写操作提供恰好一次保证。
7. 验收题
子进程写文件后崩溃,恢复时文件既不等于before也不等于after,应该怎么做?答案:视为外部修改或状态冲突,停止覆盖,保留证据并重新定位;不能猜测哪一版本“更合理”。
取消请求已发出,能否立即宣布所有副作用停止?不能。需要知道执行器是否支持取消,以及取消到达时操作的状态。无法撤销的外部动作必须继续对账。可靠Agent首先是可靠执行系统,然后才是高质量语言模型。