Files
ars-backend/docs/agent-orchestrator-review.md
Roog 6d934f4e34 main: 增强 Agent Run 调度可靠性与幂等性
- 默认切换 AgentProvider 为 HttpAgentProvider,增强网络请求的容错和重试机制
- 优化 Run 逻辑,支持多场景去重与并发保护
- 添加 Redis 发布失败的日志记录以提升问题排查效率
- 扩展 OpenAPI 规范,新增 Error 和 Run 状态相关模型
- 增强测试覆盖,验证调度策略和重复请求的幂等性
- 增加数据库索引以优化查询性能
- 更新所有相关文档和配置文件
2025-12-18 17:41:42 +08:00

54 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 查询