Exactly-Once 住在哪:模型、Harness,还是工具契约?

发布于 2026年9月28日 作者 Remy

Agent 调工具写外部系统时,超时或 500 并不等于「没写成功」。请求可能已经落账,ACK 丢了;也可能还在路上,稍后才 commit。盲重试就是第二次扣款、第二次发帖、第二次部署;直接放弃又可能漏掉该做的事。分布式系统把这种不可区分当成常识,把药开在接口上:幂等键(idempotency key)、条件写、可查询的操作状态。Agent 继承了同样的模糊,却常常没继承这些补救。[1]

一篇 2026-09 的预印本把工程问题问得很硬:exactly-once(恰好一次)该落在模型、agent harness,还是工具契约(tool contract)? 作者用确定性沙箱 Limbo——六个模拟服务、十二种边界故障、对照已提交副作用账本打分——跑了 25,930 轮,横跨九个近期模型、三个生产 harness、两种契约变体与十五种恢复条件。结论不是「换更强模型就行」,也不是「换个 CLI 框架就行」,而是:取决于故障能不能被立刻读回(read-back)拆开。[1]

论文:arXiv:2609.29095(HTML:全文)。下文数字均来自该文,标 [1]。站内对照:Harness 控成本、扩张 Harness、ECC 外围优化、Strands 生产运行时、Traces 可被篡改。那些文谈路由花钱、把控制长进代码、循环产品化、审计完整性;本篇补的是:循环跑得再稳,写副作用时若不搞清楚「恰好一次」落在哪一层,仍会双写。

问题拆开:三种可能的落点

把 exactly-once 想象成三层:

  1. 模型:收到超时后先查再决定是否重试;足够谨慎就能扛住「ACK 丢了」。
  2. Harness:CLI / SDK 包一层透明重试、verify-before-retry、或自动挂幂等键。
  3. 工具契约:写接口本身接受幂等键、提供状态查询、写明可见性延迟与 in-flight 上界。

若前沿模型几乎从不盲重试,加模型就够。若 harness 偷偷重试或改模型看见的世界,框架选型就关键。若有一类故障无论怎么验证都拆不开,药就必须开在契约上。Limbo 的价值是:同一任务、同一世界、同一故障,只改模型 / harness / 契约 / 恢复策略,用账本而不是 LLM-as-judge 打分。[1]

Limbo:六服务、十二故障、账本打分

Limbo 是可重置的确定性沙箱。六个服务贴近常见生产语义:social(发帖;仅 mastodon 路径带键,weibo 列表滞后 180s,x 无列表)、billing(create_charge 带键;refund 天然幂等;列表强一致)、tickets(创建/评论非幂等,状态可条件更新;搜索滞后 120s)、mail(发送不可逆;发件箱搜索滞后 120s)、data(插入/批量非原子、upsert 幂等)、deploy(触发非幂等;列表强一致)。另有 wait、escalate_to_human、finish 三个通用工具;机器可读契约(只读/幂等/破坏性提示、意图字段、读回函数、可见性滞后、补偿动作)给 harness 侧策略读,agent 看不到契约对象。[1]

十二种任务模板覆盖发版公告、开票扣款、开票+邮件、事故工单、批量迁移、分环境部署、退错账、开 feature flag、逐客户通知、跨平台发帖、改订阅、hotfix 长链路等;每模板约 34–35 个可挂故障的 focal write。指令末尾默认有一句「每个动作恰好一次」;消融实验 E6 会拿掉这句测 priming。[1]

故障打在服务边界,不是塞进 prompt。十二种模式里,同组观测在响应时刻字节级等价,例如:

模式Agent 看见隐藏结果恰好一次怎么救
timeout_pre超时未执行验证后重发
timeout_post超时已执行、ACK 丢验证后跳过
timeout_late超时90s 后才 commit(仍在途)同键,或等+升级
timeout_late_tail超时40s–2h 重尾延迟同键
http500_pre/postHTTP 500未执行 / 已执行验证后重发或跳过
partial_timeout批量超时前半已执行只补缺失行
duplicate_delivery成功传输交付两次事先带键
503 / 429 / outage / schema_drift明确错误未执行重试 / 等待 / 停报 / 修参

晚提交固定延迟 90 模拟秒;重尾版本按 log-uniform 在 40s–2h 抽样(中位约 6–9 分钟,文中两处表述分别为 median about 9 minutes / median 6 min,属同一分布设定)。episode 结束时仍在途的请求会落地——和现实一样。[1]

