跳到主要内容

双写竞态:当你的智能体与用户同时编辑同一个日历事件时

· 阅读需 14 分钟
Tian Pan
Software Engineer

智能体自信地报告:“我已将会议改至周四下午 3 点。”用户却盯着原本周二上午 10 点的时段发呆,因为在智能体制订计划到提交更改的这段时间内,用户自己编辑了该事件。“最后写入者胜”(Last-write-wins)策略让自动化的操作覆盖了人类的修改,而用户对助手的信任也因这一次事故而崩塌。这就是双写竞争(dual-writer race),也是智能体工具链从未专门设计应对的 bug 类别。

大多数智能体平台都无意中继承了这一问题。工具层将 update_event 视为一个简单的函数调用:获取 ID,获取新字段,返回成功。底层的提供商 API 十多年来一直提供乐观并发原语(optimistic concurrency primitives)——ETags、版本令牌(version tokens)、If-Match 前提条件——但几乎没有人将它们贯通。模型无法知道它一分钟前所推理的世界已不再是现状,因为由于它所获得的抽象层静默地丢弃了这些信息。

只要智能体触及共享状态,这种模式就会随处可见:日历、CRM 记录、Jira 工单、共享文档、项目追踪器、工单队列。用户在智能体读取后的几秒钟内打开了一条记录,双方都进行了编辑,智能体最后写入,人类的更改随之消失。智能体报告成功。控制面板显示成功。用户却不认可,而且 Prompt 模板中没有任何道歉信息能修补这种损害。

为什么工具层丢弃了版本令牌

让我们看看生产环境下一个典型的“重新安排会议”工具是如何运作的。智能体调用 get_event(event_id),SDK 返回一个包含 summarystartendattendeeslocation 的已解析对象。智能体根据这些字段进行推理,选择一个新时间,然后调用 update_event(event_id, {start, end})。封装层对提供商执行 PATCH 请求。完成。

注意缺失了什么。提供商的响应中携带了一个 etag 字段——Google Calendar 在每个事件资源上都公开了一个,Microsoft Graph 在每个事件上公开了 @odata.etag。这些令牌标识了 智能体当时看到的版本。SDK 要么在反序列化过程中将其丢弃,要么保留了它但工具封装层从未读取,抑或是读取了但在 PATCH 请求中从未设置 If-Match。当调用离开你的网关时,请求的内容实际上是“无论该事件处于什么状态,都将其设置为这些字段”。“最后写入者胜”并不是智能体选择的默认行为,而是工具层内置的默认行为。

协议层面的修复是机械性的。Google Calendar 接受 If-Match: <etag>,并在资源自读取以来发生变化时返回 412 Precondition Failed。Microsoft Graph 要求对多种资源类型的 PATCH/DELETE 请求使用 If-Match,并在 ETag 过时时返回 409 Conflict——Planner 严格执行此规则,最近一篇关于将 AI 智能体连接到 Microsoft Planner 的实践指南明确指出:“永远在更新前立即获取,绝不要缓存 ETag。”Jira 的状态转换 API 出于同样的原因将其并发转换响应从 400 切换为 409 Conflict:409 是标准的信号,表示“你的写入在竞争中失败了”。

版本令牌是提供商免费提供给你的契约。工具层的任务就是确保它在端到端之间保持完整。

三层修复:带版本读取、带前提写入、将冲突视为结果

一个双写安全的工具需要三者协同工作,跳过其中任何一个都会在无意中重新引入竞争风险。

带版本的读取(Read-with-version)。 每一个读取工具都应返回对象及其版本令牌,并且智能体的工况内存应保持它们的配对。如果你现在的工具 schema 返回的是 {event: {...}},请将其改为返回 {event: {...}, version: "abcd1234"},并将 version 字段标注为“对模型不透明但在写入时必需”。智能体不需要理解这个令牌,它只需要将其传回即可。

带前提条件的写入(Write-with-precondition)。 每一个写入工具都应接受一个版本令牌,并将其作为 If-Match 转发给提供商(或提供商对应的机制——Microsoft Graph SDK 中的 IfMatch 参数、Google API 中的 If-Match 请求头、Jira 问题中的 version 负载字段、或者你在 Salesforce 上通过之前的 SELECT FOR UPDATE 存储的 _token)。如果智能体在调用写入时没有带版本号,工具应拒绝执行——“智能体忘记传递令牌”这一故障模式应该是显式的报错,而不是静默失败。

将冲突视为一种结果(Conflict-as-outcome)。 当前提条件校验失败时,工具不应进行重试。它应向规划器返回一个结构化的结果:“冲突:事件在你的读取和写入之间已被修改;新版本为 X。”这个结果必须是模型可以推理的一等结果,而不是隐藏的重试逻辑(即重新读取、重新应用并再次写入)。隐藏的重试是最糟糕的设计选择,因为它将“人类的更改很重要”变成了“人类的更改悄无声息地消失”——这正是版本令牌最初想要防止的失败情况。

第三层之所以重要,是因为如果只告诉智能体“写入失败,请重试”,它会很自然地重新读取,重新应用同样的意图,并再次覆盖人类的修改。智能体必须知道 世界已经发生了变化,这样规划器才能决定最初的意图是否仍然合理。一个用户刚刚删除了底层事件的会议改期请求,不再是改期操作,而是一个全新的决策。

当你无法简单重试时,冲突解决是什么样的

假设智能体(agent)计划将一个 30 分钟的周会从周二上午 10 点移至周四下午 3 点。在读取和写入之间,用户接受了来自他人邀请的同一日程的不同时间,现在该日程变成了周二上午 11 点。这时触发了 412 错误。智能体该怎么办?

存在三种可靠的路径,而选择哪一条取决于工具与用户之间的契约。

第一种是基于最新状态重新规划。冲突结果包含了该日程的新版本。规划器重新读取日程,看到用户的更改,并询问模型在新的初始状态下,原始意图(移至周四下午 3 点)是否仍然适用。通常情况下是适用的——用户更改了时间,但移至周四的目标没变。有时则不适用——用户已经亲自移动了会议,智能体现在的工作是什么都不做。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates