Skip to content

Agent 请求队列与调度设计

Agent 运行可能包含模型调用、知识库检索、工具执行和文件读写,持续时间通常高于普通接口请求。在一次运行尚未结束时,同一对话线程可能收到新的用户输入或外部调用。请求队列用于接收这些请求,并控制它们进入 Agent 执行链路的顺序。

本文介绍 Yuxi Agent 请求队列的设计目标、调度规则、状态变化和当前功能边界。具体接口、数据表和事务实现不在本文展开。

设计目标

请求队列主要处理以下问题:

  • 同一对话线程内的多个请求不应并发修改同一份对话上下文。
  • Agent 运行期间仍可接收后续请求,不要求调用方等待当前运行结束后再次提交。
  • 排队状态需要持久化,页面刷新或服务恢复后仍可查询。
  • 调用方需要区分“请求已接收”和“Agent 已开始运行”。
  • 网页聊天和同步 API 对忙碌线程的处理方式不同,需要提供明确的策略选择。

请求队列不负责提高单次 Agent 运行速度,也不改变 Agent 内部的模型和工具执行方式。它负责确定请求何时进入现有的运行链路。

调度范围

队列以用户、Agent 和对话线程共同确定的执行范围为单位。同一范围内最多存在一个活跃的 Agent 运行,后续请求按提交顺序等待。

不同对话线程拥有独立的调度范围,可以并行运行。例如,一个用户在两个不同对话中分别提交任务,两条线程不会因为队列而相互阻塞。

text
线程一:请求 A(运行中) -> 请求 B(队列第 1 位) -> 请求 C(队列第 2 位)
线程二:请求 D(运行中) -> 请求 E(队列第 1 位)

队列采用 FIFO(先入先出)规则。顺序以服务端记录的创建时间和稳定顺序字段为准,不依赖浏览器时间。

请求与运行

Yuxi 将 Agent 请求和 Agent 运行作为两个不同阶段处理。

Agent 请求表示系统已经接收了一次输入。请求在提交时创建,可处于排队、已派发、已取消或已拒绝等状态。

Agent 运行表示请求已经获得执行机会,并进入实际的 Agent 执行链路。只有请求被派发后,才会创建对应运行。

这种划分主要有三项作用:

  1. 排队请求可以独立查询和取消。
  2. 页面刷新后可以恢复队列,而不依赖前端内存状态。
  3. 排队中的用户消息不会提前进入当前 Agent 运行的上下文。

第三点用于保证对话顺序。假设请求 A 正在运行,请求 B 和 C 已经排队,A 对应的 Agent 上下文不会提前包含 B 和 C。B 被派发后,其输入才会成为下一轮 Agent 运行的一部分。

调度过程

一次普通请求的处理过程如下:

  1. 系统接收输入并创建请求记录。
  2. 如果线程当前空闲,请求立即派发并创建 Agent 运行。
  3. 如果请求不能立即成为并派发 FIFO 队头,请求根据队列策略进入等待或被拒绝。
  4. 运行成功结束后,调度器检查同一线程的队头请求。
  5. 如果存在排队请求,队头请求被派发,其他请求的位置相应前移。

同一请求不会因为客户端重试而重复排队。调用方使用相同请求 ID 重试时,系统返回已有请求及其运行状态。请求 ID 被其他用户或不匹配的目标复用时,应作为冲突处理。

队列策略

当前支持 enqueuereject 两种策略。

策略线程空闲线程忙碌主要适用场景
enqueue立即派发保存并进入 FIFO 队列网页聊天、异步 Agent Call
reject立即派发返回拒绝结果,不进入队列同步 Agent Call、需要立即决策的调用方

enqueue

enqueue 用于允许延后执行的请求。请求排队后,调用方可以读取其当前位置,也可以在派发前取消。

网页聊天默认采用该策略,因此当前回复生成期间仍可提交后续输入。排队请求与当前回复分开展示,避免尚未执行的输入提前出现在对话正文中。

reject

reject 用于不接受排队的调用。只要请求不能立即成为并派发 FIFO 队头,系统就记录并返回拒绝状态,不创建 Agent 运行。这包括线程忙碌、已有积压请求、队列因失败或取消暂停,以及运行正在等待人工回答的情况。

