287 lines
11 KiB
Markdown
287 lines
11 KiB
Markdown
# v3.0 Router 重构方案:去掉独立 LLM,改用 Gateway spawn Agent
|
||
|
||
**日期**: 2026-05-21
|
||
**状态**: 待司马懿评审
|
||
**影响文件**: `router.py`, `dispatcher.py`, `main.py`, `config/default.yaml`
|
||
|
||
---
|
||
|
||
## 1. 背景:Spawn 完整链路调查
|
||
|
||
### 1.1 完整数据流
|
||
|
||
```
|
||
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 司马懿做审查
|
||
```
|
||
|
||
### 1.2 涉及的模块
|
||
|
||
| 模块 | 文件 | 职责 |
|
||
|------|------|------|
|
||
| **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 设计文档中的三层执行模型(architecture-v2.6.md)
|
||
|
||
| 层级 | 方式 | 命令 | 成本 | 适用场景 |
|
||
|------|------|------|------|---------|
|
||
| **L1 Daemon 直接操作** | SQLite/文件 | — | 几乎为零 | 状态更新、机械验证 |
|
||
| **L2 spawn sub** | 隔离 session | `openclaw agent --agent <id> --session-id <uuid>` | 轻量 | scope guard、格式校验 |
|
||
| **L3 run agent** | 完整黑板参与者 | `openclaw agent --agent <id>` | 完整 | 编码、审查、策略 |
|
||
|
||
> **核心原则:系统只有两种 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.1 核心思路
|
||
|
||
Router 有两种路由方式:
|
||
- **确定性路由**(能力匹配、retry、handoff)→ 保留,纯 L1 逻辑,不调 LLM
|
||
- **模糊路由**(首次分配、不确定场景)→ **不再调独立 LLM,改为 spawn 庞统让庞统决定**
|
||
|
||
### 3.2 路由决策流程(改后)
|
||
|
||
```
|
||
任务进入 Router.route()
|
||
│
|
||
├─ 快速路径1: 本地 action → daemon
|
||
├─ 快速路径2: retry → 原执行者
|
||
├─ Mode B: Agent handoff (next_capability) → 能力匹配
|
||
├─ 快速路径3: 生命周期流转 → 能力匹配
|
||
├─ 快速路径4: 有 assignee → 直接分
|
||
│
|
||
└─ 模糊场景(以上都不匹配)
|
||
│
|
||
→ 返回 RouteDecision(agent_id="pangtong-fujunshi", mode="delegate")
|
||
Dispatcher 拿到 mode="delegate"
|
||
→ 构建 delegate prompt("请分配此任务")
|
||
→ Spawner.spawn_full_agent(pangtong-fujunshi, delegate_prompt)
|
||
→ 庞统读黑板任务信息,自己决定分配给谁
|
||
→ 庞统通过 API 回写 assignee → ticker 下一轮 spawn 实际执行者
|
||
```
|
||
|
||
### 3.3 关键区别:改前 vs 改后
|
||
|
||
| 阶段 | 改前(独立 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) |
|
||
|
||
---
|
||
|
||
## 4. 改动清单
|
||
|
||
### 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,
|
||
)
|
||
```
|
||
|
||
### 4.2 `AgentRouter.__init__` 去掉 `llm_driver` 参数
|
||
|
||
```python
|
||
# 改前
|
||
def __init__(self, agent_profiles, llm_driver=None, counter=None):
|
||
|
||
# 改后
|
||
def __init__(self, agent_profiles, counter=None):
|
||
```
|
||
|
||
### 4.3 Dispatcher 增加 `delegate` 模式
|
||
|
||
`dispatcher.py` 的 `_build_spawn_message()` 中,为 `delegate` 模式生成专门的 prompt:
|
||
|
||
```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": "<agent_id>"}}'
|
||
4. 如果你自己能做,直接认领并执行
|
||
"""
|
||
```
|
||
|
||
注意:其余 spawn 链路(Counter.acquire → Spawner.spawn_full_agent → _monitor_process → Counter.release)完全复用,不需要改。
|
||
|
||
### 4.4 main.py 去掉 LLMDriver 初始化
|
||
|
||
删除 `routing_config` / `llm_driver` 的初始化块(约 10 行)。Router 构造不再传 `llm_driver`:
|
||
|
||
```python
|
||
router = AgentRouter(
|
||
agent_profiles=agent_profiles,
|
||
counter=counter,
|
||
)
|
||
```
|
||
|
||
### 4.5 config/default.yaml 去掉 routing 节
|
||
|
||
删除 `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
|
||
```
|
||
|
||
确定性路由的能力匹配不依赖配置,模糊路由由庞统决策不需要配置。
|
||
|
||
---
|
||
|
||
## 5. 改动前后场景对比
|
||
|
||
| 场景 | 改前 | 改后 | 变化 |
|
||
|------|------|------|------|
|
||
| retry | 原执行者(确定性)| 原执行者(确定性)| 不变 |
|
||
| Agent handoff | 能力匹配(确定性)| 能力匹配(确定性)| 不变 |
|
||
| 生命周期 review | 能力匹配(确定性)| 能力匹配(确定性)| 不变 |
|
||
| 有 assignee | 直接分(确定性)| 直接分(确定性)| 不变 |
|
||
| **首次分配/模糊** | **独立 LLM 调用** | **spawn 庞统决策** | **改** |
|
||
|
||
---
|
||
|
||
## 6. 代码量
|
||
|
||
- **删**:~130 行(`LLMDriver` 类 + routing config 初始化 + config.yaml routing 节)
|
||
- **改**:~30 行(`Router.route()` 末尾 + `Dispatcher._build_spawn_message()` 增加 delegate 分支)
|
||
- **净减**:~100 行
|
||
|
||
---
|
||
|
||
## 7. 风险与缓解
|
||
|
||
| # | 风险 | 评估 | 缓解 |
|
||
|---|------|------|------|
|
||
| 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 强得多 |
|
||
|
||
---
|
||
|
||
## 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. 评审通过后部署
|