Skip to content
工程案例研究
超凡98 / 100
证据34/35原创24/25结构20/20实用20/20
响应丢失之后,重试为什么必须先解决持久化幂等边界
CodeFlowMu 工程化实录 · 01

响应丢失之后,重试为什么必须先解决持久化幂等边界

没有收到成功响应,不等于动作没有发生。可靠重试的核心不是再执行一次,而是找回第一次已经产生的结果。

RBE-20260828-01工程案例 · 2026-08-31 修订

CodeFlowMu 工程化实录(一):响应丢失之后,重试为什么必须先解决持久化幂等边界

对一个 Agent Runtime 来说,“工具报错后能不能重试”并不是一个网络层问题。

真正危险的窗口是:副作用已经发生,但调用方没有收到成功响应。

任务已经写入磁盘,响应却在返回途中丢失。调用方恢复后只知道“这次调用没有拿到结果”,于是按同一业务意图再次提交。如果 Runtime 无法证明第一次到底做了什么,第二次“重试”就可能变成第二次真实创建。

CodeFlowMu 是什么,为什么这个问题值得单独工程化

CodeFlowMu 是一个本地优先的多 Agent 协作与数字员工运行体。它不只负责把提示词交给模型,而是让 PM、DEV、QA、OPS、EVAL 等不同职责的 Agent 在受控工作空间里持续执行真实任务,并由 Runtime 管理任务对象、执行上下文、工具调用、事件、恢复和证据。

这类系统与一次性聊天最大的不同,是工作会跨越更长时间,也会遇到进程退出、Host 中断、网络异常、调度恢复和重复唤醒。一次工具调用如果已经产生真实副作用,Runtime 就不能只依赖“调用方有没有收到成功响应”来判断动作是否发生。

因此,幂等在 CodeFlowMu 里不是一个接口优化项,而是数字员工能否可靠恢复的基础边界:系统必须能够区分“第一次创建”“同一次业务提交的恢复”和“另一份新的业务意图”。

这次问题正是在这样的运行条件下被暴露出来。

完整实验、版本边界与公开证据见:RBE-20260828-01 证据页

外部研究起点:异常并不能证明副作用没有发生

这条研究链的外部起点是 LlamaIndex 的 PR #22841fix(core): avoid retrying failed function tools

它处理的具体问题是:调用层先用一种参数形式真实执行 FunctionTool,遇到异常后又换另一种形式再执行。危险之处在于,第一次异常并不必然发生在副作用之前;如果第一次其实已经完成写入、发送或创建,那么第二次“尝试另一种调用方式”就可能重复执行真实动作。

LlamaIndex 的修复思路,是把参数形式的选择移动到真实调用之前,通过函数签名判断调用方式,避免用一次可能带副作用的执行去试探接口。

这个外部案例给 CodeFlowMu 的价值,不是提供一份可以照抄的补丁,而是提出了一个值得进入自身 Runtime 的故障模型:

当调用方看见异常时,系统是否能够证明副作用尚未发生?如果不能,重试依据是什么?

外部研究只负责提出问题。CodeFlowMu 是否存在同类工程缺口,必须由自己的实现和第一方实验回答。

第一方复现:同一个故障窗口,两条写路径给出相反结果

最初实验固定在 CodeFlowMu V2.0.4 提交 2ba1ad9b。实验不是生产事故统计,而是主动制造“动作已经完成、调用方却失去成功结果”的响应丢失窗口。

受测层次历史实验观察工程含义
上层内存去重第一次动作已经发生,但成功结果没有进入可复用缓存;恢复后相同调用再次进入执行路径进程内缓存不能解决跨恢复的结果未知
write_report相同任务、内容和提交标识重试,deduplicated=true,报告文件数保持为 1受测报告路径已经具备持久结果复用
write_task相同业务创建意图再次进入真实创建路径,分配新任务身份,TASK 数变为 2任务创建合同没有把稳定提交身份绑定到第一次结果

历史响应丢失实验:报告一份,任务两张

图 1:V2.0.4 的历史故障对照,不代表 V2.1.2 当前行为。图中计数只覆盖受测路径,不表示生产环境发生频率。来源:RBE-20260828-01 证据页

这组结果很重要,因为它否定了一个过于宽泛的结论:并不是“CodeFlowMu 所有写工具都没有幂等”。

恰恰相反,write_report 是一个重要反例。它说明上层允许重试,并不必然制造第二份副作用,只要底层接口已经拥有可恢复的持久身份和结果复用机制。

真正的缺口集中在任务创建。当时 write_report 已暴露 client_submission_id,而 write_task 还没有相应的正式持久幂等合同。调用方主观上认为“这还是刚才那一次提交”,并不等于 Runtime 已经持久保存并认可了这个业务身份。

工程判断:可靠重试不是“查重”,而是恢复第一次结果

发现两张 TASK 之后,最容易想到的修法是:创建前查一下有没有同标题任务,有就不创建。

这不是可靠幂等。

