一个Agent向服务器提交发布文档的请求。服务器已经提交了。响应在Agent收到之前消失了。Agent看到超时,于是重试。现在你有两份已发布的文档,以及一份描述"成功恢复"的日志。

这是一个设计场景,不是基准测试结果。它暴露了一个问题,值得在给Agent配备能改变外部状态的工具之前先问清楚:当系统无法判断某个操作是否发生时,它会怎么做?

打开网易新闻 查看精彩图片

超时只能证明调用方没有及时收到结果,它不能证明远程操作失败了。关于幂等API的讨论解释了这种模糊性,以及如何用调用方提供的请求标识符让重试变得安全。对于Agent工作流,应该把这种不确定性明确写进应用契约里。

给不确定性一个状态

一个布尔型的成功字段,无法描述远程写入的所有结果。需要考虑这些状态。

过期的worker租约同样值得关注:worker可能在远程提交之后崩溃了。恢复逻辑必须把这种可能性考虑进去。

界面可以这样显示:"请求已提交,但完成状态未确认,正在检查。"这是一个有用的结果,即使它不如"完成"那样让人满意。

在调用模型之前先确定操作身份

工具调用ID标识的是一次尝试。应用需要的是跨多次尝试的逻辑操作身份。以一个假设的文档工作流为例:

  • operation_id: op_8c21
  • actor_id: user_42
  • action: publish_document
  • target_id: doc_104
  • content_version: 7
  • request_fingerprint: 规范操作参数的哈希
  • state: ready

在应用代码中创建并持久化这个身份,让它在重试和重启中一直传递下去。如果目标端支持幂等键,就按照该API的契约,为同一个操作传入同一个键。

请求指纹的作用不同:它用来检测参数是否发生了变化。用同一个操作ID配上不同的目标或载荷,应该产生冲突。哈希本身无法表达意图,两个有意的操作可能拥有完全相同的载荷。

把标识符限定在操作者或租户范围内,原子性地强制唯一性,并检查提供方的保留窗口。目标端让记录过期之后,旧的键可能就不再保护重试了。

让授权始终附着在被提议的操作上

如果工作流需要审批,就记录下被批准的内容:操作、目标、载荷版本以及适用的限制。

在策略允许的情况下,对那个确切操作的重试可以复用已记录的授权。如果模型改变了载荷,它提议的就是另一个操作,不应该悄悄继承旧操作的批准。

执行时还要重新检查当前权限。一份持久的批准记录不应该绕过之后的撤销。

本地账本关不上远程事务

困难的顺序是这样的:

  1. 在本地记录操作。
  2. 发送远程写入。
  3. 目标端提交。
  4. worker在记录结果之前崩溃。

本地事务无法让第2步和第4步与一个不相关的服务保持原子性。

恢复路径取决于目标端:

  • 如果有合适的幂等支持,就按照该契约重试同一个操作。
  • 如果能按操作ID做权威查询,就查询它并核对结果。
  • 如果两者都没有,就保留未知结果,并在尝试另一次有后果的写入之前,把它转给人工调查。

如果目标端是最终一致的,空的搜索结果可能并不具有结论性。持久队列能改善投递,但消费者仍然需要一套应对重复尝试的策略。

测试那些不方便的边界

在上线之前,要演练这些情况。这些是针对周边应用的测试,不是让模型"更小心一点"的提示词。

一个实用的设计评审问题是:如果这次写入成功了,但它的响应消失了,什么证据能让下一个worker决定该做什么?

如果答案只是"Agent会自己搞定的",那么恢复协议还没有完成。