评分读已提交效果账本与终态:任务成功(TS)要求该有的效果都在、无关记录没被弄坏;**exactly-once 成功(EOS)**还要求没有超额执行。退掉的双扣仍算一次重复副作用——补偿不等于没发生过。行为标签(盲重试、同键重试、改键重试、验后重试/跳过、升级、停而不查、继续往下)来自工具调用轨迹的确定性编码器,不靠模型自评,也不用另一个 LLM 当裁判。[1]

有效性上,作者用三十一组脚本 agent 单测卡住故障语义:观测等价、畸形请求对称处理、滞后前后的陈旧/新鲜读、部分批量、键回放与冲突、晚提交能打败验证但打不败键、重尾延迟、等待阈值阈值、redelivery、升级回报真值、以及 grader 对附带损伤与 overclaim 的检出。经验数据里,agent 也分不清观测等价世界:匹配对上超时后的第一步动作一致率约 87%,不低于同一世界两次独立运行的一致率(约 82%)。E1 与 E2 重叠的 868 轮上,重复结果一致约 96%(Cohen’s (\kappa=0.90))。这些细节重要:若沙箱漏了隐藏结果,后面所有「模型 vs 契约」对比都会塌。[1]

最小 scaffold 是标准 function-calling 循环(最多 40 次工具调用)。生产 harness 侧跑 GitHub Copilot CLI 1.0.86、Hermes Agent 0.20.6、OpenAI Codex CLI 0.139,沙箱以 MCP 暴露,故障计划与真值不离开 runner。九个模型经同一网关:gpt-6-astra、gpt-6-sol、gpt-5.6-sol、gpt-5.4-mini、gpt-4.1、claude-opus-5.5、gemini-3.8-flash、grok-4.7、mai-code-1.1-flash(标识以 2026-09 网关暴露为准)。[1]

契约有两档:native——仅 billing 与一个社交平台接受幂等键;keys-everywhere——把 Stripe 风格键语义扩到每个非幂等写(含可恢复批量、晚提交、redelivery),其它不变。恢复条件从改 prompt(aware / reflect)到中间件式包装:透明 sdk-retry、仅 429/503 的 rules、vbr(verify-before-retry)、带 in-flight 上界的 wait_Δ、以及在 vbr 上加自动挂键 / 滞后再验 / 挡住不可验证重写 / 标注不确定 的 guard;另有可读真值的 state oracle 与能看见在途请求的 outcome oracle。[1]

第一区:读回能拆开时,模型说了算

先把「能拆开」说清楚。超时在三种隐藏状态下观测等价:未执行(absent)、已执行但响应丢失(committed)、请求仍在途稍后才 commit(late)。立刻读回只能区分前两种里「效果是否已经可见」的那一截;若效果已可见,谨慎策略是跳过;若确认未执行,才重发。部分批量则要求只补缺失行,而不是整批重放。误导性 500 同理:HTTP 状态像失败,账本上可能已经成功。[1]

当立刻读回能看见「写没写上」——典型是 ACK 丢失(timeout_post)、误导性 500、部分批量——前沿模型几乎不制造重复。E1 里六个前沿模型在 timeout_post 上的重复率约 0.5%;误导性 HTTP 500 约 8%。机制很朴素:超时后绝大多数先读回再行动。较弱模型(mai-code-1.1-flash、gpt-5.4-mini)在教科书场景也会翻车:丢失 ACK 约 18% 重复,误导性 500 约 47%。[1]

方差分解(Shapley / McFadden 伪 (R^2))把话说死:在「读回能解决」的故障上,模型解释约 53% 的已解释方差,契约约 30%(E1)。这是「换更好的模型有用」的区间——前沿几乎 exactly-once,弱模型不是。E3 全是前沿级时,模型与契约贡献接近(约 39% vs 42%),因为前沿彼此差异不大。[1]

「恰好一次」指令不是装饰。E6 去掉句末提醒后,可读回故障上的重复率从 12% 升到 22%;弱模型从 29% 到 51%,前沿从 4% 到 7%。文中报告的数字因此是偏保守的:真实产品指令若从不提 exactly-once,只会更糟。[1]

还有一个反直觉点:没有读回路径,有时比有一条「看不见滞后效果」的读回更安全。 无读回时,前沿在已提交故障上约 87% 会升级给模拟值班;有读回时则约 96% / 89%(强一致 / 最终一致)去验证,并相信那次看不见在途效果的读。限定在「读回后再重发」的丢失 ACK 子集上,最终一致路径重复率 13.4%,强一致 0.8%((n=947))。「看起来能验」比「老实承认验不了」更危险。[1]