同步 Agent Call 默认采用该策略。同步调用会等待最终运行结果,如果允许其进入队列,请求等待时间将同时包含排队和执行两个阶段。因此,同步入口通过明确拒绝,使调用方可以自行决定重试、切换线程或终止本次调用。

拒绝是预期的调度结果,不属于服务器内部错误。

状态说明

请求和运行分别维护状态。请求状态用于描述排队阶段,运行状态用于描述实际执行阶段。

请求状态说明
queued请求已保存,正在等待派发
dispatched请求已派发,并已关联 Agent 运行
cancelled请求在派发前被取消
rejected采用 reject 策略时因线程忙碌被拒绝
failed请求在派发前处理失败

请求派发后,执行结果由 Agent 运行状态表达,例如完成、失败、取消或中断。前端在排队阶段订阅请求状态,在收到运行创建信息后切换到运行事件流。

取消处理

取消排队请求和停止 Agent 运行是两个独立操作。

  • 排队请求尚未开始执行,可以单独取消。取消后不会影响当前活跃运行,后续请求的位置会重新计算。
  • 已派发请求已经进入运行阶段,需要通过运行取消能力停止,不再通过队列取消接口处理。

这种区分可以避免取消一个排队项时误停当前运行,也可以保持请求状态与实际执行状态一致。

失败、中断与后续请求

Agent 运行成功完成后自动派发下一条请求。运行失败、被取消或进入需要人工处理的中断状态时,系统按以下规则处理后续请求:

  • failed/cancelled 时已经在等待的请求会保持暂停。页面会展示原因,用户可以点击“继续队列”;该动作只派发当前 FIFO 队头。
  • failed/cancelled 发生时队列为空,之后提交的新请求属于新的输入意图,可以正常立即执行。
  • interrupted 表示当前运行正在等待回答或审批。中断前已经存在的排队请求继续保留;中断期间的新普通请求会在写入 Message/Request 前返回 run_interrupted,也不能通过“继续队列”绕过。用户完成 resume 后,既有队列才会按原有完成链路继续。

页面刷新后会恢复暂停原因和继续操作。若完成后的自动派发因短暂故障遗漏,系统会把该队列识别为待恢复状态并继续既有 completed 调度语义,而不会把仍有请求的队列视为已空闲。

持久化与恢复

请求、输入消息和派发关系保存在数据库中。浏览器刷新后,前端可以重新读取当前线程的排队请求和位置。

Agent 运行由后台任务系统执行。pending AgentRun 同时表达已经提交、仍需投递或等待 worker 接收的执行意图。为处理“数据库已经记录派发,但任务尚未成功投递”这一故障窗口,completed hook 重试和服务启动恢复都会优先重新投递已有 pending run;没有 pending run 时才会派发 ready 队头。恢复过程复用已有请求和运行记录,不创建重复运行。

同一对话线程的 intake、resume、continue 和自动接力会锁定线程对应的 Conversation 记录,再读取和修改 request/run 事实。线程级共同锁负责保证并发请求的严格 FIFO,active-run 唯一索引继续作为最终数据库保护。

持久化恢复保证的是调度状态可继续处理,不代表失败中的 Agent 运行会自动重新执行。具体是否重试由运行层的重试规则决定。

对话展示

排队区和对话正文承担不同职责:

  • 排队区展示尚未开始的请求、当前位置和取消操作。
  • 对话正文展示已经进入 Agent 运行的用户输入和回复。

请求派发后,其用户消息从排队状态转入对应运行轮次。多个请求的展示顺序与执行顺序一致,例如:

text
请求 A -> A 的回复 -> 请求 B -> B 的回复

排队中的 B 不会覆盖或打断正在生成的 A 的回复。

当前范围

当前版本包含以下能力:

  • 普通聊天、异步 Agent Call 和评估入口使用统一请求接收流程。
  • 支持 enqueuereject 策略。
  • 同一线程串行调度,不同线程可并行运行。
  • 支持排队位置查询、页面刷新恢复和派发前取消。
  • 请求派发后转入已有 Agent 运行与事件流链路。

当前版本不包含以下能力:

  • 运行中的输入引导(steer)。
  • 请求优先级和插队。
  • 自动替换当前运行。
  • 运行失败后的自动回滚。
  • 多个请求合并为一次 Agent 运行。

这些能力涉及运行上下文、工具副作用和消息展示语义,需要在扩展队列策略时分别设计。

本项目基于 MIT License 开源,欢迎使用和贡献。