阅读需 8 分钟

揭示重复副作用的 AI 代理重试日志

AI 代理重试日志将每次尝试关联到一个预期操作,保留未知结果,并在调查过程中揭示重复副作用。

揭示重复副作用的 AI 代理重试日志

代理重试不是对某个操作的第二份副本,而是为了完成一个已声明操作而进行的第二次尝试。审计轨迹必须保留这一区别。如果记录里只有一串相似的 HTTP 调用,恢复和重复就会看起来完全一样。

当操作会改变现实世界时,这个问题最严重:创建工单、撤销访问权限、提交付款、推送部署,或通过 SSH 运行命令。代理可能在目标端已经执行操作后才收到超时,也可能在任何字节离开机器之前因为本地故障而重试。这两种情况需要不同的处理方式,但太多系统都会把它们写成同一句:request failed, retrying

我见过团队手动比较时间戳和请求内容,调查所谓的重复操作。这远不如事件模型可靠。操作发生时,就把这些关系写入每条记录。调查人员应该能够选中一个操作,看到它的意图、每次尝试、后续尝试发生的原因,以及最终解决问题的结果。

重试属于操作,而不是日志行

每个会产生副作用的代理操作都需要两个身份:操作 ID 用来标识预期结果,尝试 ID 用来标识一次执行尝试。从代理决定要做什么开始,直到关闭或核对这一意图,操作 ID 都应保持不变。每次网络调用或 SSH 执行都要获得新的尝试 ID。

假设代理打算停用一个账户,于是创建操作 op_7f2c。第一次请求 att_01 到达身份 API,但连接在响应返回前断开。第二次请求 att_02 可能是合理的恢复尝试。两条记录都必须指向 op_7f2c,而 att_02 还必须直接指向触发它的尝试 att_01

不要把会话 ID 当作操作 ID。一次代理会话可能包含多个操作,而一个操作也可能在会话结束后继续存在,由主管进程恢复处理。也不要只使用请求哈希。哈希描述的是字节,操作描述的是预期效果。两个请求可能只有无关紧要的传输细节不同,却仍然属于同一个操作。反过来,即使字节完全相同,代理有意发送两次时也可能代表两个不同的预期操作。

当代理形成类似“停用账户 A”“为警报 B 创建一个事件”“在主机 C 上运行一次迁移”的承诺时,就应使用操作身份。将这一意图记录在结构化字段中。自然语言摘要对人很有帮助,但不能单独作为身份,因为不同代理运行之间的措辞会变化。

清晰的层级应当是这样:

  • 会话标识一次代理进程运行。
  • 操作标识一个预期的外部效果。
  • 尝试标识一次实际执行。
  • 观察标识之后收到的证据,例如回调、写后读取检查或操作员决定。

这个层级能解决一个常见的棘手情况:代理发送请求后超时,接着询问另一个端点操作是否已经发生。这个查询不是重试,而是附加到原始操作上的一次观察。如果把它当成另一次尝试,最有价值的证据就会被埋在错误的类别里。

未知是一种结果,不是错误消息

超时带来的是不确定性,而不是失败证据。日志需要有一个表示这种不确定性的状态,并且在后续证据解决它之前一直保留这个状态。

许多客户端库会把连接被拒绝、DNS 失败、响应体到达过晚,以及服务器提交写入后连接重置等事件,统统压缩成一个异常。对应用控制流来说,这种便利没有问题。但它不能成为最终审计记录。记录必须说明调用方观察到了什么,并避免声称调用方无法证明的目标端事实。

对于一次尝试,要把本地观察与已确定的操作状态分开。尝试可以是 not_sentsent_no_responseresponse_receivedexecution_error。操作则可以是 opensucceededfailedunknowncancelled。名称可以不同,但两者必须分开。

not_sent 表示客户端在发送前就停止了。例如,本地查找凭据失败可能属于这个状态。由于没有远程请求发出,重试不会造成远程副作用重复。

