幂等这件事,别指望模型自己记住。重试是框架给的,超时是网络给的,同一轮里调两次是采样给的——这三样都不在 prompt 的射程内。我的结论很直:写操作的幂等键由框架生成、透传,工具侧去重。读操作别折腾,缓存够了。
模型重试不是意外,是默认行为
Chronicle 那篇讲得挺实在:agent 的失败难复现,因为推理本身不是 bitwise 可复现的,工具读的是会变的状态,多步轨迹重跑基本不重样。它想解决的是回归测试,但反过来读一遍更刺人——连"把刚才那次重放一遍"都做不到,还想靠 prompt 里写一句"不要重复调用"来保证唯一性?
Overclaiming 那篇更扎心。agent 会高估自己的完成度,而最终那段回复往往是用户唯一能看到的账本。工具侧没有幂等的话,账本和实际副作用直接对不上——它说退款成功了,其实发了两次;它说建了一个工单,其实建了三个。用户信任的是那段自然语言,出问题的是数据库。
这两个问题其实是一件事的两面:模型看不见自己之外发生了什么,而副作用发生在模型之外。
幂等键该由框架生成,不该由模型写
具体做法我说细一点。框架在派发 tool call 的那一刻生成 key,拼法要确定性——(session_id, step_id, tool_name, call_index) 之类,哈希成一个字符串。这里有个容易踩的坑:别用 UUID。UUID 在重试时是新值,等于没做幂等。要的是同一次意图在重试后算出同一个 key。
透传方式随你,塞进工具参数、放 header、放 trace context 都行。工具侧拿到之后 INSERT ... ON CONFLICT DO NOTHING,或者 Redis 的 SETNX 加个 TTL,然后返回一个 duplicate: true 之类的标志。这样 agent 知道"上一次其实成功了",可以接着往下走,而不是傻等超时再重来。
为什么不能让模型自己写这个 key?因为重试发生在它的上下文之外。它在 token 流里吐出一个 key,重试时重新采样,大概率是另一个字符串。这不是模型不听话,是它压根没有那个信息。让它猜一件它观测不到的事,本来就不合理。
HarnessTax 和那篇 harness 组件研究支持这个方向:execution loop 固定,其余组件的差异对长任务表现影响很大。幂等透传属于 harness 的一部分,不是 prompt 的一部分。Claude Code 现在没有 CLAUDE.md 时会去读 AGENTS.md,也是同一个趋势——大家把约定往 harness 层收。
读操作强上幂等是浪费
反过来讲一句:给读操作套幂等键,我不认。读没有副作用,重复调最多浪费时间和 token。你要为它维护 key 存储、TTL、一致性,收益接近零。这种问题缓存层解决就够——按参数哈希存一下,TTL 给短一点,成本远低于搭一套幂等基础设施。
但有个边界得说清楚:名字带 get 不代表真没副作用。ZCode 静默上传 Git history就是个现成例子,工具实际干了什么,和它的命名、文档可能完全不是一回事。所以分类不能靠名字,要靠工具自己声明的副作用等级,声明和实现对不上就是 bug,测试里该有一条盯着。
一句话收
框架生成透传,工具校验去重,模型不负责。prompt 里那句"不要重复调用"是安慰剂,它拦不住框架层的 retry,也拦不住采样。
不确定的地方也有:同一轮里并行发多个 tool call,key 怎么排现在没有共识,我先用 call_index 顶着。也许过阵子回头看,这套显式透传会被 trace_id 隐式传递替掉。按我现在看,显式还是比隐式靠谱——出问题时能一眼看出是哪个 key 撞了。