第二区:读回拆不开时,只剩契约

同一批前沿模型,在 晚提交(late commit) 上重复约 56%,在 redelivery(传输至少一次交付两次) 上约 74%。验证在这里帮倒忙:读回时原请求还在飞,空结果诱使重发;等原请求落地就成了双写。297 次「超时后一分钟内重发」的晚提交里,69% 产出重复;等了至少 60s 再重发的 123 个 agent 全部重复(100%),且 99% 落在最终一致读路径上——它们按文档可见性滞后从错误时刻起算,而不是从未知的 commit 时刻起算。从未重发的 84 轮里,除 1% 外都 exactly-once。Redelivery 连错误都不给,只有事先带上的幂等键挡得住。[1]

这一区方差几乎全在契约:契约约 81%,模型约 8%;harness 实验里 harness 在不可拆故障上贡献 0%,在可拆故障上约 3%。预注册的混合分析把两区搅在一起,会显得「故障阶段」占大头、模型只占约 9%——分层之后才清楚:模型只在第一区重要。[1]

契约类上的对比同样干净。前沿对「可带键的写」在已提交故障上仅约 1.5% 重复,天然幂等写 0.0%;且未被要求时也会在约 98% 的此类 episode 上挂键(弱模型约 85%)。强一致可读的非幂等写约 28% 重复,最终一致约 31%。混合效应回归里,相对「可键 / 幂等 / 强可验」,「非幂等 + 最终一致或无读回」的重复优势比约 6.4(95% CrI 5.2–7.9)。[1]

证明:没有 in-flight 上界,纯验证做不到 exactly-once

论文 Proposition 1:对不支持键的非幂等写,超时后存在两个观测等价世界——(W_{\mathsf{absent}})(从未执行)与 (W_{\mathsf{late}}^{\delta})((t_0+\delta) 才执行)。任何只依赖观测的恢复策略 (\pi),若在 (W_{\mathsf{absent}}) 里几乎必然在时间 (T) 内完成任务,则对所有 (\delta>T+\lambda)((\lambda) 为读路径滞后),(\pi) 在晚提交世界里几乎必然造出重复。[1]

直觉就是图 1:超时 + 空读回在「没写上」和「还在飞」之间不可区分;要完成「没写上」的世界就必须重发;同一观测历史会把重发带进晚提交世界,原请求随后落地 → 双写。[1]

已知 in-flight 上界 (\Delta) 能救回纯验证:等到 (t_0+\Delta+\lambda) 再读——但少有 API 写明这种上界,Limbo 工具也没有;真延迟若重尾,任何有限假定都会在尾部外暴露。Proposition 2:若服务对每个幂等键至多生效一次且重复键回放原结果(批量中断可续跑),则「用同一键重发直到 ACK」在全部五种 outcome state 上都 exactly-once。[1]

E5 把「干等」的代价量出来。固定 90s 延迟时,wait 60s EOS 约 43%,wait 120s 约 99%,可见时间从 vanilla 约 2.9 模拟分钟升到约 3.8。重尾时:wait 5min EOS 约 67%,wait 1h 约 84%,却要约 49.9 分钟/轮,且一小时上界只覆盖约 82% 世界——剩下世界里每次重发都重复。同一设定下 keys-everywhere + guard 约 94% EOS、约 1.5 分钟。Outcome oracle(知道何时会 commit)约 99% / 19.5 分钟,是任何「等待策略」的上界。抽象句里的对照也写明:重尾下哪怕每轮等一小时,仍不如「每个写都提供幂等键」——后者把重复率从 28% 降到 4%,因为 agent 会用键。[1]

键本身还要稳定。Keys-everywhere 下 432 次晚提交重发里,复用原键的 0% 重复;首发没带键的 56 次里 68% 重复;重试时换新键(例如拼 -retry1)的 2 次 100% 重复。Harness 若按意图钉死一枚键、覆盖模型在重试时再生的键,就能掐掉这类失败。[1]

Keys-everywhere:28% → 4%,Guard 可跨 Harness 迁移

E2/E2k 把契约当因果旋钮(四模型汇总)。Native + vanilla:晚提交重复约 61%,redelivery 约 74%。Keys-everywhere + vanilla:agent 在约 98% 故障 episode 上挂键,晚提交降到约 9%,redelivery 约 7%。Keys-everywhere + guard:晚提交约 7%,redelivery 0%,EOS 约 99%。引言汇总:反事实「每个写都接键」时,无其它改动即可把重复率从 28% 降到 4%;剩下的重复都涉及首发无键或重试改键。[1]