标题、正文摘要、发送者甚至时间窗口都可能相同,却代表两次真正独立的业务意图。反过来,同一次业务提交在恢复时也可能带着不同的 trace、连接、进程或重试次数。模糊内容匹配既可能漏掉重复,也可能误伤合法新任务。

因此 CodeFlowMu V2.1.2 没有把问题定义成“怎么更聪明地查重”,而是定义成:

如何给一次创建意图一个稳定、可持久化、可恢复、可冲突检测的提交身份。

这也是这次工程化最关键的变化:重试从调用方策略,变成创建接口合同的一部分。

V2.1.2:把“一次业务提交”变成持久事实

V2.1.2 在任务创建路径中建立了这条关系:

client_submission_id → request_digest → task_id / task_path → creation_result

client_submission_id 描述的是一次业务创建意图,而不是某一次 HTTP、MCP 或进程调用。需要跨响应丢失、跨进程恢复的调用方,必须在重试时复用同一个稳定 ID。

如果重试时重新生成一个 ID,Runtime 会把它理解为新的业务意图。这不是幂等失败,而是调用方给出了一个新的身份。

只有提交 ID 仍然不够。同一个 ID 如果第一次要求 DEV 修复登录,第二次却变成 OPS 发布环境,Runtime 不能因为 ID 相同就错误复用第一次结果。因此 V2.1.2 同时保存规范化的 request_digest

实现中的摘要方案为 write-task-v1:从 sender、recipient、subject、body、priority、thread_key、parent、references、depends_on、risk_level 等语义字段构造确定性 JSON,再计算 SHA-256。对象键排序,字符串统一 Unicode NFC 与换行,数组保留顺序;调用时间、重试次数、trace ID 等本次执行信息不进入业务摘要。

Runtime 同时保存 digest_schema_version。同一提交 ID 如果出现不同摘要或不同摘要版本,不会被静默解释成同一任务,而是返回 conflict,且不产生新的 TASK 副作用。

这是一份任务创建接口合同,不是把整个 Runtime 宣称成通用事务数据库。既有 write_report 去重语义保持不变,FCoP 的 TASK / REPORT / REVIEW / EVAL 协议也没有因为这次补丁而改版。

为什么还需要三阶段创建回执

如果只在 TASK 创建完成后再写一条“这个 submission 已经成功”的索引,仍然存在一个更窄但真实的崩溃窗口:

  1. TASK 已经成功落盘;
  2. 回执索引还没来得及写;
  3. 进程退出;
  4. 调用方带同一提交 ID 重试。

如果 Runtime 只看“最终回执不存在”,仍然可能错误地再创建一次。

因此 V2.1.2 把创建结果持久化为三个阶段:

reserved → task_created → committed

状态已有持久事实恢复规则
reserved提交 ID、摘要版本、摘要、预分配任务身份与路径TASK 尚不存在时沿原身份继续;已存在且内容匹配时接管原任务,不换号
task_createdTASK 已创建及文件摘要已经确认匹配时继续提交回执;文件反而缺失时返回类型化冲突,不能假装“从未创建”
committed完整机器可读创建结果同 ID、同摘要直接复用第一次结果,不再次执行创建

提交级互斥与任务序号保护解决并发占位;原子持久化和中间态恢复规则解决崩溃恢复。目标路径如果已经存在,但身份或内容不匹配,也不能通过覆盖文件或另建第二张任务绕开不确定性。

对于长期停留在中间态的 reservation,Runtime 提供只读诊断:可以观察当前状态、目标 TASK 是否存在、摘要是否一致以及 reservation 是否长期未推进。诊断只报告事实,不因为“超时”自动删除记录,也不自行替业务层重新创建或裁决任务。

createdreusedconflict:把重试结果变成机器语义

dispositionaction_taken含义
createdtrue本次调用完成首次创建
reusedfalse对应创建结果已经存在,返回第一次的权威任务身份
conflictfalse同一提交身份与已有摘要或恢复事实冲突,没有新增 TASK

action_taken=false 并不必然意味着请求失败。在 reused 场景里,它恰好说明 Runtime 没有再执行第二次创建,却仍然找回了第一次的成功结果。

这里的“成功”只表示任务对象创建结果被恢复,不代表任务已经执行完成、QA 已通过或业务已经交付。技术幂等不能替代上层治理状态。

兼容路径也没有被包装成更强保证。旧调用方不提供 client_submission_id 时,仍然可以使用原有 legacy 创建方式;但这种调用不会被宣称拥有跨响应丢失、跨进程的强幂等语义。

独立 QA:不是看有没有报错,而是看最终只剩几个权威对象

实现完成后,未参与实现的 QA 独立执行响应丢失和并发场景。验收关注的是实际 TASK 数量、task_id 是否一致,以及创建回执是什么。

场景独立 QA 实际观察
A2:创建成功后模拟响应未收到,再用同一提交身份重试TASK 数为 1;前后 task_id 相同;第二次返回 reused / action_taken=false
A4:8 路并发使用同一提交 ID 和同一摘要created=1 / reused=7;唯一 task_id;TASK 数为 1

