Agent 请求队列与调度设计
Agent 运行可能包含模型调用、知识库检索、工具执行和文件读写,持续时间通常高于普通接口请求。在一次运行尚未结束时,同一对话线程可能收到新的用户输入或外部调用。请求队列用于接收这些请求,并控制它们进入 Agent 执行链路的顺序。
本文介绍 Yuxi Agent 请求队列的设计目标、调度规则、状态变化和当前功能边界。具体接口、数据表和事务实现不在本文展开。
设计目标
请求队列主要处理以下问题:
- 同一对话线程内的多个请求不应并发修改同一份对话上下文。
- Agent 运行期间仍可接收后续请求,不要求调用方等待当前运行结束后再次提交。
- 排队状态需要持久化,页面刷新或服务恢复后仍可查询。
- 调用方需要区分“请求已接收”和“Agent 已开始运行”。
- 网页聊天和同步 API 对忙碌线程的处理方式不同,需要提供明确的策略选择。
请求队列不负责提高单次 Agent 运行速度,也不改变 Agent 内部的模型和工具执行方式。它负责确定请求何时进入现有的运行链路。
调度范围
队列以用户、Agent 和对话线程共同确定的执行范围为单位。同一范围内最多存在一个活跃的 Agent 运行,后续请求按提交顺序等待。
不同对话线程拥有独立的调度范围,可以并行运行。例如,一个用户在两个不同对话中分别提交任务,两条线程不会因为队列而相互阻塞。
线程一:请求 A(运行中) -> 请求 B(队列第 1 位) -> 请求 C(队列第 2 位)
线程二:请求 D(运行中) -> 请求 E(队列第 1 位)队列采用 FIFO(先入先出)规则。顺序以服务端记录的创建时间和稳定顺序字段为准,不依赖浏览器时间。
请求与运行
Yuxi 将 Agent 请求和 Agent 运行作为两个不同阶段处理。
Agent 请求表示系统已经接收了一次输入。请求在提交时创建,可处于排队、已派发、已取消或已拒绝等状态。
Agent 运行表示请求已经获得执行机会,并进入实际的 Agent 执行链路。只有请求被派发后,才会创建对应运行。
这种划分主要有三项作用:
- 排队请求可以独立查询和取消。
- 页面刷新后可以恢复队列,而不依赖前端内存状态。
- 排队中的用户消息不会提前进入当前 Agent 运行的上下文。
第三点用于保证对话顺序。假设请求 A 正在运行,请求 B 和 C 已经排队,A 对应的 Agent 上下文不会提前包含 B 和 C。B 被派发后,其输入才会成为下一轮 Agent 运行的一部分。
调度过程
一次普通请求的处理过程如下:
- 系统接收输入并创建请求记录。
- 如果线程当前空闲,请求立即派发并创建 Agent 运行。
- 如果请求不能立即成为并派发 FIFO 队头,请求根据队列策略进入等待或被拒绝。
- 运行成功结束后,调度器检查同一线程的队头请求。
- 如果存在排队请求,队头请求被派发,其他请求的位置相应前移。
同一请求不会因为客户端重试而重复排队。调用方使用相同请求 ID 重试时,系统返回已有请求及其运行状态。请求 ID 被其他用户或不匹配的目标复用时,应作为冲突处理。
队列策略
当前支持 enqueue 和 reject 两种策略。
| 策略 | 线程空闲 | 线程忙碌 | 主要适用场景 |
|---|---|---|---|
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 运行的用户输入和回复。
请求派发后,其用户消息从排队状态转入对应运行轮次。多个请求的展示顺序与执行顺序一致,例如:
请求 A -> A 的回复 -> 请求 B -> B 的回复排队中的 B 不会覆盖或打断正在生成的 A 的回复。
当前范围
当前版本包含以下能力:
- 普通聊天、异步 Agent Call 和评估入口使用统一请求接收流程。
- 支持
enqueue和reject策略。 - 同一线程串行调度,不同线程可并行运行。
- 支持排队位置查询、页面刷新恢复和派发前取消。
- 请求派发后转入已有 Agent 运行与事件流链路。
当前版本不包含以下能力:
- 运行中的输入引导(steer)。
- 请求优先级和插队。
- 自动替换当前运行。
- 运行失败后的自动回滚。
- 多个请求合并为一次 Agent 运行。
这些能力涉及运行上下文、工具副作用和消息展示语义,需要在扩展队列策略时分别设计。