Harness 几乎不改故事。E3 下 Copilot / Hermes / Codex 与最小 scaffold 的 native 重复率分别约 27% / 29% / 30% / 26%,轮廓一样:丢 ACK 几乎不重复,晚提交与 redelivery 大量重复。Native 契约里 guard(挂在 MCP 里)几乎不动针(约 28–29%),因为没键就修不了。E3k 同一模型 gpt-5.6-sol 跨四 harness:keys-everywhere 下 vanilla 与 guard 的重复率均为 0%,keys+guard 的 EOS 100%。同一契约级机制不改 harness 也能迁过去。 成本倒差很多:Codex CLI 一轮约 153k token,最小 scaffold 约 12k。[1]

中间件也有天花板。透明 sdk-retry 把 EOS 从约 72% 打到约 50%,重复率抬到约 50%——模型还没看见,丢 ACK 已被框架重试成双写。对已会验证的前沿,aware / reflect / vbr / guard 帮助有限;claude-opus-5.5 vanilla 与 guard 的 EOS 都约 77%。Guard 甚至会反噬:对 gpt-6-sol 的晚提交,标注「我会先验再重试」把模型从升级(12%→3%)推向验后重试(59%→88%),而 guard 的读回同样看不见在途请求,晚提交重复从 50% 升到 71%。State oracle(知当前态、不见在途)约 80% EOS;outcome oracle 约 87%——剩余重复里约 95% 是无键写上的 redelivery,客户端策略到不了。弱模型上 guard 仍有用:mai-code-1.1-flash EOS 从约 59% 到 76%,工具调用成本几乎不变(约 8.6 vs 8.7)。[1]

Agent 不知道自己双写了

E1 中至少产生一次重复的 1,279 轮里,agent 以 completed 收尾的约占 90%(95% 区间 78–96%);其中约 80% 还没列出任何不确定操作。人看最终报告会以为没事。对不可逆写(邮件、评论)也没有更谨慎的迹象:模糊故障后不经验证就重发,不可逆约 9.6%,可逆约 6.3%,差异不显著。站内 Traces 可被篡改 讲的是「记录可被当事人改写」;这里是另一条失真——语义上已双写,报告仍写成功。两套问题都提醒:不要把 agent 自报当成权威。[1]

工程含义:协议该声明什么

讨论节把处方写给三类人。对工具与协议设计者:最有效干预不是更好的模型,而是让重复变得无害的契约。幂等键是几十年前的便宜药,native 契约里十一个非幂等写路径只有两条接键——若有什么,这还偏乐观地美化了许多真实 API。MCP 已能声明工具「幂等」,但多半只是提示;作者主张三样规范性补充:非幂等工具的标准幂等键参数、每个写对应的读回/status 操作、以及写明的可见性与 in-flight 上界。没有最后一项,Proposition 1 说单靠验证 成不了 exactly-once;数据还加了一条实践推论:看不见滞后或在途效果的读回,比没有读回更糟,因为 agent 会信它而不去升级。[1]

对harness 构建者:透明重试坐在模型下面、模型看不见,会把每次丢 ACK 变成双写。拥有传输层的 harness 也是挂键、按意图钉键、记住未知结果写、拒绝不可验证重复的自然位置——文中的 guard 不到两百行,只读工具契约。承诺要克制:宣称「我会先验再重试」会让强模型放弃升级。静默挂键、或只声明不确定而不假装已解决,更不容易制造这种自满。服务若文档写了短的 in-flight 上界,等到上界再验证是便宜补充。数据里 harness 本体几乎无关:三个生产 CLI 与最小 scaffold 跑同一模型,好坏几乎一样。还要把结果不确定面给模型和用户——多数双写轮次的终报自称成功,人读了不会去查。[1]

对模型开发与评测:前沿显然吸收了「超时 ≠ 失败」的分布式系统常识,但只在读回能定案时成立,且部分因为被要求恰好一次。残差错误集中在常识不够用的地方——在途请求、陈旧读路径、误导性服务器错误——以及诚实上报。读回拆不开时,模型侧唯一正确动作是拒绝对不确定信息行动:升级,或把操作标成不确定。有的模型会做,没有一个可靠;承诺验证的 guard 还会劝退这种谨慎。只评终态的 benchmark 会漏掉后来被补偿的双写,也分不清幸运重试与安全策略;观测等价对 + 效果账本才能看见两者。[1]

和站内几篇怎么对照