进程重启后的结果复用、同 ID 异摘要冲突、legacy 返回兼容、中间态文件缺失与 stale reservation 诊断另有定向测试。

这里必须保留证据边界:A2/A4 的独立 QA 证明的是受测任务创建合同在两个关键故障模型中收敛,不等于“所有工具、所有 Host、所有网络环境都已经被独立验证”。

但对本次工程问题而言,前后差异已经足够明确:

历史受测版本中,同一创建意图在响应丢失后可以产生两张 TASK;V2.1.2 中,提供稳定提交 ID 和一致摘要的受测重试、并发与重启路径会收敛到同一个权威任务身份。

从补丁到产品版本:还必须经过发布门禁

V2.1.2 于 2026-08-30 完成私有母版 Runtime/Shell 发布。公开文章不直接链接私有 CodeFlowMu 仓库;对外可核查的版本事实、脱敏 QA 结果与范围说明统一保留在本文的公开证据页

最终发布验证记录为:

  • Runtime:1842 pass / 0 fail / 1 skip
  • Shell:1037 pass / 0 fail / 0 skip
  • V2.1.1 与 V2.1.2 同协议关键矩阵分别连续 10 轮通过:Runtime 1630/1630、Shell 550/550
  • typecheck、Shell build、安装器契约、规则和版本一致性通过。

这些数字是发布测试集合的实际结果,不应该被相加成所谓“幂等可靠率”。

本次发布对象是 CodeFlowMu 私有母版 Runtime/Shell,不是 Open Dev Team Edition,也不代表真实 LAN/Gateway、浏览器 profile 或用户生产项目已经被同一套夹具覆盖。Windows 符号链接权限导致的 1 项 skip、既有依赖审计告警以及 Python 定向夹具 warning 继续保留在证据说明中。

这次工程化真正改变了什么

过去,调用方看到的是“请求没有成功返回”,只能自行决定要不要再试。

现在,在提供稳定提交身份的前提下,Runtime 可以回答三件不同的事:

  • 这是第一次创建,执行并返回新结果;
  • 这是同一次业务提交的恢复,返回原来的结果;
  • 这次请求与已经占用的提交身份冲突,拒绝产生新的副作用。

这使“重试”从一种带猜测的调用策略,变成可以被持久记录、恢复、审计和测试的工程合同。

对于数字员工运行体,这个变化比“自动重试几次”重要得多。数字员工会长时间运行、跨进程恢复、由调度器重新唤醒,也可能在 Host、网络和工具层遇到不确定失败。如果每一次恢复都要靠 Agent 猜“刚才是不是已经执行成功”,系统就没有真正跨过从 Demo 到生产运行体的边界。

可以迁移到其他 Agent Runtime 的检查方法

对每一个会产生副作用的工具,至少检查:

  1. 副作用已经落盘、响应丢失后,进程重启能否找回第一次结果?
  2. 相同稳定 ID、相同请求是结果复用,还是再次执行?
  3. 相同 ID、不同请求是否明确冲突,并保证零新增副作用?
  4. 并发请求的“检查 + 占位 + 创建”是否真的受到一致并发保护?
  5. 中间状态能否沿原身份恢复,而不是靠另建对象逃避不确定性?
  6. 旧调用是否保持兼容,同时没有被错误宣传为拥有同等级别的幂等保证?
  7. 已经正确的其他工具路径是否保持原有保护,没有在统一改造中退化?

如果一个 Runtime 只能回答“失败了就再试一次”,它还没有真正定义副作用边界。

更可靠的答案应该是:每一次可重试的业务意图,都有一个能够跨响应丢失、并发和进程恢复继续存在的持久身份。

工程结论

这次 CodeFlowMu 工程化没有证明“所有 Agent 工具都可以安全重试”。它证明了一个更窄、也更有价值的命题:

外部研究提出的失败模型,可以通过第一方正反实验收窄为具体接口缺口,再通过持久提交身份、请求摘要、创建回执、恢复状态机和独立 QA 变成一个可执行的 Runtime 合同。

响应丢失真正暴露的问题,从来不是“要不要再调用一次”。

而是系统有没有足够持久的事实,在第二次调用到来时回答:

第一次究竟做了什么。

证据范围与主要来源

  • 历史实验、V2.1.2 工程更新与公开版本说明:公开 JSON fixture、Reader 与 check 用于验证冻结历史材料的一致性;公开页同时给出脱敏实现、独立 QA、发布门禁和残余风险说明。
  • LlamaIndex PR #22841:提供外部故障模型与调用形式修复背景,不作为 CodeFlowMu 的实现或验收证据。
  • CodeFlowMu V2.1.2 的实现、独立 QA 与发布原始日志位于受限私有母版中;本文不向公共读者提供不可访问的私有仓库链接。
  • 真实生产发生频率、全部操作系统、真实 LAN/Gateway 与用户生产项目不在本文证据覆盖范围内。

Last updated: