批量 Agent 评测是一个很典型的长任务。
用户上传一份 Excel 或 CSV,里面可能有几百到几千条对话。系统要做数据质检、会话上下文构建、输出解析、LLM-as-judge 逐条评测、坏行修复、问题聚类、行动项生成和报告落库。中间任何一步都可能慢,任何一次模型调用都可能失败。
这类任务当然可以上消息队列。但在中等吞吐的内部评测平台里,第一选择不一定是 Kafka 或 RabbitMQ。很多时候,一张设计清楚的数据库任务表,加上事务原子领取、阶段状态、覆盖率和失败恢复,就足够稳。
这一篇从 Rubric 评测任务的角度,讲不用独立消息队列也能跑长任务的设计。
一、为什么评测不能放在 HTTP 请求里
同步 HTTP 请求适合短操作,不适合评测任务。
评测任务的耗时不可控:文件越大,样本越多;规则越复杂,prompt 越长;模型接口越慢,整体耗时越长。如果把整个流程放在一次请求里,浏览器会超时,网关会超时,用户也不知道任务到底执行到哪一步。
正确交互应该是:
上传文件 → 创建任务 → 立即返回 taskId → 后台 Worker 执行 → 前端轮询进度或报告
上传接口只负责三件事:鉴权、解析文件、落库任务。后续评测由 Worker 接管。
这样用户可以关闭页面再回来,任务状态仍然在数据库里;某个阶段失败,也能展示明确错误,而不是让请求直接断掉。
二、任务表就是轻量队列
一张任务表可以承担队列的核心职责:保存任务、表达状态、支持领取、记录进度。
任务可以有这些字段:
id
ownerUserId
fileName
status
stage
rubricKey
rubricVersion
rubricSnapshot
model
totalRows
totalInstances
diagnosedInstances
failedInstances
coverageRate
warnings
errorMessage
startedAt
completedAt
createdAt
updatedAt
其中 status 和 stage 要分开。
status 表示任务终态或运行状态:
PENDING:等待执行;RUNNING:执行中;COMPLETED:完整完成;PARTIAL:部分完成,但报告可用;INSUFFICIENT_DATA:覆盖率不足,不输出普通报告;FAILED:任务失败。
stage 表示当前执行到哪一步:
QUEUED;DATA_QUALITY;DIAGNOSIS;CLUSTERING;ACTION_GENERATION;REPORTING;DONE。
这两个字段不能混在一起。一个任务可能在 DIAGNOSIS 阶段失败,也可能在 ACTION_GENERATION 阶段部分失败后仍然生成报告。status 告诉用户结果是否可用,stage 告诉系统和排障人员流程停在哪里。
三、原子领取避免多实例重复消费
生产环境里可能有多个应用实例,每个实例都跑内嵌 Worker。如果没有领取机制,同一个任务可能被多个 Worker 同时执行。
数据库任务表可以通过事务和条件更新实现原子领取。
伪流程:
begin transaction
找到最早的 PENDING 任务,或超时的 RUNNING 任务
update where id = ? and status = 原状态
如果更新行数为 1,领取成功
commit
这个 compare-and-set 语义很重要。多个 Worker 同时看到同一个任务时,只有一个能更新成功,其他 Worker 会拿到 0 行更新,然后继续找下一个任务。
如果任务处于 RUNNING 但 updatedAt 很久没变化,可以认为 Worker 异常退出,将它重新纳入领取候选。这个机制相当于轻量租约。
这套方案的好处是简单:任务状态、进度、归属和数据都在同一个数据库事务边界里,不需要处理消息队列和数据库之间的双写一致性。
四、任务开始时固定 Rubric 快照
Rubric 评测任务有一个普通异步任务没有的要求:规则必须可复现。
任务创建时,应该读取当前默认发布版本的 Rubric,并把它的完整 snapshot 保存到任务上。后续执行、报告、导出都使用这份 snapshot,而不是运行时再去读最新规则。
create task
→ load published rubric version
→ save rubricKey / rubricVersion / rubricSnapshot
→ save model / promptVersion / analysisVersion
→ save rows
这样即使评测过程中规则包发布了新版本,也不会影响正在跑的任务。三个月后打开旧报告,也能解释当时每个分数和红线的判断依据。
对评测系统来说,版本快照不是审计锦上添花,而是结果可信度的一部分。
五、行级诊断状态比任务级状态更重要
批量评测最容易犯的错,是只在任务级记录成功或失败。
真实情况更细:一个任务有 1000 条样本,其中 980 条诊断成功,20 条模型输出坏了或接口失败。此时任务不应该整体失败,也不应该假装全部成功。
所以需要为每条可评测样本建立诊断记录:
rowId
taskId
status
attemptCount
intentKey
score
redFlagTypes
referenceBasis
questionsCheck
errorMessage
completedAt
任务进入诊断阶段前,先为所有候选样本初始化 PENDING 记录。每个批次完成后,校验通过的行更新为 COMPLETED,失败行更新为 FAILED,同时刷新任务的 diagnosedInstances、failedInstances 和 coverageRate。
行级状态带来三个好处:
- 好行可以先保存,不被坏行拖累;
- 失败行可以单独重试;
- 覆盖率可以实时更新。
这也是长任务可靠性的基础。
六、并发要受控,不要把模型接口打爆
评测任务最慢的部分通常是 LLM 调用。为了加速,需要并发;为了稳定,又不能无限并发。
可以拆成两个维度配置:
batchSize:每次请求放多少条样本
concurrency:同时跑多少个批次
小 batch 的优点是隔离性好,一条坏输出影响范围小;缺点是请求次数多。大 batch 的优点是吞吐高;缺点是响应更长、更容易出现结构化错误。Rubric 评测因为规则多、输出严格,通常更适合小 batch 加受控并发。
并发上限也要保守。模型接口有速率限制,应用实例也有资源限制。把并发开太高,表面上加速,实际可能造成超时、重试、失败率上升,整体更慢。
比较稳的策略是:默认小批次,最大并发设硬上限,通过环境变量调节,运行日志记录每批耗时和失败率,再逐步校准。
七、覆盖率决定报告是否可用
评测任务不一定 100% 成功。系统要决定什么时候可以出报告,什么时候只能提示数据不足。
覆盖率可以这样计算:
coverageRate = diagnosedInstances / totalInstances
然后定义终态:
| 终态 | 含义 |
|---|---|
COMPLETED | 覆盖率 100% |
PARTIAL | 覆盖率达到阈值,但有失败行 |
INSUFFICIENT_DATA | 覆盖率低于阈值,不输出普通报告 |
阈值可以配置。比如内部试用阶段可以低一些,生产报告可以要求更高。
这里最重要的是文案边界:覆盖率表示报告可信度,不表示 Agent 质量。覆盖率低只能说明诊断完成得少,不能说明 Agent 答得差。
部分完成报告也要显式提示:当前结论基于已完成的诊断样本,请结合覆盖率判断。
八、失败恢复要围绕阶段和行展开
长任务失败大致分三类。
第一类是任务级失败,比如文件解析失败、Rubric 快照缺失、数据库不可用。这类失败应该让任务进入 FAILED,并保存错误信息。
第二类是阶段级失败,比如聚类或报告生成失败。诊断明细可能已经存在,但最终报告不可用。这时 stage 能帮助定位停在哪一步。
第三类是行级失败,比如某些样本模型输出无法修复。这类失败不应该拖垮整个任务,只影响覆盖率。
重试策略也要分层:
- 任务级失败:用户点击重试,重新进入队列;
- 阶段级失败:根据已保存的中间结果从对应阶段继续;
- 行级失败:只重试失败样本,不重跑全部样本。
这要求中间结果必须及时落库,而不是等整条流水线结束再一次性保存。长任务的可靠性,很多时候就来自「走一步存一步」。
九、报告生成不应该依赖内存状态
任务跑完后,报告应该完全由数据库状态生成,而不是依赖 Worker 内存里的临时对象。
原因很简单:Worker 可能重启,用户可能过几天才打开报告,任务可能由一个实例执行、另一个实例服务页面请求。如果报告依赖内存,系统就不可靠。
报告生成应该只依赖这些持久化数据:
- 任务记录;
- 原始对话行;
- 诊断明细;
- 问题簇;
- 行动项;
- 人工决策;
- Rubric snapshot;
- 导出快照。
这样任何时候重新打开任务,都能重建同一份报告。需要缓存导出内容时,也应该保存为 export snapshot,而不是靠运行时临时拼。
十、什么时候真的需要消息队列
不用消息队列不是说永远不用。
当系统出现这些情况时,就可以考虑引入 MQ:
- 任务吞吐明显超过单库轮询能力;
- 需要跨服务分发不同类型任务;
- 需要复杂优先级、延迟队列或死信队列;
- Worker 和 Web 服务必须完全解耦部署;
- 任务执行对实时性有更严格要求;
- 已经有成熟 MQ 运维体系。
在此之前,数据库任务表往往更适合早期和中等规模内部平台。它简单、可查、可事务化,本地开发也轻。评测系统真正复杂的地方是 Rubric、诊断协议、失败隔离和报告闭环,不一定要把基础设施复杂度提前拉满。
十一、总结

批量 Agent 评测慢、贵、会失败,所以它必须被当成长任务设计。
但长任务不等于一上来就需要消息队列。对于中等吞吐的评测平台,一张数据库任务表,加上事务原子领取、阶段状态、行级诊断、覆盖率、失败恢复和持久化报告,已经能支撑很长一段路。
这篇的核心不是反对 MQ,而是强调任务语义先于架构热闹。先把状态机设计清楚,把每一步结果落库,把失败边界划明白,再决定是否需要更重的队列基础设施。
评测任务慢且会失败,状态机比热闹架构更重要。