13 KiB
Agent 请求队列与调度设计
Agent 运行可能包含模型调用、知识库检索、工具执行和文件读写,持续时间通常高于普通接口请求。在一次运行尚未结束时,同一对话线程可能收到新的用户输入或外部调用。请求队列用于接收这些请求,并控制它们进入 Agent 执行链路的顺序。
本文介绍 Yuxi Agent 请求队列的设计目标、调度规则、状态变化和当前功能边界。具体接口、数据表和事务实现不在本文展开。
设计目标
请求队列主要处理以下问题:
- 同一对话线程内的多个请求不应并发修改同一份对话上下文。
- Agent 运行期间仍可接收后续请求,不要求调用方等待当前运行结束后再次提交。
- 排队状态需要持久化,页面刷新或服务恢复后仍可查询。
- 调用方需要区分“请求已接收”和“Agent 已开始运行”。
- 网页聊天和同步 API 对忙碌线程的处理方式不同,需要提供明确的策略选择。
请求队列不负责提高单次 Agent 运行速度,也不改变 Agent 内部的模型和工具执行方式。它负责确定请求何时进入现有的运行链路。
调度范围
队列以用户、Agent 和对话线程共同确定的执行范围为单位。同一范围内最多存在一个活跃的 Agent 运行,后续请求按提交顺序等待。
不同对话线程拥有独立的调度范围,可以并行运行。例如,一个用户在两个不同对话中分别提交任务,两条线程不会因为队列而相互阻塞。
线程一:请求 A(运行中) -> 请求 B(队列第 1 位) -> 请求 C(队列第 2 位)
线程二:请求 D(运行中) -> 请求 E(队列第 1 位)
普通请求采用 FIFO(先入先出)规则。顺序以服务端记录的创建时间和稳定顺序字段为准,不依赖浏览器时间;待处理的 Steer 会成为下一条请求,其余请求之间仍保持 FIFO。
请求与运行
Yuxi 将 Agent 请求和 Agent 运行作为两个不同阶段处理。
Agent 请求表示系统已经接收了一次输入。请求在提交时创建,可处于排队、已派发、已取消或已拒绝等状态。
Agent 运行表示请求已经获得执行机会,并进入实际的 Agent 执行链路。只有请求被派发后,才会创建对应运行。
这种划分主要有三项作用:
- 排队请求可以独立查询和取消。
- 页面刷新后可以恢复队列,而不依赖前端内存状态。
- 排队中的用户消息不会提前进入当前 Agent 运行的上下文。
第三点用于保证对话顺序。假设请求 A 正在运行,请求 B 和 C 已经排队,A 对应的 Agent 上下文不会提前包含 B 和 C。B 被派发后,其输入才会成为下一轮 Agent 运行的一部分。
调度过程
一次普通请求的处理过程如下:
- 系统接收输入并创建请求记录。
- 如果线程当前空闲,请求立即派发并创建 Agent 运行。
- 如果请求不能立即成为并派发 FIFO 队头,请求根据队列策略进入等待或被拒绝。
- 运行成功结束后,调度器检查同一线程的队头请求。
- 如果存在排队请求,队头请求被派发,其他请求的位置相应前移。
同一请求不会因为客户端重试而重复排队。调用方使用相同请求 ID 重试时,系统返回已有请求及其运行状态。请求 ID 被其他用户或不匹配的目标复用时,应作为冲突处理。
队列策略
当前支持 enqueue、reject 和 steer 三种策略。
| 策略 | 线程空闲 | 线程忙碌 | 主要适用场景 |
|---|---|---|---|
enqueue |
立即派发 | 保存并进入 FIFO 队列 | 网页聊天、异步 Agent Call |
reject |
立即派发 | 返回拒绝结果,不进入队列 | 同步 Agent Call、需要立即决策的调用方 |
steer |
立即派发 | 当前步骤结束后优先执行 | 运行中修正后续方向 |
enqueue
enqueue 用于允许延后执行的请求。请求排队后,调用方可以读取其当前位置,也可以在派发前取消。
网页聊天默认采用该策略,因此当前回复生成期间仍可提交后续输入。排队请求与当前回复分开展示,避免尚未执行的输入提前出现在对话正文中。
reject
reject 用于不接受排队的调用。只要请求不能立即成为并派发 FIFO 队头,系统就记录并返回拒绝状态,不创建 Agent 运行。这包括线程忙碌、已有积压请求、队列因失败或取消暂停,以及运行正在等待人工回答的情况。
同步 Agent Call 默认采用该策略。同步调用会等待最终运行结果,如果允许其进入队列,请求等待时间将同时包含排队和执行两个阶段。因此,同步入口通过明确拒绝,使调用方可以自行决定重试、切换线程或终止本次调用。
拒绝是预期的调度结果,不属于服务器内部错误。
steer
steer 是 enqueue 的优先执行形式,不引入新的请求或运行状态。请求仍以 queued 保存;Chatbot Middleware 在下一次模型调用前发现待处理 Steer 时结束当前 Graph,worker 按既有 completed 接力流程派发该请求。
因此,已经开始的模型调用和工具批次会正常完成并写入 checkpoint,Steer 不会强制取消工具。当前 Run 按普通 completed 结束,Steer 创建的新 Run 继续读取同一线程上下文。已有普通 Chat 排队项也可以原地提升为 Steer;同一线程一次只接受一个待处理 Steer。
Steer 意图采用持久化请求作为唯一事实来源,按以下生命周期边界消费,避免到达时机造成丢失:
- 请求事务提交后,
abefore_model在下一次模型调用前检查待处理 Steer。 aafter_model对不含工具调用的模型轮次再次检查,覆盖 Steer 恰好到达最后一次模型检查之后的窗口;含工具调用时不跳过工具批次。- 当前 Run 以
completed结束后,worker 通过队列头派发 Steer;若进程在接力前退出,worker 启动恢复会重新扫描 queued 请求并执行同一派发逻辑。
这套兜底只保证 Steer 意图最终进入下一次 Run,不改变“已开始的模型调用和完整工具批次不可强制终止”的安全边界。
状态说明
请求和运行分别维护状态。请求状态用于描述排队阶段,运行状态用于描述实际执行阶段。
| 请求状态 | 说明 |
|---|---|
queued |
请求已保存,正在等待派发 |
dispatched |
请求已派发,并已关联 Agent 运行 |
cancelled |
请求在派发前被取消 |
rejected |
采用 reject 策略时因线程忙碌被拒绝 |
failed |
请求在派发前处理失败 |
请求派发后,执行结果由 Agent 运行状态表达,例如完成、失败、取消或中断。前端在排队阶段订阅请求状态,在收到运行创建信息后切换到运行事件流。
| 运行状态 | 执行 ownership 与结局 |
|---|---|
pending |
数据库已提交的投递意图,尚未由 worker attempt 取得 lease |
running |
当前 attempt 持有 lease 并周期 heartbeat;其他 attempt 不得并行执行 |
cancel_requested |
取消意图已记录,当前 owner 在安全边界停止;lease 仍用于识别失联 worker |
completed / failed / cancelled |
终态写入只接受当前 owner,并同时清除 lease |
interrupted |
等待用户回答或审批,可由显式 resume 请求恢复 |
取消处理
取消排队请求和停止 Agent 运行是两个独立操作。
- 排队请求尚未开始执行,可以单独取消。取消后不会影响当前活跃运行,后续请求的位置会重新计算。
- Steer 在等待活跃 Run 到达安全点时不能取消,避免取消操作与 Middleware 消费引导意图竞态;若目标 Run 失败或取消、队列进入暂停后,可以删除该 Steer。
- 已派发请求已经进入运行阶段,需要通过运行取消能力停止,不再通过队列取消接口处理。
这种区分可以避免取消一个排队项时误停当前运行,也可以保持请求状态与实际执行状态一致。
运行取消先在 PostgreSQL 提交 cancel_requested,Redis key/pubsub 只负责降低 worker 感知延迟。Worker 也会低频读取数据库,因此 Redis 丢信号时仍能停止;只有再次确认 durable 取消事实才写 cancelled。Worker shutdown、ARQ timeout 等基础设施 CancelledError 会释放当前 lease 并继续向上传播,不能冒充用户取消;临时执行故障释放 lease 后必须抛出 ARQ 原生 RetryJob,不能用普通异常把数据库中的 pending 留成无投递事实。实时事件写入失败可以造成 SSE 缺口,但不得阻断 PostgreSQL retry/终态,客户端仍以终态补偿。
失败、中断与后续请求
Agent 运行成功完成后自动派发下一条请求。运行失败、被取消或进入需要人工处理的中断状态时,系统按以下规则处理后续请求:
- failed/cancelled 时已经在等待的请求会保持暂停。页面会展示原因,用户可以点击“继续队列”;该动作只派发当前 FIFO 队头。
- failed/cancelled 发生时队列为空,之后提交的新请求属于新的输入意图,可以正常立即执行。
- interrupted 表示当前运行正在等待回答或审批。中断前已经存在的排队请求继续保留;中断期间的新普通请求会在写入 Message/Request 前返回
run_interrupted,也不能通过“继续队列”绕过。用户完成 resume 后,既有队列才会按原有完成链路继续。
页面刷新后会恢复暂停原因和继续操作。若完成后的自动派发因短暂故障遗漏,系统会把该队列识别为待恢复状态并继续既有 completed 调度语义,而不会把仍有请求的队列视为已空闲。
持久化与恢复
请求、输入消息和派发关系保存在数据库中。浏览器刷新后,前端可以重新读取当前线程的排队请求和位置。
Agent 运行由后台任务系统执行。pending AgentRun 同时表达已经提交、仍需投递或等待 worker 接收的执行意图。为处理“数据库已经记录派发,但任务尚未成功投递”这一故障窗口,completed hook 重试和服务启动恢复都会优先重新投递已有 pending run;没有 pending run 时才会派发 ready 队头。恢复过程复用已有请求和运行记录,不创建重复运行。
Worker 取得 Run 时写入带进程 identity 的唯一 attempt token、heartbeat 和 lease 到期时间。Heartbeat 只允许当前 token 续租;正常终态和 ARQ retry publication 都先在数据库中关闭或释放 ownership。Worker 启动与周期扫描会把无 lease 或已过期的 running / cancel_requested 幂等收敛为 failed,并记录 worker_lease_expired。interrupted 只表示等待用户回答或审批,不能被 worker 崩溃复用。Lease 失败表示进程死亡窗口中的外部工具副作用可能已经发生,系统不会把它伪装成可安全自动重试或 exactly-once。
同一对话线程的 intake、resume、continue 和自动接力会锁定线程对应的 Conversation 记录,再读取和修改 request/run 事实。线程级共同锁负责保证并发请求的严格 FIFO,active-run 唯一索引继续作为最终数据库保护。
持久化恢复保证的是调度状态可继续处理,不代表失败中的 Agent 运行会自动重新执行。具体是否重试由运行层的重试规则决定。
对话展示
排队区和对话正文承担不同职责:
- 排队区展示尚未开始的请求、当前位置和取消操作。
- 对话正文展示已经进入 Agent 运行的用户输入和回复。
请求派发后,其用户消息从排队状态转入对应运行轮次。多个请求的展示顺序与执行顺序一致,例如:
请求 A -> A 的回复 -> 请求 B -> B 的回复
排队中的 B 不会覆盖或打断正在生成的 A 的回复。
当前范围
当前版本包含以下能力:
- 普通聊天、异步 Agent Call 和评估入口使用统一请求接收流程。
- 支持
enqueue、reject和主会话 Chat 的steer策略。 - 同一线程串行调度,不同线程可并行运行。
- 支持排队位置查询、页面刷新恢复和派发前取消。
- 请求派发后转入已有 Agent 运行与事件流链路。
当前版本不包含以下能力:
- 强制取消正在执行的模型或工具。
- 多个 Steer 的排序、合并或连续接替。
- 通用请求优先级和任意插队。
- 运行失败后的自动回滚。
- 多个请求合并为一次 Agent 运行。
这些能力涉及运行上下文、工具副作用和消息展示语义,需要在扩展队列策略时分别设计。