sent_no_response 表示调用方已经发送了操作,但没有拿到可用响应。这是危险状态。只有在目标端具备可靠的去重机制,或者操作本身不可能产生重复效果时,自动重试才可能安全。

response_received 也不自动等于成功。服务器可能返回验证错误、冲突响应,或者返回一个描述异步工作的成功响应。保存状态、相关响应指纹以及目标端签发的操作引用,然后根据具体 API 的契约设置操作状态。

不要在实际意思是 unknown 时写成 failed。这个词能让仪表盘看起来整洁,却会告诉下一个代理或操作员重复一个可能已经发生的操作。事故期间,一个不诚实的字段就可能把一次错误操作变成一连串错误操作。

HTTP 语义不会让业务操作可以安全重复

HTTP 方法描述的是协议语义,而不是你的业务保证。RFC 9110 规定,如果多个相同请求的预期效果与一个请求的效果相同,那么这个方法就是幂等的。它将 PUT、DELETE 和安全方法列为幂等方法,而 POST 默认并不幂等。

这条指导很有用,但工程师经常把它推得太远。DELETE 请求可能在协议层面是幂等的,因为删除一个不存在的资源仍然会得到不存在的结果。但你的审计问题可能完全不同:代理是否删除了正确的账户,是否两次触发了下游清理,以及第二次调用是否使用了不同的权限?协议标签回答不了这些问题。

PUT 也会带来麻烦。将资源设置为固定表示形式的 PUT 通常可以容忍重试。但如果 PUT 端点每次收到请求都会触发通知、分配记录或运行集成,它就没有提供人们想象中的安全性。阅读目标端的文档契约,并在强制丢失响应的情况下测试行为。方法名称不是证据。

POST 端点通常支持幂等性令牌。发送由操作 ID 派生的稳定令牌,不要使用尝试 ID。如果 att_01att_02 使用不同令牌,就等于破坏了保护你免受重试影响的功能。

请求信封可能如下所示:

{
  "operation_id": "op_7f2c9c",
  "attempt_id": "att_01",
  "idempotency_key": "op_7f2c9c",
  "intent": {
    "kind": "disable_account",
    "subject_ref": "user:1842"
  },
  "destination": {
    "method": "POST",
    "route_template": "/v1/accounts/{id}/disable",
    "authority_ref": "vault:identity-prod"
  },
  "request_fingerprint": "sha256:...",
  "dispatch_state": "sent_no_response"
}

不要记录授权请求头、会话 Cookie、私有 SSH 材料,或包含密钥的请求体。记录凭据引用和规范化请求表示的指纹。指纹能帮助调查人员比较不同尝试,同时不会把审计轨迹变成另一个密钥存储区。

目标端必须遵守幂等性令牌,它才能防止重复效果。如果目标端遵守了令牌,记录其返回的引用,以及响应是否来自之前保存的结果。如果目标端不遵守,令牌就只是一个不起作用的请求头,重试策略必须据此处理。

SSH 重试可能重复不止一条命令

SSH 让重试统计更难,因为一次连接可以携带 shell 语法、管道、重定向,以及可能部分完成的多个命令。SSH 会话失败,并不能说明远程命令的哪些部分已经运行。

考虑下面的命令:

create-user deployer \u0026\u0026 install-key deployer /tmp/new.pub \u0026\u0026 restart-service api

如果客户端在发送后失去连接,重试可能因为用户已经存在而失败;如果辅助程序会追加密钥,也可能重复安装密钥;或者再次重启服务。Shell 中的 \u0026\u0026 只控制一次执行内部的行为,对重新建立连接并再次运行整条命令没有保护作用。

只有在命令不包含任何秘密材料时,才记录完整命令。否则保存脱敏后的显示形式和规范化指纹。记录主机别名或主机密钥引用、远程账户引用、相关时的工作目录、收到的退出状态以及执行边界。边界应说明辅助程序是否启动了命令,以及是否收到退出状态,而不只是本地调用方是否报告了错误。

