360 lines
15 KiB
Markdown
360 lines
15 KiB
Markdown
# 多智能体圆桌研讨 · 后端开发文档
|
||
|
||
本文档面向需要维护或扩展"多智能体圆桌研讨"功能后端的开发者。前端 API 速查请看 `multi-agent-api.md`,此文档关注**实现细节、设计取舍、性能特征**。
|
||
|
||
---
|
||
|
||
## 1. 功能定位
|
||
|
||
把"任务目标 + 用户约束"丢给一个**协调智能体**,由它通过 `agent_orchestration` 技能调度 6 个固定职能的子智能体(情报收集 / 环境评估 / 方案设计 / 风险审查 / 执行规划 / 总结陈述)协同产出方案,直到协调智能体认为信息足够、停止派活并输出最终结论为止。
|
||
|
||
对应前端页面:`/page/roundtable/planning` 的 **Step 2 多智能体研讨**。
|
||
|
||
---
|
||
|
||
## 2. 涉及的后端文件
|
||
|
||
```
|
||
offline-backend-20260512/backend/
|
||
├── app/gateway/
|
||
│ ├── app.py # 注册 multi_agent.router
|
||
│ └── routers/
|
||
│ └── multi_agent.py # 本功能唯一新增的 router
|
||
└── .deer-flow/agents/
|
||
├── roundtable-intelligence/ # 6 个内置子智能体目录
|
||
│ ├── config.yaml # ├── id / name / description
|
||
│ └── SOUL.md # └── 角色 prompt
|
||
├── roundtable-environment/
|
||
├── roundtable-solution-design/
|
||
├── roundtable-risk-review/
|
||
├── roundtable-execution-plan/
|
||
└── roundtable-summary/
|
||
```
|
||
|
||
**零代码改动构造内置 agent**:6 个目录通过既有的 `_sync_legacy_agents()`(`app/gateway/routers/agents.py`)在下次任意 `/api/agents` 请求时自动 upsert 进 `agents` 表,`user_id IS NULL` 标记为内置。
|
||
|
||
---
|
||
|
||
## 3. 接口总览
|
||
|
||
路由前缀 `/api/multi-agent`,所有接口需要 `Authorization: Bearer <JWT>`。
|
||
|
||
| 方法 | 路径 | 用途 |
|
||
|---|---|---|
|
||
| POST | `/api/multi-agent/init` | 一次性创建协调智能体 + 6 个子席位线程,返回 `thread_ids` 映射 |
|
||
| POST | `/api/multi-agent/run/stream` | SSE 流式驱动一次 leader 或 special 调用 |
|
||
|
||
字段细节见 `multi-agent-api.md`,这里不重复。
|
||
|
||
---
|
||
|
||
## 4. 内部实现详解
|
||
|
||
### 4.1 `/init` 内部流程
|
||
|
||
```
|
||
[阶段1] 并行准备 6 个子智能体席位 (asyncio.gather)
|
||
├─ GET /api/agents/{id} × 6 → 取 name + description
|
||
└─ POST /api/threads × 6 → 建新 thread
|
||
|
||
[阶段2] 创建协调智能体
|
||
├─ POST /api/threads → 协调智能体的 thread
|
||
└─ POST /api/agents (with model + skills=[agent_orchestration] + soul)
|
||
└─ soul 里包含可调度的 6 个 agent_name + description 列表
|
||
```
|
||
|
||
**协调智能体 SOUL 是动态拼出来的**(`multi_agent.py:216-224`):
|
||
|
||
```python
|
||
soul = (
|
||
"#核心定位:\n"
|
||
"用途:根据用户的任务目标,调用其他智能体干活,只负责协调汇总,...\n"
|
||
"#调度方法:\n"
|
||
"通过使用agent_orchestration技能调用其他智能体干活,...\n"
|
||
"#可调度智能体:\n"
|
||
f"{descriptions}" # ← 6 个子席位的 name+description
|
||
)
|
||
```
|
||
|
||
由于 `main_agent_name` 由前端每次新建(`roundtable-coordinator-{timestamp36}`),所以协调智能体本身是一次性的、用完即弃。
|
||
|
||
### 4.2 `/run/stream` 内部流程
|
||
|
||
接收 `agent_type` 后走两条分支:
|
||
|
||
#### `agent_type = "leader"` (协调智能体)
|
||
|
||
```
|
||
1. broadcast「总控收到用户消息」到所有非自己线程 (asyncio.gather)
|
||
2. 拼 RunCreateRequest,转发到 POST /api/threads/{coordinator}/runs/stream
|
||
- excluded_tools: ["web_search"]
|
||
- context.model_name = 用户传的 model
|
||
3. 流式透传 LangGraph 事件给前端
|
||
4. 流结束后从最后一帧 values 取最末一条 message
|
||
5. 解析 message.tool_calls (agent_orchestration 技能调用):
|
||
- 用 _extract_name_task() 提取 [agent_name, task_text]
|
||
- 对每个 [agent_name, task]:
|
||
broadcast「总控给 X 派活,内容为: ...」到所有非派活双方
|
||
6. 末尾追加状态帧:
|
||
{
|
||
"status": [["agent_name", "task"], ...], // 空数组 = 共识达成
|
||
"content": "<leader 最终文本>",
|
||
"agent_name": "<coordinator>"
|
||
}
|
||
```
|
||
|
||
#### `agent_type = "special"` (子智能体)
|
||
|
||
```
|
||
1. 拼 RunCreateRequest,转发到 POST /api/threads/{sub_agent}/runs/stream
|
||
- excluded_tools: ["web_search","ask_clarification","present_files","view_image"]
|
||
2. 流式透传 LangGraph 事件
|
||
3. 流结束后从最后一帧 values 取交付内容 (content)
|
||
4. broadcast「子智能体 X 完成 Y 工作,交付内容为: ...」到所有非自己线程
|
||
5. 末尾追加状态帧:
|
||
{
|
||
"status": "子智能体 X 完成 Y 工作",
|
||
"content": "<交付内容>",
|
||
"agent_name": "<sub agent>"
|
||
}
|
||
```
|
||
|
||
### 4.3 _broadcast 机制
|
||
|
||
```python
|
||
async def _broadcast(client, base, headers, message, agent_threads, skip):
|
||
targets = [tid for name, tid in agent_threads.items() if name not in skip]
|
||
await asyncio.gather(*(_append_thread_message(...) for tid in targets))
|
||
```
|
||
|
||
`_append_thread_message` 对每个目标 thread:
|
||
|
||
```
|
||
GET /api/threads/{id}/state # 拿当前 messages 列表
|
||
state.values.messages.append({"type":"human","content":...})
|
||
POST /api/threads/{id}/state # 写回完整 state
|
||
```
|
||
|
||
这是**为了让每个子线程的对话历史里都能看到全局上下文**(其他席位说了什么、协调智能体怎么派活的)。子智能体下次被调用时,模型自然能基于这份"群消息回放"做判断。
|
||
|
||
**关键不变量**:`skip` 列表通常包含发起方和接收方,避免发起方收到回声、接收方收到重复消息。
|
||
|
||
---
|
||
|
||
## 5. 6 个内置 agent SOUL 设计要点
|
||
|
||
每个 `roundtable-*` 的 SOUL.md 遵循统一结构:
|
||
|
||
```
|
||
# {席位名}
|
||
|
||
## 角色定位
|
||
你是"多智能体圆桌研讨"会议的 **{席位}** 席位。会议由"总控协调智能体"统一调度,
|
||
你只在被派到任务时发言,不需要主动推动议程。
|
||
|
||
## 核心职责
|
||
- ...具体职责...
|
||
|
||
## 输出风格
|
||
- 每次发言以 {emoji} 开头
|
||
- 第一句直接给结论
|
||
- 不超过 4 句
|
||
|
||
## 边界
|
||
- 只产出 {本席位输出}
|
||
- 不替其他席位做 {其他席位输出}
|
||
- 任务结束后停下,等待总控派下一个任务
|
||
|
||
## 示例输出
|
||
> ...
|
||
```
|
||
|
||
| 席位 | agent_id | emoji | 核心职责 |
|
||
|---|---|---|---|
|
||
| 情报收集 | `roundtable-intelligence` | 📢 | 同类案例 + 历史风险预警 |
|
||
| 环境评估 | `roundtable-environment` | 🌍 | 硬件容量 + 量化承载力 |
|
||
| 方案设计 | `roundtable-solution-design` | 📐 | 主备方案 + 关键路径 |
|
||
| 风险审查 | `roundtable-risk-review` | ⚠️ | 合规红线 + 收敛建议 |
|
||
| 执行规划 | `roundtable-execution-plan` | 📅 | 里程碑 + 资源排布 |
|
||
| 总结陈述 | `roundtable-summary` | 🎤 | 归纳 + 冲突标记 + 盖印 |
|
||
|
||
**为什么不让子智能体自己调度**:所有 SOUL 都明文写了"只在被点到时发言,由总控协调智能体派活"。子智能体不装 `agent_orchestration` 技能。只有协调智能体装。这是单调度源,避免多智能体相互抢调度权造成的死循环。
|
||
|
||
---
|
||
|
||
## 6. 关键设计决策
|
||
|
||
### 6.1 为什么用 HTTP loopback 而不是内部函数调用
|
||
|
||
`multi_agent.py` 调用 `/api/threads`、`/api/agents`、`/api/threads/.../runs/stream` 都是用 `httpx` 走 `http://localhost:8001/...` 自身循环回来,而不是直接 `from app.gateway.services import ...`。
|
||
|
||
**好处**:
|
||
- 自动复用 Gateway 的鉴权 + CSRF + permission 中间件,不用再手写
|
||
- 复用既有路由的请求验证(Pydantic)和错误响应
|
||
- `Authorization` 头透传,被调端自然知道是哪个用户在调
|
||
|
||
**代价**: 每次调用都有一次额外的 TCP+JWT 解析开销(~10-50ms)。但因为 init 已经做了并行优化(A)、broadcast 已经省掉(C),目前总体耗时是可接受的。
|
||
|
||
如未来需要进一步优化,可改为直接调用 `app/gateway/services.py` 里的内部函数,绕过 HTTP 一层。
|
||
|
||
### 6.2 为什么协调智能体每次新建
|
||
|
||
前端在每次进入 step 2 时生成 `roundtable-coordinator-{Date.now().toString(36)}`,作为 `main_agent_name`。
|
||
|
||
**好处**:
|
||
- 不同会话/不同用户/同一用户不同任务之间完全隔离
|
||
- 协调智能体的对话历史就是本次研讨的全部上下文,不会被上次任务污染
|
||
- SOUL 不会被"上次的可调度列表"覆盖(虽然现在 6 个席位是固定的)
|
||
|
||
**代价**: 每次都要在 `agents` 表插一行,`.deer-flow/agents/<id>/` 下生成新目录。长期会累积大量协调智能体目录,**未来需要清理脚本**(标记 description 以 "你是总控控制协调智能体" 开头的 agent 自动 GC)。
|
||
|
||
### 6.3 子智能体席位为什么固定 6 个
|
||
|
||
- 圆桌的 6 个角色定位是产品级强约束(情报/环境/方案/风险/执行/总结)
|
||
- 写在后端 SOUL.md 一处维护,跟其他内置 agent 同等地位
|
||
- 前端只需要把 6 个 hyphen-case 名作为 `agent_names` 传过来即可
|
||
|
||
如果后续要支持"用户自选席位",在前端加 mapping 面板让用户从 `GET /api/agents` 里挑,不动后端。
|
||
|
||
### 6.4 为什么 SSE 末尾还要追加一个 `{status:...}` 帧
|
||
|
||
LangGraph 原生 SSE 里没有"调度结果"这种语义,前端只能从最后一帧 values 里去翻 tool_calls。如果由前端解析,前后端协议会绑死 LangGraph 内部数据形状,迁移成本高。所以后端在流末尾追加一个**协议层的总结帧**:
|
||
|
||
```json
|
||
{
|
||
"status": [["agent_name", "task"], ...] | "<完成提示>",
|
||
"content": "<最终 AI 文本>",
|
||
"agent_name": "<本轮 agent>"
|
||
}
|
||
```
|
||
|
||
前端 `streamMultiAgent` helper 通过 `isFinalStatusFrame()` 识别该帧(必须同时有 `status` + `content` 键),与其他 LangGraph 帧区分。
|
||
|
||
---
|
||
|
||
## 7. 性能特征
|
||
|
||
### 7.1 已应用的优化
|
||
|
||
| ID | 优化 | 收益 |
|
||
|---|---|---|
|
||
| **A** | `/init` 阶段 1 并行化(`asyncio.gather`) | 阶段 1 从 2-4s 降到 0.3-0.7s |
|
||
| **C** | 删除 `/init` 末尾的 intro 广播(SOUL 已含定位) | 阶段 3 整段消失,省 1-2s |
|
||
|
||
应用前 `/init` 耗时 **3-8s**,应用后 **0.5-1.2s**。
|
||
|
||
### 7.2 仍然存在的开销点
|
||
|
||
| 调用 | 单次耗时 | 主因 |
|
||
|---|---|---|
|
||
| `GET /api/agents/{id}` | 50-300ms | 每次触发 `_sync_legacy_agents()` 扫整个 `.deer-flow/agents/` 目录(目前 30+ 项) |
|
||
| `POST /api/threads` | 100-200ms | LangGraph thread 表插入 + 默认 state 初始化 |
|
||
| `POST /api/agents` | 100-300ms | 文件系统写入 SOUL.md + config.yaml + agents 表 |
|
||
| `GET/POST /state` | 50-150ms × 2 | checkpointer 读写 |
|
||
| `POST /runs/stream` 启动 | 50-100ms | RunManager + StreamBridge 初始化 |
|
||
| LLM 推理 | **数百毫秒~数十秒** | 取决于 model + 上下文长度 |
|
||
|
||
**LLM 推理才是绝对大头**,基础设施开销在十几个百分点以内。
|
||
|
||
### 7.3 未来可探索
|
||
|
||
- **B**(已识别未做): broadcast 跳过 GET-state,直接 POST 新建态。但 POST 需要构造完整的 state shape,改造成本不小。
|
||
- **D**(已识别未做): loopback HTTP → 内部函数调用。代码改动大,但能省掉 ~20-30 次 HTTP 解析开销。
|
||
|
||
---
|
||
|
||
## 8. 依赖与扩展点
|
||
|
||
### 8.1 `agent_orchestration` 技能
|
||
|
||
协调智能体强依赖此技能,SOUL 里硬编码:
|
||
|
||
```
|
||
通过使用agent_orchestration技能调用其他智能体干活,使用技能时传入agent_name和需要做的活
|
||
```
|
||
|
||
技能的实际派活产物是一个形如 `python3 scripts/agent_orchestration.py "agent_name" "task"` 的 shell 调用文本,后端用正则提取:
|
||
|
||
```python
|
||
_TOOL_CALL_PATTERN = re.compile(r'\.py\s+"([^"]*)"\s+"([^"]*)"')
|
||
```
|
||
|
||
**若该技能被删除/改名**,leader 流程会拿不到 `tool_calls`,直接走"无派活=共识达成"分支。注意 `skills/` 下必须保留此技能。
|
||
|
||
### 8.2 模型选择
|
||
|
||
前端通过 `GET /api/models` 拿到列表,在 `model` 字段传 `Model.name` (与 `config.yaml.models[].name` 对应)。
|
||
|
||
- `/init` 的 `model` 字段 → 协调智能体的默认模型 (写进 agents 配置)
|
||
- `/run/stream` 的 `model` 字段 → 该次具体运行用什么模型 (写进 `context.model_name`)
|
||
|
||
前端允许"逐席位覆盖模型",由前端在 special 调用时按 `agentConfigs[seat].model` 传入。后端不感知"席位"概念,只看到 `model` 字段。
|
||
|
||
### 8.3 自定义新席位
|
||
|
||
如果要加第 7 个席位(比如"成本测算"):
|
||
|
||
1. `.deer-flow/agents/roundtable-cost/` 加 `config.yaml` + `SOUL.md`
|
||
2. 前端 `RoundtablePlanningPage.tsx` 的 `ROUNDTABLE_ROLE_TEMPLATES` + `AGENT_NAME_TO_ROLE` 加映射
|
||
3. 圆桌可视化(圆圈环形图)加一个气泡 + 头像
|
||
|
||
**后端代码完全不需要改**。
|
||
|
||
---
|
||
|
||
## 9. 故障排查
|
||
|
||
| 现象 | 可能原因 | 排查 |
|
||
|---|---|---|
|
||
| `/init` 500 "create coordinator agent failed" | `agent_orchestration` 技能不存在 / config.yaml 写入失败 | 看后端日志 `multi_agent` logger;检查 `skills/` 目录 |
|
||
| `/run/stream` 502 "upstream returned HTTP 4xx" | JWT 过期 / 转发到上游时鉴权挂了 | 前端检查 Authorization 头是否带上 |
|
||
| leader 永远 `status: []` 立即结束 | 协调智能体没产生 `tool_calls`,直接给文本回答 | 协调智能体 SOUL 不够强、或模型不支持工具调用,换 model |
|
||
| special 内容为空 | 子智能体被 `excluded_tools` 限制到无可用工具 / 模型无文本输出 | 看上游 `runs/stream` 的 values 帧 messages.last.content |
|
||
| 进入 step 2 卡住转圈 | `/init` 超时或失败 | F12 Network 看 `/api/multi-agent/init` 状态码与响应体 |
|
||
|
||
### 服务端日志关键字
|
||
|
||
```bash
|
||
# multi_agent 路由的日志
|
||
grep "multi_agent" .runtime/logs/*.log
|
||
|
||
# 派活解析失败
|
||
grep "dispatch failed" .runtime/logs/*.log
|
||
|
||
# 广播失败(通常无害)
|
||
grep "update state .* failed" .runtime/logs/*.log
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 测试入口
|
||
|
||
后端目前没有针对本功能的单测。建议补:
|
||
|
||
```
|
||
backend/tests/test_multi_agent.py
|
||
- test_init_creates_threads_for_all_subagents
|
||
- test_init_creates_coordinator_with_orchestration_skill
|
||
- test_init_returns_400_when_agent_names_empty
|
||
- test_stream_leader_emits_status_frame_with_content
|
||
- test_stream_special_emits_status_frame_with_content
|
||
- test_extract_name_task_parses_orchestration_skill_output
|
||
- test_broadcast_skips_listed_agents
|
||
```
|
||
|
||
可用 `httpx.MockTransport` 拦截 loopback 请求,不依赖真实 Gateway。
|
||
|
||
---
|
||
|
||
## 11. 路径速查
|
||
|
||
| 资源 | 路径 |
|
||
|---|---|
|
||
| 路由实现 | `offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` |
|
||
| 路由注册 | `offline-backend-20260512/backend/app/gateway/app.py`(`include_router(multi_agent.router)`) |
|
||
| 6 个内置 agent | `offline-backend-20260512/backend/.deer-flow/agents/roundtable-*/` |
|
||
| `agent_orchestration` 技能定义 | `skills/public/agent_orchestration/`(若存在) |
|
||
| 前端 API helper | `frontend-web/src/roundtable-planning/api/multi-agent.ts` |
|
||
| 前端页面 | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` |
|
||
| 前端 API 速查 | `frontend-web/docs/multi-agent-api.md` |
|