跳到主要内容

智能体完成任务时房间已空:异步后台任务中的过时上下文交付

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个需要 90 秒才能完成任务的后台智能体,其操作基于的是 90 秒前的世界快照。当它返回结果时,用户可能已经导航到了不同的视图,开始了一个新的对话,归档了原始请求,或者完全关闭了标签页。大多数智能体框架无论如何都会交付结果,修改状态以反映结果,并将这次往返视为成功。但这并不是成功。这是智能体在一间空屋子中结束。

这种失败模式比直接丢弃结果更糟糕。丢弃结果只是一次投递失败——虽然烦人但可以恢复。而应用陈旧的结果则是对一个用户不再提出的问题的回答,它是针对不再匹配的状态编写的,往往会覆盖用户已经开始的新工作。用户会注意到发生了他们没有要求的事情,却无法重构原因,从而对系统失去信任,这种信任损失是简单的超时永远不会造成的。

解决办法不是更快的智能体,而是一个交付时的相关性门控,它将返回的时刻视为一个全新的决定,而不是派发时刻预设的定论。

派发时间谬误

异步智能体任务的默认心理模型是等待时间较长的“请求-响应”。用户提出要求,智能体进行处理,然后在某个时间点返回答案。框架也倾向于这种构架——续订令牌、轮询端点、完成回调(webhooks)、以及工作完成时的推送通知。基础设施很稳固。

但基础设施没有体现的是,任务的输入不仅仅是派发时的提示词(prompt)。输入是从派发到交付期间会话状态的整个轨迹。如果用户说“帮我找下周二去东京最便宜的机票”,然后在 90 秒后,他们已经查看了日历,取消了下周二的会议,并重新预订了下个月的行程,那么原始答案不仅是陈旧的——相对于智能体从未见过的世界状态来说,它甚至是错误的。

派发时间谬误假设:用户提问时成立的事实,在智能体回答时依然是正在处理的问题。在同步对话中,这一假设是成立的,因为用户正盯着加载动画。而在后台任务中,这一假设本身就是个 Bug。

为什么 90 秒是危险区间

亚秒级的响应存在于同步世界中;用户被锁定在对话中。耗时数天的任务存在于明确交付的世界中;用户已经提交了请求并离开了,结果明天出现在收件箱里正是他们所预期的。

危险区间是中间地带——时间长到用户不会等待,但又短到他们还没有在心理上将该任务归类为离线处理。30 秒到 5 分钟大约是“空屋子”问题的高峰期,因为系统基于“用户仍停留在派发任务时的页面”这一假设在运行,而用户则基于“可以自由去做别的事情”这一假设在行动。

这也是智能体框架竞争最激烈的区间。运行数十分钟的深度研究智能体足够庞大,以至于每个人都会为它们设计收件箱。亚秒级的工具调用足够快,以至于没人会费心去搞异步。唯独在这个中间地带,懒惰的默认做法——“我们直接把结果发回用户之前所在的位置”——产生的危害最大。

交付时的相关性门控

架构上的转变是在交付时刻插入检查,而不仅仅是在派发时刻。该检查回答一个问题:结果是否仍然适用于当前的会话状态?

这种门控的一个可行方案包含三个部分。

首先,在任务开始时快照相关的会话状态,并随任务一起发送。这不是完整的对话记录,而是智能体推理所依赖的一切——当前文档、对话线程 ID、页面或工具上下文、以及使提示词具有意义的参数。对其进行哈希(Hash)处理。

其次,在交付时,根据当前会话状态重新计算相同的哈希值并进行比较。哈希值相等意味着用户没有移动,结果可以直接应用。哈希值不同意味着用户已经移动,系统需要判断这种差异是否重要。

第三,根据该差异将结果分类到三个桶中。仍适用 (Still-applicable):当变化只是表面性的——用户在相同上下文中导航、滚动,或者进行了不影响答案有效性的编辑。仅建议 (Advisory-only):当变化是实质性的,但结果作为信息而非操作仍然有用——将其作为通知呈现,让用户选择是否应用,而不是直接修改状态。丢弃 (Discard):当变化已使底层问题失效——记录结果,告知用户原始任务已被后续事件取代,不要静默应用任何内容。

对于模糊情况,默认应为“仅建议”,而非“仍适用”。存疑时,显示通知,不要修改状态。

快照需要捕捉的内容

人们往往容易产生过度快照的冲动——存储完整的会话、完整的 DOM 状态、每一项偏好。这会产生一个过于敏感的哈希值:每一次细微的视觉变动都会导致结果失效,丢弃率飙升,工程师们最终会因为干扰太大而关闭门控(gate)。