更安全的模式是使用远程脚本,并让脚本在执行前检查操作标记。标记必须放在目标系统能够原子读取的位置。根据环境不同,数据库事务、部署记录或以独占方式创建的文件都可以胜任。进程崩溃或第二个代理进程运行后,本地代理缓存无法证明任何事情。

例如,部署脚本可以接受 OPERATION_ID,在激活前将它写入发布记录;如果记录已经存在,就返回已有结果。这样,审计事件既能捕获本地操作 ID,也能捕获远程记录 ID。调查人员就有了一座连接代理记录与主机证据的桥梁。

不要因为任意 Shell 命令“基本安全”,就把它归为可重试。应按命令类别管理。只读采集可以自由重试。设置状态的命令需要明确的收敛条件。追加、财务、破坏性或通知类命令,在结果未知后需要远程去重记录或人工决定。

每次后续尝试都需要明确原因和父级

明确批准重复操作
配置每个密钥的审批,在每次敏感重试前要求点击确认或使用 Touch ID。

重试事件必须说明是哪次尝试触发了它,以及什么条件使再次尝试变得合理。retry_count: 2 太弱了。它只能说明之前有调用,却无法指出哪次调用失败、代理是否改变了什么,或是否有人批准了继续操作。

使用受控的原因集合,再单独附加支持细节。可用原因包括 connection_not_establishedrate_limiteddestination_5xxresponse_lost_after_dispatchcredential_refreshedoperator_requested。不要让代理自行编写看似让人放心、却无法分类或审查的说明文字。

每次重试都保留以下关联:

{
  "operation_id": "op_7f2c9c",
  "attempt_id": "att_02",
  "retry_of_attempt_id": "att_01",
  "retry_reason": "response_lost_after_dispatch",
  "retry_decision": "destination_idempotency_confirmed",
  "attempt_budget_remaining": 1,
  "request_fingerprint": "sha256:...",
  "prior_request_fingerprint": "sha256:..."
}

两个指纹通常应该相同。如果不同,就记录原因。时间戳请求头发生变化可能是预期行为,但账户标识符、金额、主机名、路由或权限发生变化,就不属于通常意义上的重试。这是一个新操作,或者是经过人工修改的意图,审计轨迹必须说明这一点。

代理系统经常在这里误导自己。模型读到错误后修改参数来“修复”问题,然后把下一个请求称为重试。这其实是一个新的决定,也可能产生新的效果。把它关联为重试,会掩盖计划变化,让审查几乎无法进行。

为每个操作限制尝试预算,并记录预算决定。在目标端说明的等待时间后重试限流响应,与超时后重试一个未知写入不同。前者通常有明确的远程响应,后者则需要幂等性证据或核对结果,之后才能重复。

幂等性令牌和审计身份解决的是不同问题

幂等性令牌告诉目标端将重复提交视为一个操作。审计操作 ID 告诉你自己的调查人员哪些尝试属于同一个意图。可以同时使用两者,但不要假装一个能替代另一个。

令牌的作用域可能是某条路由、某个商户、某个时间窗口或某个账户。有些 API 只在有限时间内保留令牌。有些 API 会为重复请求返回原始响应,另一些会返回冲突。有些 API 会在请求体发生变化时拒绝重复使用令牌。这些细节应写入连接器契约和测试套件。

操作 ID 的作用更广。它连接代理会话、审批证据、请求构造、传输尝试、远程响应以及后续核对。即使供应商 API 不提供幂等性,即使操作使用 SSH,或者最终由操作员手动完成恢复,它也应继续有效。

不要因为进程内存消失,就在重启后生成新的操作 ID。发送请求前先持久化待处理操作。恢复时检查每个未解决操作,并选择三条路径之一:使用远程证据进行核对,在有文档记录的幂等性保证下重试,或升级给人工处理。重启是工程事件,不是忘记不确定性的许可。