Control the Harness, Control the Cost 论证账单与治理主权常在 harness 默认,不在价目表。本篇补一句:在副作用语义上,harness 换皮几乎不改变重复轮廓;真正搬动针的是契约是否提供键,以及是否有人稳定地挂上键。 控成本要管 harness;控双写要先管工具接口。

扩张 Harness 与 ECC 谈把反复出现的控制决策长进外围代码、而不是无限堆上下文。Limbo 的 guard(文中称不到两百行、只靠工具契约)正是这类外围:自动挂键、滞后再验、挡住不可验证的非幂等重写、标注不确定——但要小心别承诺「验证能解决一切」,否则会挤掉模型原本更保守的升级策略。

Strands 把循环、会话、工具闸门产品化。若 SDK 默认透明重试非幂等写,就落在论文 sdk-retry 那一格:EOS 被腰斩。产品化循环时,应把「意图级幂等键钉死」「未知结果写记住」「不可验证则拒重」做成默认控制面,而不是再包一层看不见的 HTTP 重试。

Tamper traces 要求审计原材料离开 agent 主机信任域。本篇要求副作用语义上的权威离开「模型自我感觉」:账本在服务侧,键在契约侧,不确定要写进报告而不是 completed。两条线都是别把当事人同时当成裁判。

落地清单(按优先级)

  1. 工具契约先提供幂等键。 每个非幂等写接受稳定 key;重复同参回放原结果,不同参拒绝。Agent 在有键时会用(文中约 98%);没键则再聪明也挡不住 late commit / redelivery。[1]
  2. Harness / MCP 中间层负责挂键与钉键。 按意图生成并固定一枚键;重试时覆盖模型新生成的键;对不可验证的非幂等重复,挡住并要求升级,而不是宣称「已验证」。Guard 可挂在 MCP 服务端,跨 Copilot / Hermes / Codex 同构迁移。[1]
  3. 不要把「先验再重试」当成 late commit 的完整解。 无 in-flight 上界时,Proposition 1 说纯验证做不到 exactly-once;重尾下等一小时仍不如全量键。[1]
  4. 读回路径要诚实。 写明可见性滞后与能否看见在途;最终一致却被当成强一致,比没有读回更糟。每个写最好有 status / list 可查。[1]
  5. 关掉对非幂等写的透明客户端重试。 sdk-retry 把丢 ACK 直接变成双写;仅对明确可重试的 429/503 自动重试更接近 rules。[1]
  6. 不信任 episode 末尾的「成功」。 约九成双写轮次仍报 completed;产品侧应对模糊写强制列出 uncertain ops,或对接服务侧账本对账,而不是只看 agent 总结。[1]
  7. 指令里明确要求恰好一次。 去掉这句会抬高可读回与晚提交上的重复;但指令救不了无键的 redelivery。[1]
  8. 评测用观测等价对 + 效果账本。 只看终态会漏掉已补偿的双写,也分不清幸运重试与安全策略;Limbo 的设计本身就是评测清单。[1]

局限(论文自述,写作时一并收紧)

服务是模拟的,语义跟生产文档约定走,故障打在边界而非 prompt。模型经单一网关、默认采样;只评了三个 harness 并限制工具面,其它重试/摘要行为可能不同。重尾延迟是建模选择,定性结论(有限等待留尾巴)一般成立,具体成功率依赖分布形状。任务偏短到中等;gpt-4.1 因配额只覆盖约 87% 设计、不进汇总。若干分层与 E2k/E3k/E5/E6 为预注册后的探索分析。Guard 是刻意简单的基线,不是最优策略。[1]

收束

Exactly-once 不是单一旋钮。读回能拆开的世界里,它住在模型——前沿几乎不双写丢 ACK,弱模型会,明确指令有帮助。读回拆不开的世界里,它住在工具契约——再多推理也完不成无键写上的恰好一次;干等贵且不完整;客户端中间件被契约封顶。给每个写提供幂等键、让 harness 挂稳并跟踪这些键,能在模型与 harness 之间消掉大部分剩余缺口;而 agent 很少主动承认自己造出的重复。[1]

站内一直在写「控制面长在 harness」:控钱、控上下文、控循环、控审计。本篇把控制面再往下钉一层——副作用接口的契约。模型该做验证与诚实上报;harness 该挂键、钉键、拒绝不可验证的重复,且别过度承诺;真正让重复变得无害的,仍是几十年前分布式系统就写在接口上的那枚键。

参考

[1] Where Does Exactly-Once Live? Model, Harness, and Tool-Contract Effects on Duplicate Side Effects in LLM Agents. arXiv:2609.29095. https://arxiv.org/abs/2609.29095 · HTML: https://arxiv.org/html/2609.29095