另一个极端——仅对提示词文本进行快照——是目前大多数系统的做法。这又太迟钝了:系统无法察觉用户是否已切换到另一个文档、开始了新会话,或者撤销了最初触发任务的操作。

合适的粒度应该是提示词的因果包络(causal envelope):即在保持恒定的情况下,能使原始提示词的原意依然成立的最小状态集。对于“总结此文档”任务,它是文档 ID 和版本。对于“寻找最便宜的航班”任务,它是行程参数加上日历窗口。对于编码代理的“修复此测试”任务,它是文件路径、测试名称以及工作树哈希。这种准则是针对每种任务类型去问:世界需要发生什么样的变化,才会导致这个答案不再是用户想要的答案。

这并非框架的通用属性。它是代理作者拥有的、针对每个任务的契约,就像 HTTP 端点拥有其幂等键(idempotency key)一样。

是通知,而非自动应用

当门控将结果标记为仅供参考或丢弃时,用户体验(UX)与架构同样重要。大多数助手的默认做法是就地应用(apply in-place)结果——草拟邮件、编辑文档、创建日历事件。当用户正在注视屏幕时,就地应用没有问题。但当用户已经离开时,这种做法就极具侵略性,因为出现了非用户直接引导的状态变化,这会侵蚀他们对系统代劳行为的认知模型。

行之有效的模式是在 UI 中区分两种交付模式。“你之前的请求已就绪”是一个通知,范围限定在原始任务内,用户在愿意切换上下文时主动打开它。“我正在继续我们刚才的操作”是内联状态变更(in-line state mutation),仅在门控确认用户仍处于相同上下文时才有效。

这两种模式外观各异。一种带有徽标(badge)、收件箱和明确的接受或丢弃步骤。另一种则完全没有多余的修饰(chrome),因为用户已经处于闭环中。将两者混为一谈会产生突如其来的意外——无缘无故产生的状态应用——从而摧毁信任。

评估什么

异步代理的评估套件(eval suites)往往根据其生成的答案质量进行评分,并按任务类型和难度划分。这完全忽略了“空屋”故障(empty-room failure),因为答案本身可能是正确的,但交付上下文已经失效了。

捕捉此类问题的评估需要对抗性场景,即用户的会话状态在任务执行中途发生变化。构建测试用例来派遣任务,确定性地改变会话——切换文档、开始新线程、撤销原始操作——并断言系统接下来的行为。通过标准不是答案是否正确,而是系统在给定增量(delta)的情况下,是正确应用、正确降级为建议,还是正确丢弃。

评估套件中值得加入的几个具体场景:

  • 执行中上下文切换(Context-switch mid-flight):派遣任务,导航到另一个文档,等待交付。断言结果以通知形式交付,而非自动应用。
  • 执行中撤销(Reversal mid-flight):派遣任务,撤销触发任务的操作。断言结果被丢弃并附带面向用户的日志记录,而非悄无声息地应用到已无关的状态。
  • 无操作变更(No-op mutation):派遣任务,滚动文档,等待交付。断言结果就地交付;门控没有因细微的视觉变化而过度触发。
  • 重问变体(Re-asked variant):派遣任务,在第一个任务完成前开始一个相关但不同的任务。断言两个结果都被正确路由,且互不覆盖。

综合指标不是准确率。它是每千次后台完成后产生的过时应用错误率(stale-application errors),并按增量类型分类。一个通过悄悄丢弃所有任务来将应用错误降至零的系统并没有胜出;评估需要同时衡量误报应用(false-applies)和误报丢弃(false-discards)。

需要内化的不变性

真正的架构认知是:异步代理任务不是一个函数调用。函数调用的输入在调用瞬间就固定了,唯一的输出就是函数的返回值。而异步代理任务的输入是会话状态从开始到交付的轨迹(trajectory),系统在交付时的行为是契约的一部分,而非事后的补救。

一旦你接受了这一点,设计压力就会转移。续签令牌(Continuation tokens)是必要的,但还不够。状态持久化是必要的,但还不够。足够的是交付时的相关性检查、每个任务对“有意义的状态变更”的定义,以及将通知和内联应用视为不同的原生模式的 UX。

那些能够处理好这一点的团队,是在用户在生产环境中察觉之前,就已经针对“空屋”场景运行评估的团队。而那些搞砸了的团队,将花费一个季度的时间来弄清楚为什么他们的异步功能投诉率比它取代的同步功能还要高——答案将是:异步路径正在做与同步路径完全相同的事情,只不过晚了 90 秒,且针对的是用户早已离开的状态。

References:Let's stay in touch and Follow me for more thoughts and updates