一个常见但错误的建议是,对每个失败的写入都使用指数退避进行重试。退避可以减轻服务压力,却不会把未知写入变成安全写入。能否重复操作,以及目标端如何去重,才决定再次尝试是否可以接受。

核对可以消除不确定性,但不会改写历史

保留未知结果
加密且由哈希链保护的审计日志保留证据,同时不会让代理接触 API 密钥或 SSH 密钥。

核对是收集关于未解决操作的后续证据,不是修改第一次尝试,直到它看起来像成功。

假设代理在请求体中使用客户端提供的引用来创建事件。初始 POST 结束时状态为 sent_no_response。在重试之前,代理按该引用查询事件。如果找到匹配记录,就追加一个观察事件,引用原始操作 ID、查询指纹、返回的远程标识符和匹配条件。然后通过核对将操作关闭为 succeeded。

如果查询没有找到任何结果,要谨慎处理。当 API 存在复制延迟、搜索索引延迟或过滤能力有限时,缺少结果几乎不能证明什么。记录这次否定观察,包括使用的时间和端点。只有在目标端契约说明幂等性令牌仍然有效时才重试,否则就等待并请求决定。

如果查询找到两条匹配记录,不要把操作标记为成功后继续。将它关闭为 duplicate_effect_confirmed,保留两个远程标识符,并创建一个单独的修复操作。修复操作不能共享原始操作 ID,因为它有不同的预期效果。

将只追加事件作为事实来源,而不是使用可变状态行。你可以为用户界面投影一个方便的当前状态,但证据必须保留完整转换过程:创建意图、发送尝试、响应丢失、执行查询、找到远程记录、解决操作。调查人员需要看到整个顺序,包括已经发生的错误重试。

具备防篡改证据的日志还能提供另一项能力:验证后续进程没有悄悄删除第一次未知的尝试。Sallyport 使用无法读取写入内容的加密哈希链审计日志,记录代理会话和单个操作;sp audit verify 可以离线对密文检查哈希链。这有助于保留时间线,但事件结构仍然需要本文所述的操作和尝试关联。

代理委托需要一个操作所有者

让 SSH 重试可追溯
SSH 命令通过 Sallyport 内置的 sp-ssh helper 运行,而 SSH 密钥始终保存在加密保险库中。

子代理会让重复效果更容易出现,因为每个进程都可能认为自己拥有任务。让一个进程负责操作 ID,并要求每个被委托的工作进程在操作上下文中携带这个 ID。

规划器可以要求一个工作进程收集信息,再让另一个工作进程执行操作。信息收集调用应该拥有自己的操作,因为它们代表不同的意图。只有在执行进程处理的是同一个已声明效果时,才应将原始操作 ID 传给它。它的每次调用仍使用新的尝试 ID,并标明发起调用的工作进程。

不要让工作进程独立重试未知写入,同时父进程也在重试。父进程必须先收到工作进程的发送状态,再决定下一步。如果工作进程意外退出,就将操作标记为未解决并进行核对。缺少子进程结果,不是主管进程重放命令的理由。

每个会话的审批记录也属于证据链。当代理进程获得执行一组调用的权限时,要将进程身份、审批时间和撤销时间,与操作结果分开记录。审批说明谁获准尝试,但不能证明目标端是否执行了操作。

对于敏感操作,如果第一次尝试的结果未知,就要求对重试本身进行逐次审批。当审批卡写明原始意图、之前的发送状态和计划采用的恢复方式时,人们更容易做出正确判断。只显示“允许 API 调用”的通用提示,会隐藏本应让人放慢脚步的关键事实。

调查视图应该展示时间线,而不是一堆请求

调查人员需要一个从意图开始、以最有证据支持的结果结束的单一操作页面或查询结果。按时间排序的请求日志会迫使审查者在压力下重建父子关系,而且经常要跨越多个时钟略有不同的系统。

在顶部显示操作状态,但让证据出现在下方。每次尝试都应显示其编号、发送状态、重试父级、原因、权限引用、目标端、请求指纹、响应摘要和耗时。每次观察都应说明检查了什么,以及证据为何改变或没有改变状态。

不要因为最终效果没有造成伤害,就隐藏重复请求。今天无害的重复,可能在 API 发生变化或集成增加 webhook 后变成昂贵的副作用。记录应让审查者区分“代理正确重试,目标端完成去重”和“代理发送了两次请求,只是碰巧没有造成问题”。

将恢复期间的请求体变化明确记录为分支。原始操作保持打开状态,或接收它已经确定的结果。修改后的操作获得新的操作 ID,并通过 supersedes_operation_id 之类的字段建立关联。这个记录如实说明了情况:代理不是简单重试,而是改变了预期操作。

在信任设计之前,先编写一个强制失败测试。让目标端提交一个已知的幂等测试操作,然后丢弃返回给调用方的响应。确认下一次代理运行保留原始操作 ID,使用相同的目标端令牌,记录第一次未知的尝试,执行有文档说明的核对或重试,并用证据关闭操作。如果测试无法回答这些问题,事故也不会替你回答。

我查看代理操作日志时,首先寻找的不是 HTTP 状态,而是稳定的操作 ID。没有它,每次重试调查都只能靠猜。有了它,你就能提出真正重要的问题:代理原本想做什么,哪些内容离开了机器,两次尝试之间发生了什么变化,以及哪些证据支持最终结果。

常见问题

AI 代理的重试总是安全问题吗?

重试可能是合理的恢复操作,但前提是日志将它关联到之前的尝试,并记录重试前发生了什么。如果缺少这种关系,调查人员就无法判断代理是在超时后恢复,还是把同一个副作用执行了两次。

AI 代理重试时,哪个 ID 应该保持不变?

为预期中的业务操作使用一个稳定的操作标识符,然后为每次传输尝试分配新的尝试标识符。重试时操作标识符保持不变,尝试标识符则绝不能重复。

超时是否意味着 API 请求失败?

不一定。超时只表示调用方没有收到可用响应。远程服务可能拒绝了请求,也可能已经完成了一次,甚至在调用方改变请求后完成了多次。

有了幂等性密钥,还需要记录重试日志吗?

幂等性可以减少协作目标端产生的重复副作用,但它不会记录代理决定重试的过程,也不能证明目标端确实遵守了请求。即使 API 接受幂等性令牌,也要保留操作日志。

日志应该如何记录未知的请求结果?

在完成核对前,将真实结果记录为 unknown。不要因为代理丢失响应就写成 failed,也不要在目标端确认完成或后续证据证明完成之前写成 succeeded。

代理应该在什么时候停止重试操作?

只有在操作拥有稳定的操作标识、明确的重试原因和有限的尝试预算时才重试。对于不支持幂等性的不可逆操作,一旦结果未知就应停止,并要求人工决策或进行核对。

HTTP 状态码足以构成 AI 代理审计轨迹吗?

不够。状态码描述的是一次 HTTP 交互,而操作记录还需要包含预期效果、凭据引用、目标端、尝试之间的关系、响应证据以及最终确定的状态。单靠 HTTP 日志通常缺少调查人员需要的事实。

代理委托给子代理后,重试应该如何处理?

父进程负责操作标识符,并将它传给子任务。子进程可以创建自己的尝试标识符,但必须保留父操作标识符,并说明自己的执行角色。

得知结果后,可以编辑重试记录吗?

应保留原始事件,再追加引用它的更正或核对事件。可变日志会让人有机会悄悄改写历史,而事故调查恰恰需要可信的时间线。

API 产生重复副作用后,调查人员需要哪些证据?

他们需要原始意图、按顺序排列的每次尝试、使用的凭据或权限、请求指纹、观察到的响应、重试原因以及最终核对结果。他们还需要证明后续软件无法悄悄修改这些记录。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov