# 多智能体圆桌研讨 · 后端开发文档 本文档面向需要维护或扩展"多智能体圆桌研讨"功能后端的开发者。前端 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 `。 | 方法 | 路径 | 用途 | |---|---|---| | 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": "", "agent_name": "" } ``` #### `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": "" } ``` ### 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//` 下生成新目录。长期会累积大量协调智能体目录,**未来需要清理脚本**(标记 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` |