
给 Agent 工具套上超时后,调用方收到 TIMEOUT,很容易把这一步记成「工具已停止」。这两个状态不是一回事。超时可以结束等待,运行中的异步工作仍可能继续;如果它稍后写文件或提交订单,用户看到失败时,副作用却已经发生。
用固定的 Python 协程就能把边界拆开。慢操作等 30 毫秒再写一个本地布尔值,调用方只等 5 毫秒。另一组协程在超时后收到任务取消。脚本只用本机协程,不接模型服务。asyncio.wait 只报告哪些任务已完成、哪些仍在等待;返回 pending 集合时,不会替调用方取消那些任务。
工具超时,为什么还会产生副作用?
第一组里,5 毫秒的等待期限先到,asyncio.wait 返回 pending 任务,调用方停止等待时副作用标记仍是 False。协程没有被取消,等它运行完再看,标记变成 True。这不是模糊的竞态猜测,脚本把调用方返回和操作真正结束分开记录。
operation:start
caller:TIMEOUT:5ms
immediate sideEffect=False
operation:side-effect
later sideEffect=True
任务超时的名字容易让人联想到「任务出局」,但等待结束和任务取消仍是两件事。Python 的 asyncio.wait 返回超时结果时,任务仍然留在 pending 集合里。若工具封装只报告等待超时,上层不能据此断定工具已经中止。外部请求或文件写入能不能停,要看对应 API 有没有取消机制,以及工具是否真的响应取消。
这会影响重试。若第一次调用超时后马上重试,第一份工作可能仍在执行,两个请求就会同时修改同一资源。只给 Agent 加一个调用时限不等于保护了副作用;幂等键可以防止重复提交,协作取消可以减少无用工作,它们解决的是不同问题。没有取消能力时,返回状态最好明确写成「等待超时,执行结果未知」,不要伪装成「执行失败且没有副作用」。超时错误描述的是等待边界,不是工作端的最终状态。
等待超时和取消有什么区别?
第二组仍然先让等待方超时,但超时分支对任务调用 cancel()。协程在等待点捕获 CancelledError 并退出;副作用标记保持 False。关键在于取消是否传到正在运行的工作,以及工作是否能在安全位置响应。若底层函数吞掉取消异常或处于不可中断调用中,外层请求取消也不等于它已经停下。
cooperative:start
cooperative caller:TIMEOUT:5ms
cooperative:aborted
cooperative sideEffect=False
取消也不是回滚。发出请求后若远端已经完成写入,客户端本地的 abort 无法把它撤回来。工具若包含多步操作,要分清写入是否已经提交;收到取消时可以停止后续步骤,但已提交部分需要补偿或查询确认。对只读搜索,超时后放任后台工作通常代价有限;对有副作用的工具,我会先保证重试可识别同一操作,再把取消当作节省资源的补充。
发出取消,写入就能回滚吗?
测试只覆盖本机 Python asyncio 任务和等待超时的行为。它没有证明任何模型会如何理解超时文本,也没有测某个 Agent SDK 是否把取消传到工具实现。实际接入时,应查工具函数的取消约定,并在副作用边界记录稳定的调用标识;超时后再查询该标识的状态,通常比立刻盲目重做更稳。
超时后可以直接重试吗?
本地小程序里,普通等待超时后,协程仍然改了状态;主动取消的版本则在副作用前退出。取舍在这里:对无副作用的短任务,超时后等它自然结束可能更简单;对写操作,不能把「调用方不等了」说成「操作取消了」。若底层不支持取消,就把结果记为未知,并用幂等或状态查询收住重试风险。