4.9 KiB
Agent Note: 移除普通 send 的隐式批处理
Status: implemented
English | 中文
问题
每次普通 Agent.send() 接受的载荷都是一条完整的调用方消息。如果机会式地把所有待处理载荷放入同一个轮次,相邻调用是否共享边界就会取决于驱动器的运行时机:即使调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
普通轮次包含提示词准入、turn/start、turn/end 和持久性检查点。合并消息会让后一条普通消息加入前一条普通消息的模型请求,无法观察同一会话日志中前一个已关闭普通轮次的结果;获准与被阻止提示词的混合还会引入调用方从未显式请求的生命周期状态。
steer() 已经用于在 agent(智能体)驱动器运行时选择 steering(中途引导)语义,inject() 则记录面向模型的上下文而不充当普通消息。普通 send 的隐式批处理会让 send() 与这两种显式操作产生语义重叠,无法保持单一含义。
决策
每次成功的 send() 都会同步校验 agent(智能体)状态、创建并冻结内容快照、追加一个独立的 FIFO 队列项,然后发布 agent/queued。agent loop(智能体循环)在每个轮次开始时最多取出一个普通队列项。如果两个普通队列项最终都进入轮次处理,第二个普通轮次只能在第一个普通轮次结束且其持久性检查点处理结束后开始;广义取消、dispose(资源释放)或启动前失败可以丢弃尚未启动的队列项,而不创建空轮次。
提示词准入只处理一条消息。获准提示词成为该轮次的 user/message;被阻止提示词追加一条持久的 prompt/blocked,并让这个单消息轮次以 rejected 结束。实现中没有混合批次或全阻止批次分支。
运行中的 steer() 会把消息加入 steering(中途引导)FIFO。打开的轮次会在下一个 steering 检查点记录该消息;该检查点发生在请求或 continuation 决策之前。Steering 可以让默认 continuation 决策进入下一步骤,但 continuation 或终止策略仍可在该步骤开始前停止轮次。轮次关闭且其持久性检查点处理结束后,剩余的 steering 会成为后续排队输入。终止性的 agent/turn-stop、取消或 dispose(资源释放)可能丢弃该消息。空闲时的 steer() 委托给 send(),因此创建一个独立的普通队列项。inject() 保持现有的轮次封闭与持久化刷新行为。cancel()、status 和 whenIdle() 仍是面向整个 agent 的操作,不变成逐消息控制。
曾考虑的替代方案
为吞吐量保留普通 send 的机会式批处理。 当消息进入队列的速度超过驱动器的处理速度时,合并排队的普通提示词可以减少模型调用,但会让轮次边界取决于调度,并让后一条普通消息在前一个普通轮次关闭且其检查点处理结束之前就运行。额外模型调用的代价低于显式生命周期语义的价值;未来的任何普通 send 批处理功能都必须提供调用方可见的显式契约,并由测量结果证明其必要性。
验证
- 单元与性质覆盖固定了同一调用栈、相邻微任务、不同来源和重入 send 的行为:每个轮次只有一条消息,并按 FIFO 排序。
- 真实组合测试会通过 stdio 构建产物同时写入两行,并观察两个模型请求和两个轮次边界。
- 延迟第一个普通轮次的持久化刷新可以证明下一个排队的普通轮次不能在检查点处理结束前开始,且其请求能看到前一条助手结果;刷新即使失败,下一个普通轮次也要等它结束后才会开始。
- 提示词否决、监听器失败、广义取消、dispose 和
turn/start提交前失败都会保持已记录轮次边界平衡,不会合并消息或让仍应处理的排队工作滞留。 - 轮次打开时、轮次关闭后与空闲时的
steer()、inject()、面向整个 agent 的状态和whenIdle()保持原有覆盖。
后果
普通轮次边界是确定的;FIFO 后继项进入轮次处理时,会观察前一个已完成普通轮次在会话中已关闭的结果;检查点处理结束不表示失败的持久化刷新已经成功。多个排队项仍可在同一个全局 running 区间内执行,该区间也可以覆盖轮次关闭及其检查点,因此 running 不表示轮次必然仍然打开。广义取消可以丢弃整个未启动队尾,状态和静止性仍是面向整个 agent 的观察,而不是逐消息结果。
依赖普通 send 偶然批处理的工作负载会产生更多模型请求和检查点,队列清空时间也可能延长;持续有消息进入时,FIFO 队列还可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。