From bdf06232a699a707e091abc714f04c752d5b729f Mon Sep 17 00:00:00 2001 From: cfdaily Date: Thu, 21 May 2026 11:49:35 +0800 Subject: [PATCH] auto-sync: 2026-05-21 11:49:35 --- docs/design/v3.0-router-refactor.md | 391 +++++++++++++--------------- 1 file changed, 176 insertions(+), 215 deletions(-) diff --git a/docs/design/v3.0-router-refactor.md b/docs/design/v3.0-router-refactor.md index c50537a..d470465 100644 --- a/docs/design/v3.0-router-refactor.md +++ b/docs/design/v3.0-router-refactor.md @@ -1,148 +1,161 @@ -# v3.0 Router 重构方案:去掉独立 LLM,改用 Gateway spawn Agent +# v3.0 调度重构方案:去掉独立 LLM,改为广播认领 + 确定性交接 **日期**: 2026-05-21 **状态**: 待司马懿评审 -**影响文件**: `router.py`, `dispatcher.py`, `main.py`, `config/default.yaml` +**影响文件**: `router.py`, `dispatcher.py`, `ticker.py`, `main.py`, `config/default.yaml` --- -## 1. 背景:Spawn 完整链路调查 +## 1. 问题 -### 1.1 完整数据流 +### 1.1 LLMDriver 是设计之外的野路子 +当前 Router 的 `LLMDriver` 用独立 `OpenAI()` 客户端直连 zhipu API 做路由决策: +- 不属于 L1(调了 LLM)、不属于 L2(不是 openclaw agent)、不属于 L3(不是完整 Agent) +- 不走 Gateway(无模型路由、无 fallback、无计费) +- 需要单独维护 api_base/api_key(和 Gateway 配置重复) + +**已导致实际故障**:`general-20260521-0004` 因凭据为空反复调度失败。 + +### 1.2 _dispatch_pending 绕过了认领机制 + +设计文档核心原则(§1.2): +> "编排是 AI Agent 在黑板上**自主领活**(动态协作)" +> "Daemon 是**投递员,不是决策者**" + +但 `_dispatch_pending` 的实际行为是: ``` -Ticker (30s一轮) - → _tick_project(project_id) - ├─ 1. 扫描状态摘要 - ├─ 2. 依赖推进(done 的解锁下游) - ├─ 3. 僵尸/超时检测(_check_timeouts) - ├─ 4. 调度 pending → _dispatch_pending() - ├─ 5. 调度 review → _dispatch_reviews() - ├─ 6. 聚合父 Task 状态 - ├─ 7. 写 daemon_tick 事件 - ├─ 8. 健康检查 - └─ 9. 经验蒸馏 - -_dispatch_pending() - → queries.pending_dispatchable() # 找 pending 且 dependency 都满足的任务 - → dispatcher.dispatch(task) # 每轮最多 3 个 - ├─ Router.route(task) # ← 要改的:独立 LLM 调用 - │ → 返回 agent_id - ├─ Counter.can_acquire(agent_id) # 并发检查(全局上限5 + per-agent上限1-3) - ├─ Counter.acquire(agent_id) # 占用名额 - ├─ Spawner.spawn_full_agent() - │ ├─ cmd = ["openclaw", "agent", "--agent", agent_id, - │ │ "--session-id", uuid, "--message", prompt, "--json"] - │ ├─ asyncio.create_subprocess_exec(*cmd) # 异步非阻塞 - │ └─ asyncio.create_task(_monitor_process) # 后台监控 - └─ 黑板写 claimed + current_agent - -_monitor_process() # 异步等待,不阻塞 ticker - → await asyncio.wait_for(proc.wait(), timeout=600s) - → 超时 → proc.kill() - → _record_attempt(task_id, outcome) # 写 task_attempts 表 - → on_complete(agent_id, outcome) # → Counter.release() - -_dispatch_reviews() # review 状态的调度 - → 检查有没有 review 记录(防重复) - → 检查有没有产出(无产出直接标 failed) - → spawn 司马懿做审查 +pending → Router 决定 agent → 强制 claimed + spawn → Agent 被动执行 ``` -### 1.2 涉及的模块 +代码里认领的全部基础设施(API + CAS + inbox 事件)都已经实现,但被跳过了。 -| 模块 | 文件 | 职责 | -|------|------|------| -| **Ticker** | `daemon/ticker.py` | 30s 轮询,驱动整个调度循环 | -| **Dispatcher** | `daemon/dispatcher.py` | 决策 + spawn 执行,协调 Router/Counter/Spawner | -| **Router** | `daemon/router.py` | 路由决策:返回 agent_id + mode | -| **Spawner** | `daemon/spawner.py` | 实际 spawn 进程 + 监控 + 记录 | -| **Counter** | `daemon/counter.py` | 并发控制:全局 Semaphore + per-agent Semaphore | +### 1.3 设计违背 -### 1.3 设计文档中的三层执行模型(architecture-v2.6.md) +architecture-v2.6.md §1.1: +> "Agent 决策,Daemon 执行" — 庞统做 plan、张飞领任务、关羽发现风险,都写在黑板上。 -| 层级 | 方式 | 命令 | 成本 | 适用场景 | -|------|------|------|------|---------| -| **L1 Daemon 直接操作** | SQLite/文件 | — | 几乎为零 | 状态更新、机械验证 | -| **L2 spawn sub** | 隔离 session | `openclaw agent --agent --session-id ` | 轻量 | scope guard、格式校验 | -| **L3 run agent** | 完整黑板参与者 | `openclaw agent --agent ` | 完整 | 编码、审查、策略 | +Daemon 替 Agent 做决策(Router 决定分给谁),违反了"Agent 决策"原则。 + +--- + +## 2. 设计文档中已有的完整方案 + +### 2.1 认领基础设施(已实现) + +| 组件 | 代码位置 | 说明 | +|------|----------|------| +| **claim API** | `POST /tasks/{id}/claim` | 原子 CAS 认领 | +| **claim_task()** | `operations.py:155` | `WHERE status='pending' AND (assignee IS NULL OR assignee=?)` | +| **inbox agent_claim** | `inbox.py:264` | Agent 通过 JSONL 通知 Daemon 认领 | +| **claim_timeout** | `ticker.py:530` | claimed 超时 5 分钟 → 重置为 pending(续杯) | + +### 2.2 竞态解决(已设计,§3.6) + +1. **默认:先到先得** — SQLite CAS,谁先 claim 谁做 +2. **升级:庞统仲裁** — 争议时 @庞统 请求仲裁 +3. **最终:用户拍板** — @user 请求用户决定 + +### 2.3 续杯兜底(已实现) + +``` +pending → 广播 → 无人认领 → claim_timeout(5min) → pending → 再广播 → ... → 庞统兜底 +``` + +`claim_timeout_minutes = 5.0`,`_check_timeouts` 自动把超时的 claimed 重置为 pending。 + +### 2.4 三层执行模型(§4.2) + +| 层级 | 方式 | 命令 | 适用场景 | +|------|------|------|---------| +| **L1 Daemon 直接操作** | SQLite/文件 | — | 状态更新、机械验证 | +| **L2 spawn sub** | 隔离 session | `openclaw agent --agent --session-id ` | scope guard、格式校验 | +| **L3 run agent** | 完整黑板参与者 | `openclaw agent --agent ` | 编码、审查、策略 | > **核心原则:系统只有两种 LLM 调用方式,都通过 Gateway,没有第三种。** --- -## 2. 问题 - -### 2.1 LLMDriver 是设计之外的野路子 - -当前 Router(`LLMDriver` 类)用独立的 `OpenAI()` 客户端直接调 zhipu API 做路由决策: -- 不属于 L1(调了 LLM)、不属于 L2(不是 openclaw agent)、不属于 L3(不是完整 Agent) -- 不走 Gateway(无模型路由、无 fallback、无计费) -- 需要单独维护 api_base/api_key(和 Gateway 配置重复) -- 凭据为空时静默 fallback 到庞统,不报错 - -### 2.2 实际故障 - -`general-20260521-0004` 任务反复调度失败: -``` -Router.route() → LLMDriver._get_client() → OpenAI(**kwargs) -→ OpenAIError: Missing credentials -→ fallback to pangtong-fujunshi (但 Router 不 spawn,只返回决策) -→ Dispatcher 拿到 agent_id="pangtong-fujunshi" → spawn 庞统 -→ 但庞统的 prompt 是"执行任务"而非"分配任务" -``` -而且 `config/default.yaml` 的 `routing.api_base/api_key` 都是空字符串,注释说"空=用 OpenClaw 默认"——但 OpenAI 库不会自动读 OpenClaw 配置。 - -### 2.3 设计违背 - -architecture-v2.6.md §1.1 核心设计原则: -> "Agent 决策,Daemon 执行" — 庞统做 plan、张飞领任务、关羽发现风险,都写在黑板上。Daemon 读黑板,执行 spawn/通知。 - -Router 用独立 LLM 做决策,违反了"Agent 决策"原则——决策应该由 Agent(通过 Gateway)来做,不是 Daemon 自己调 LLM。 - ---- - -## 3. 方案:Router 改为"能力匹配 + spawn 庞统兜底" +## 3. 方案:广播认领 + 确定性交接 ### 3.1 核心思路 -Router 有两种路由方式: -- **确定性路由**(能力匹配、retry、handoff)→ 保留,纯 L1 逻辑,不调 LLM -- **模糊路由**(首次分配、不确定场景)→ **不再调独立 LLM,改为 spawn 庞统让庞统决定** +任务调度分两条路径: -### 3.2 路由决策流程(改后) +**确定性路径**:已明确知道下一步该谁做 → 直接 spawn,不需要广播 +- retry → 原执行者 +- Agent handoff(next_capability)→ 能力匹配 +- 有 assignee → 直接分 +- review 生命周期 → 能力匹配 + +**广播认领路径**:首次分配 / 不确定场景 → spawn 所有空闲 Agent,自主 claim +- Agent 读黑板 → 自己判断是否适合 → claim +- 无人认领 → 续杯 → 庞统兜底 + +### 3.2 完整调度流程 ``` -任务进入 Router.route() +Ticker._tick_project() │ - ├─ 快速路径1: 本地 action → daemon - ├─ 快速路径2: retry → 原执行者 - ├─ Mode B: Agent handoff (next_capability) → 能力匹配 - ├─ 快速路径3: 生命周期流转 → 能力匹配 - ├─ 快速路径4: 有 assignee → 直接分 + ├─ 1. 扫描状态 + ├─ 2. 依赖推进 + ├─ 3. 超时检测(claimed 5min → pending,working 30min → failed) │ - └─ 模糊场景(以上都不匹配) - │ - → 返回 RouteDecision(agent_id="pangtong-fujunshi", mode="delegate") - Dispatcher 拿到 mode="delegate" - → 构建 delegate prompt("请分配此任务") - → Spawner.spawn_full_agent(pangtong-fujunshi, delegate_prompt) - → 庞统读黑板任务信息,自己决定分配给谁 - → 庞统通过 API 回写 assignee → ticker 下一轮 spawn 实际执行者 + ├─ 4. 调度 pending 任务:_dispatch_pending() + │ for each pending task: + │ ├─ 有 next_capability?→ 能力匹配 → 直接 spawn 对应 Agent + │ ├─ 有 assignee?→ 直接 spawn 该 Agent + │ ├─ retry?→ spawn 原执行者 + │ └─ 都没有?→ _broadcast_claim() + │ → spawn 每个空闲 Agent,传入"黑板上有新任务,请认领"的 prompt + │ → Agent 读黑板(GET /tasks?status=pending) + │ → Agent 判断是否适合自己 + │ → Agent claim(POST /tasks/{id}/claim)或退出 + │ + ├─ 5. 调度 review 任务:_dispatch_reviews()(不变) + └─ 6-10. 其他处理(不变) ``` -### 3.3 关键区别:改前 vs 改后 +### 3.3 广播认领的 Spawn Prompt -| 阶段 | 改前(独立 LLM) | 改后(spawn 庞统) | -|------|-----------------|-------------------| -| **决策者** | `LLMDriver`(OpenAI 客户端) | 庞统 Agent(通过 Gateway) | -| **调用方式** | `OpenAI().chat.completions.create()` | `openclaw agent --agent pangtong-fujunshi` | -| **走的路径** | 直连 zhipu API | Gateway → 模型路由 | -| **凭据** | config/default.yaml 手动配 | Gateway 统一管理 | -| **上下文** | 300 token prompt(标题+描述+能力列表) | 完整黑板上下文(SOUL+AGENTS+任务详情) | -| **速度** | 1-2s | 30-60s | -| **准确性** | 低(信息少,模型弱) | 高(完整上下文,模型强) | -| **失败处理** | 静默 fallback,不报错 | 正常 Agent 失败流程(retry/escalate) | +广播 spawn 的 Agent 收到的 prompt: + +``` +你是 {agent_id}。黑板上有新的待认领任务。 + +## 操作 +1. 读黑板查看待认领任务: + curl http://{api_host}:{api_port}/api/projects/{project_id}/tasks?status=pending + +2. 分析每个 pending 任务,判断是否适合你: + - 你的能力:{capabilities} + - 任务类型、描述、优先级是否匹配你的专长 + +3. 如果有适合你的任务,立即认领: + curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/claim \ + -H 'Content-Type: application/json' \ + -d '{"agent": "{agent_id}"}' + +4. 认领成功后,开始执行(状态改为 working) + +5. 如果没有适合你的任务,直接退出 +``` + +### 3.4 Claim 竞争处理 + +多个 Agent 同时 claim 同一个任务: +- **SQLite CAS**:`WHERE status='pending'` — 只有第一个成功,其余 rowcount=0 +- **Agent 行为**:claim 失败 → 检查其他 pending 任务 → 没有适合的 → 退出 +- **自然负载均衡**:不同 Agent 倾向认领不同类型的任务(张飞→coding,司马懿→review) + +### 3.5 无人认领兜底 + +``` +广播 → 5 分钟内无人 claim → claim_timeout → pending → 下轮 ticker 再广播 +→ 连续 3 轮无人认领 → spawn 庞统 → 庞统决定分配或自己执行 +``` + +连续无人认领的检测:在 `events` 表中记录 `broadcast_sent` 事件,`_check_timeouts` 中统计广播轮次,超过阈值 escalate to 庞统。 --- @@ -150,118 +163,65 @@ Router 有两种路由方式: ### 4.1 删除 `LLMDriver` 类(router.py) -删除整个 `LLMDriver` 类(约 120 行),包括 `_get_client()`、`route()`、`_build_prompt()`。 - -Router 的 `route()` 方法末尾改为: - -```python -# 当前(Mode A: 独立 LLM 调用) -if self.llm_driver: - decision = self.llm_driver.route(...) - ... - -# 改后(委托庞统) -return RouteDecision( - agent_id=self.FALLBACK_AGENT, # "pangtong-fujunshi" - reason="Uncertain routing, delegate to coordinator", - mode="delegate", - confidence=0.0, -) -``` +删除整个 `LLMDriver` 类(~120 行)。`AgentRouter.route()` 末尾改为返回 delegate。 ### 4.2 `AgentRouter.__init__` 去掉 `llm_driver` 参数 -```python -# 改前 -def __init__(self, agent_profiles, llm_driver=None, counter=None): +### 4.3 新增 `_broadcast_claim`(ticker.py) -# 改后 -def __init__(self, agent_profiles, counter=None): -``` - -### 4.3 Dispatcher 增加 `delegate` 模式 - -`dispatcher.py` 的 `_build_spawn_message()` 中,为 `delegate` 模式生成专门的 prompt: +在 `_dispatch_pending` 中,确定性路径都不匹配时,调用新方法广播认领: ```python -if mode == "delegate": - return f"""你是任务协调员。请分析以下任务,决定最合适的执行者。 - -## 任务信息 -- 项目: {project_id} -- 任务ID: {task_id} -- 标题: {title} -- 描述: {description} -- 类型: {task_type} - -## 团队 -- 张飞(zhangfei-dev): 编码、实现、脚本 -- 司马懿(simayi-challenger): 审查、质量检查、辩论 -- 关羽(guanyu-dev): 风控、合规 -- 赵云(zhaoyun-data): 数据获取、清洗、验证 -- 姜维(jiangwei-infra): 部署、基础设施、Docker、vnpy -- 庞统(pangtong-fujunshi): 规划、协调、策略 - -## 操作 -1. 分析任务需求 -2. 选择最合适的 Agent -3. 通过 API 回写分配结果: - curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/status \\ - -H 'Content-Type: application/json' \\ - -d '{{"status": "claimed", "agent": ""}}' -4. 如果你自己能做,直接认领并执行 -""" +async def _broadcast_claim(self, task, db_path, project_id): + """广播任务给所有空闲 Agent,自主认领""" + idle_agents = self._get_idle_agents() + for agent_id in idle_agents: + # 并发检查 + if not await self.counter.can_acquire(agent_id): + continue + prompt = self._build_claim_prompt(agent_id, task, project_id) + await self.spawner.spawn_full_agent( + agent_id=agent_id, + message=prompt, + task_id=task.id, + on_complete=lambda aid, _: self.counter.release(aid), + ) ``` -注意:其余 spawn 链路(Counter.acquire → Spawner.spawn_full_agent → _monitor_process → Counter.release)完全复用,不需要改。 +### 4.4 Dispatcher 增加 delegate 模式(dispatcher.py) -### 4.4 main.py 去掉 LLMDriver 初始化 +`_build_spawn_message` 增加 `delegate` 分支(庞统兜底 prompt),和广播认领共存: +- 广播认领失败 → 任务回到 pending → 多轮后 → spawn 庞统 delegate -删除 `routing_config` / `llm_driver` 的初始化块(约 10 行)。Router 构造不再传 `llm_driver`: +### 4.5 main.py 去掉 LLMDriver 初始化 -```python -router = AgentRouter( - agent_profiles=agent_profiles, - counter=counter, -) -``` +### 4.6 config/default.yaml 去掉 routing 节 -### 4.5 config/default.yaml 去掉 routing 节 +### 4.7 广播轮次追踪 -删除 `daemon.routing` 配置节: -```yaml -# 删掉: -routing: - model: "glm-4-flash" - api_base: "..." - api_key: "..." - confidence_threshold: 0.7 - timeout: 5.0 - max_tokens: 200 - temperature: 0.1 -``` - -确定性路由的能力匹配不依赖配置,模糊路由由庞统决策不需要配置。 +`_broadcast_claim` 写入 events 表 `broadcast_sent` 事件,`_check_timeouts` 检测连续广播轮次。 --- -## 5. 改动前后场景对比 +## 5. 场景对比 -| 场景 | 改前 | 改后 | 变化 | -|------|------|------|------| -| retry | 原执行者(确定性)| 原执行者(确定性)| 不变 | -| Agent handoff | 能力匹配(确定性)| 能力匹配(确定性)| 不变 | -| 生命周期 review | 能力匹配(确定性)| 能力匹配(确定性)| 不变 | -| 有 assignee | 直接分(确定性)| 直接分(确定性)| 不变 | -| **首次分配/模糊** | **独立 LLM 调用** | **spawn 庞统决策** | **改** | +| 场景 | 改前(独立 LLM 分配) | 改后(广播认领 + 确定性交接) | +|------|---------------------|---------------------------| +| retry | 原执行者 | 不变(确定性) | +| Agent handoff | 能力匹配 | 不变(确定性) | +| 有 assignee | 直接分 | 不变(确定性) | +| review 生命周期 | 能力匹配 | 不变(确定性) | +| **首次分配** | **独立 LLM 决定分给谁** | **广播所有空闲 Agent,自主认领** | +| **无人认领** | **无此场景(强制分配)** | **续杯 → 庞统兜底** | --- ## 6. 代码量 -- **删**:~130 行(`LLMDriver` 类 + routing config 初始化 + config.yaml routing 节) -- **改**:~30 行(`Router.route()` 末尾 + `Dispatcher._build_spawn_message()` 增加 delegate 分支) -- **净减**:~100 行 +- **删**:~130 行(`LLMDriver` + routing config 初始化 + config.yaml routing 节) +- **改**:~30 行(`Router.route()` 末尾 + `Dispatcher._build_spawn_message()` delegate 分支) +- **新增**:~60 行(`_broadcast_claim` + `_build_claim_prompt` + 广播轮次追踪) +- **净减**:~40 行 --- @@ -269,18 +229,19 @@ routing: | # | 风险 | 评估 | 缓解 | |---|------|------|------| -| 1 | 庞统成为单点 | 中 | 庞统 max_concurrent=3,delegate 是轻量决策(30s 内完成),不是重活 | -| 2 | 速度变慢(1-2s → 30-60s) | 低 | 首次分配不急,准确比快重要。且大部分场景走确定性路由,不触发 delegate | -| 3 | 庞统繁忙时 delegate 被跳过 | 中 | Counter 返回 skipped,下轮 ticker 重试。庞统 3 并发足够 | -| 4 | delegate prompt 不够精确 | 低 | 庞统有完整上下文(SOUL+AGENTS+团队信息),比 300 token 的 LLM prompt 强得多 | +| 1 | 广播 spawn 消耗资源(每个 pending 任务都 spawn 所有空闲 Agent) | 中 | 只有"无确定性路径"的任务才广播;且 Agent 读黑板后无适合任务会快速退出 | +| 2 | 多 Agent 竞争 claim | 低 | SQLite CAS 先到先得,已实现 | +| 3 | 无人认领 | 低 | 续杯机制兜底,多轮后庞统接管 | +| 4 | Agent 认领了不适合的任务 | 低 | Agent 有完整上下文(SOUL+AGENTS+能力),比 LLM 判断更准确 | +| 5 | 广播速度比直接分配慢 | 低 | 首次分配不需要快,准确比快重要 | --- ## 8. 实施步骤 -1. `router.py`:删除 `LLMDriver` 类 + `AgentRouter` 去掉 `llm_driver` 参数 + `route()` 末尾改 delegate -2. `dispatcher.py`:`_build_spawn_message()` 增加 `delegate` 分支 -3. `main.py`:删除 `llm_driver` 初始化块 -4. `config/default.yaml`:删除 `routing` 节 -5. 发司马懿评审 -6. 评审通过后部署 +1. `router.py`:删除 `LLMDriver` 类 + `AgentRouter` 去掉 `llm_driver` + `route()` 末尾改 delegate +2. `ticker.py`:新增 `_broadcast_claim` + `_build_claim_prompt`,修改 `_dispatch_pending` 增加 广播路径 +3. `dispatcher.py`:`_build_spawn_message()` 增加 delegate 分支(庞统兜底) +4. `main.py`:删除 `llm_driver` 初始化块 +5. `config/default.yaml`:删除 `routing` 节 +6. 测试:创建 pending 任务 → 观察广播 spawn → Agent claim → 执行