5.3 KiB
Agent Note: 删除普通 send 的隐式批处理
Status: implemented
English | 中文
问题
假设调用方连续两次调用 Agent.send(),先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。
这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
这种分组改变的不只是模型调用次数。一个普通轮次包含提示词准入、turn/start、turn/end 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统允许一条消息、阻止另一条消息,还需要引入调用方没有请求的混合状态。
决策
规则很简单:一次成功的 send() 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
队列项入队之前,send() 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,send() 发布 agent/queued。
如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 turn/end 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 cancel()、dispose(资源释放)或 turn/start 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
提示词准入每次只决定一条消息。获准提示词成为该轮次的 user/message;被阻止的提示词记录一条持久的 prompt/blocked,并让自己的单消息轮次以 rejected 关闭。实现中不存在混合批次或全阻止批次分支。
上述不合批规则只适用于普通 send()。agent 运行时,steer() 会把输入放入独立的 steering(中途引导)FIFO。只要当前轮次仍然打开,agent loop 就会在下一个 steering 检查点记录该输入;该检查点位于模型请求或继续轮次的决策之前。收到 steering 会把再执行一步作为默认选择,但继续轮次的策略或终止策略仍可在该步骤开始前停止。轮次关闭且其持久性检查点处理结束后,剩余的 steering 会成为后续排队输入;终止性的 agent/turn-stop、取消或 dispose 可以将其丢弃。agent 空闲时,steer() 委托给 send(),因此会创建一个独立的普通队列项。
inject() 继续添加面向模型的上下文,而不提交普通消息;其现有的轮次封闭与持久化刷新行为保持不变。cancel() 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。status 和 whenIdle() 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 running 区间,该区间还可能覆盖轮次关闭及其检查点,因此 running 不表示轮次一定处于打开状态。
曾考虑的替代方案
保留普通 send 的自动批处理,以减少模型调用。 当消息进入队列的速度超过驱动器的处理速度时,这种做法可以提高吞吐量,但会让轮次边界取决于调度,并让后一条消息在前一轮关闭且到达检查点之前运行。本决策保留可预测的边界,并接受额外调用。未来若要加入批处理功能,必须提供调用方可见的显式契约,并有测量结果作为依据。
验证
- 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。
- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。
- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。
- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和
turn/start之前的失败;已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。 - 其他测试分别覆盖轮次打开时、轮次关闭后和空闲时的
steer(),以及inject()、面向整个 agent 的状态和whenIdle()。
后果
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成或取消句柄;广义取消可以丢弃整个尚未启动的队尾,状态和静止性也仍是面向整个 agent 的观察。
代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。