文章提出 Agent 四层架构:Context(上下文)、Plan(意图)、Attempt(提交记录)、Effect(外部变更),指出重复执行问题根源在于缺少持久化执行凭证。
An AI agent 可以记住一场 30 页的对话,却仍然重复同一个操作。
它发出了一个请求。连接超时了。Agent 记得目标、记得计划、记得工具调用——但唯独不记得外部系统是否发生了变化。于是它重试了一遍。
这不是向量记忆的问题。这是操作回执的问题。
"Agent 记忆"通常指的是对话历史、检索到的文档或持久化的项目知识。这些都有用,但它们回答的是"Agent 知道什么",而不是"在另一个系统里发生了什么"。
我发现把记忆分成四层很有用:
前两层服务于推理。后两层防止重复发邮件、重复发布、重复创建 listing,以及其他代价高昂的"好意"重试。
更多上下文并不能弥合这道鸿沟。模型可以精确回忆起那个请求,却仍然无法知道服务器在连接消失前是否已提交了它。
提交之前,失败很简单:什么都没发送出去,所以重试可能是安全的。
提交之后,失败是模糊的。超时、连接重置或无法读取的响应可能意味着:
把两种情况都当作"失败"处理,会把一个传输问题转化成一个重复操作的 bug。
因此这个操作需要一种大多数快乐路径工作流都忽略的状态:
planned -> submitted -> succeeded
\-> rejected
\-> outcome_unknown -> reconciling
\-> succeeded
\-> safe_to_retry
\-> manual_review
outcome_unknown 不是要隐藏的错误消息。它是关于系统当前能证明什么的边界的持久知识。
回执应该在发起外部请求之前写入。否则,正是使其有价值的那次精确失败,也可能导致它无法存在。
一个小的回执可以包含:
{
"operation_id": "20260816T012030Z-1fd54b31a2",
"operation": "articles.create",
"target": "/api/articles",
"state": "submitted",
"intent_fingerprint": "sha256:…",
"submitted_at": "2026-08-16T01:20:30Z",
"external_id": null,
"authentication_recorded": false
}
指纹应该从意图的去标识化或白名单表示中计算得出,而不是从密钥中计算。回执需要足够的身份信息以便后续识别其效果;它不需要成为一个第二凭证存储。
这也和日志行不同。日志描述事件。回执是一条有生命周期的操作记录。系统随着认知的变化更新同一条记录。
我在一个写入 DEV API 的小 CLI 中测试了这个模式。那个 CLI 已经可以预览变更、要求显式确认、写入私有 intent 文件,并且在网络故障后从不重试写操作。
但 intent 文件永远只是 intent 文件。成功的响应不会把它推进到 succeeded,模糊的传输失败也不会把它推进到 outcome_unknown。安全规则存在于客户端,而持久状态却落后于它。
修正后的形状刻意做得很无聊:
receipt = record_intent(operation, sanitized_request)
update(receipt, state="submitted", submitted_at=now())
try:
response = send_once()
except ExplicitRejection as error:
update(receipt, state="rejected", error=error.code)
raise
except TransportFailure as error:
update(receipt, state="outcome_unknown", error=error.code)
raise
else:
update(
receipt,
state="succeeded",
external_id=response.id,
completed_at=now(),
)
故意在异常路径中没有重试。测试覆盖成功、显式拒绝和模糊失败作为不同的回执状态。
有一个实现细节比我预期的更重要:状态更新应该是原子的。通过私有临时文件替换回执可以避免把进程中断变成半条 JSON 文档——让审计系统自己产生模糊的证据。
未知结果通过一次读来解决,而不是另一次写。
协调器应该:
当存在幂等键或返回的标识符时,按其查询外部系统。
否则执行有界的搜索并匹配最小安全的意图指纹。
如果预期效果存在,则标记操作为 succeeded。
只有当确实能证明不存在时,才标记为 safe_to_retry。
把其余所有情况发送给 manual_review。
这正是 API 设计改变安全边界的所在。提供幂等键和精确的读后写查找的平台,比两者都没有的平台更容易安全自动化。当平台没有可靠的方式证明 absence 时,"我无法判断"才是正确答案。
回执解决一个狭窄的问题:尝试了什么请求、传输报告了什么、后来观察到了什么外部效果。
它们不能证明请求是明智的、负载在语义上是正确的,或者验证查询检查了正确的东西。一份完美维护的回执可以以极高的保真度保存一个错误的决策。
这意味着回执应该放在策略检查、影响性操作的人类审批、语义验证和验证者自身的负面控制旁边——而不是替代它们。
还有其他限制:
这些限制正是 manual_review 属于状态机的原因。
scratch 上下文可以过期。旧计划可以归档。失败的方法可以成为历史。
但外部可见的、结果未知的尝试不应该被遗忘或被总结掉。在世界与 Agent 的意图协调一致之前,要保留它。
实际问题不仅仅是:"Agent 记得什么?"
而是:"在 Agent 再次行动之前,系统能证明发生了什么?"
在你的系统中,哪一次外部写入在超时后最难协调?