- 默认切换 AgentProvider 为 HttpAgentProvider,增强网络请求的容错和重试机制 - 优化 Run 逻辑,支持多场景去重与并发保护 - 添加 Redis 发布失败的日志记录以提升问题排查效率 - 扩展 OpenAPI 规范,新增 Error 和 Run 状态相关模型 - 增强测试覆盖,验证调度策略和重复请求的幂等性 - 增加数据库索引以优化查询性能 - 更新所有相关文档和配置文件
6.7 KiB
6.7 KiB
Agent Orchestrator 工程级 Review
A. 现状总结
整体链路(RunDispatcher → Job → RunLoop → Provider → OutputSink)已能跑通,职责划分基本清晰,append-only + seq 方案也成立;但在并发幂等、run 生命周期一致性、取消可靠性、provider 超时/重试,以及 SSE 丢事件补偿方面仍有明显缺口,尚未达到对接真实 LLM Provider 的最低可靠性门槛。
B. 高风险问题列表
- P0 | 影响: 并发触发同一 prompt 时可能生成“幽灵 run”,run_id 不一致且重复出消息/计费 | 复现: 并发调用
dispatchForPrompt同一trigger_message_id,第二个请求 dedupe 命中但仍派发新 job | 修复: 以appendRunStatus返回的 message 为准,若 run_id 不一致则直接返回已有 run_id 且不 dispatch;或使用确定性 run_id/事务化 get-or-create | 位置:app/Services/RunDispatcher.php:21,app/Services/OutputSink.php:30,app/Services/ChatService.php:61 - P0 | 影响: job 重试/重复派发会生成多个
agent.message,导致重复输出与双重计费 | 复现: 让队列重试或重复 dispatch 同一 run_id | 修复: 给 agent.message 增加 dedupe_key(按 run_id + step/chunk),并在 RunLoop 开始前检查 run.status 是否已终态 | 位置:app/Services/OutputSink.php:16,app/Services/RunLoop.php:45,app/Jobs/AgentRunJob.php:21 - P1 | 影响: 同 session 可并发多个 RUNNING(违反“单 run”假设),后续状态/输出互相覆盖 | 复现: 连续/并发发送多条 prompt;
latestStatus检查无锁 | 修复: 在 session 行锁内判断并创建 RUNNING,或用 Redis/DB 锁/独立 run 表做硬约束 | 位置:app/Services/RunDispatcher.php:40,app/Http/Controllers/ChatSessionController.php:45 - P1 | 影响: run 生命周期非原子写入,崩溃后会出现“有回复但仍 RUNNING”或“RUNNING 无后续” | 复现: 在
appendAgentMessage后杀 worker;或 dispatch 失败后 RUNNING 已落库 | 修复: 将 agent.message 与 DONE 写入同一一致性边界;添加 watchdog/finally 标记 FAILED/CANCELED | 位置:app/Services/RunLoop.php:45,app/Services/RunDispatcher.php:53 - P1 | 影响: cancel 不能严格阻止 agent.message 落库,且取消后可能无 CANCELED 终态 | 复现: cancel 请求在 provider 返回后、写回前到达;或 job 崩溃 | 修复: 在写回前/写回时再校验 cancel;在 finally 中若存在 cancel 请求则写 CANCELED | 位置:
app/Services/RunLoop.php:20,app/Services/CancelChecker.php:9 - P1 | 影响: provider 调用可能无限挂起或频繁失败,run 长时间 RUNNING | 复现: provider 超时/429/5xx | 修复: 设置 Http timeout/retry/backoff,配置 job $timeout/$tries;对 429 做退避 | 位置:
app/Services/Agent/HttpAgentProvider.php:20,app/Jobs/AgentRunJob.php:13 - P2 | 影响: SSE 可能漏事件(backlog 与订阅之间存在窗口),Redis publish 异常会让主流程报错 | 复现: 在 backlog 发送后、订阅前插入消息;或 Redis 异常 | 修复: 订阅后检测 seq gap 回补;publish 异常仅告警不抛 | 位置:
app/Http/Controllers/ChatSessionSseController.php:46,app/Services/ChatService.php:130 - P2 | 影响: 上下文固定 20 条且无 token 预算/截断,遇大消息容易爆 token | 复现: 长文本 prompt/agent 输出 | 修复: 通过配置控制窗口/预算并做截断或摘要 | 位置:
app/Services/ContextBuilder.php:10 - P2 | 影响: JSONB payload 查询无索引,run/cancel 查询随数据量增长会退化 | 复现: 大量消息后 run.status/取消查询变慢 | 修复: 加表达式索引
payload->>'run_id'/payload->>'trigger_message_id'| 位置:app/Services/RunDispatcher.php:28,app/Services/CancelChecker.php:11
C. 对接真实 LLM Provider 前必须补齐的最小门槛清单
- 并发幂等:RunDispatcher 必须做到“同 trigger 只产生一个 run”,且不 dispatch 重复 job
- 输出幂等:agent.message 必须可去重,RunLoop 必须在终态时 no-op
- run 生命周期一致性:DONE/FAILED/CANCELED 的落库要有一致性边界或兜底 finalizer
- 取消语义:写回前必须再次校验 cancel,并确保取消后不再写 agent.message
- Provider 调用稳定性:timeout + retry/backoff + 429/5xx 处理 + job timeout/tries
- SSE 补偿:解决 backlog/subscribe 间隙与 publish 异常导致的漏消息
- Context 预算:可配置窗口/预算并限制大 payload
- 最小测试覆盖:并发幂等、取消、重试/失败路径
D. 建议的下一步路线选择
- 方案1:先补 Orchestrator —— 补齐并发幂等、输出去重、生命周期终态一致性、取消写回保护、provider 超时/重试、SSE 缺口补偿、context 预算配置,再接真实 Provider
- 方案2:直接接真实 Provider —— 目前仅适合“实验/灰度验证链路通不通”;已有 append-only + seq + SSE 基础与 ContextBuilder 过滤,但不满足可靠性门槛
E. 最小变更的 patch 建议(不要求实现)
app/Services/RunDispatcher.php:appendRunStatus后读取返回 message 的payload.run_id;若与新 runId 不一致则直接返回已有 run_id 且跳过 dispatch;必要时把 “单 session 运行中检查” 放入带锁事务app/Services/OutputSink.php: 允许appendAgentMessage接收并透传 dedupe_key(默认run:{runId}:agent:message),避免重复输出app/Services/RunLoop.php: 开始时/写回前检查 run 是否已终态;写回 DONE 前再检查 cancel,必要时写 CANCELED 并停止app/Jobs/AgentRunJob.php: 设置$tries/$backoff/$timeout;在 finally 中若仍 RUNNING 则落 FAILED(需查询最新 run.status)app/Services/Agent/HttpAgentProvider.php+config/services.php: 增加 timeout、retry/backoff 配置项与 429 退避策略app/Services/ChatService.php:publishMessageAppended改为捕获异常仅记录日志,避免 afterCommit 抛错中断主流程app/Http/Controllers/ChatSessionSseController.php: 收到 pubsub 时若 seq gap>1 则sendBacklog回补;可加心跳定期拉取- 新 migration:为
messages增加表达式索引payload->>'run_id'、payload->>'trigger_message_id'(并结合type)以保证查询性能
已实施增强(最小可行版本)
- RunDispatcher 并发幂等:RUNNING 消息以 dedupe_key 作为唯一真相,只有新建 RUNNING 才 dispatch
- RunLoop/OutputSink 幂等:agent.message/run.status 使用 dedupe_key,终态重复执行可安全 no-op
- Cancel/终态收敛:多检查点取消 + Job finally 兜底写入 CANCELED/FAILED
- Provider 可靠性:超时/重试/错误归一化(ProviderException)并写入错误元数据
- SSE 补偿:seq gap 回补 + 心跳 + publish 异常吞吐日志
- Postgres 索引:payload 表达式索引加速 run/cancel 查询