Files
sanguo_moziplus_v2/docs/design/v3.0-router-refactor.md
T
2026-05-21 11:12:09 +08:00

287 lines
11 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.
# 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=3delegate 是轻量决策(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. 评审通过后部署