deerflow-code/frontend-web/docs/multi-agent-backend-dev.md
2026-09-07 18:24:55 +08:00

75 KiB
Raw Permalink Blame History

多智能体圆桌研讨 · 后端开发文档

本文档面向需要维护或扩展「多智能体圆桌研讨」功能后端的开发者。前端 API 速查见 multi-agent-api.md;前端实现见 multi-agent-frontend-dev.md;推荐弹窗专项进度见 recommend-agents-progress.md(若未提供则当作待补文件)。

⚠️ 历史版本曾引用 multi-agent-step1-api.md、multi-agent-step3-api.md,当前仓库未提供这两份文件;Step 1(intent)与 Step 3(artifacts)的接口签名直接参考本页第 4 / 7 节即可。

源码内已有中文模块级注释(app/gateway/routers/intent.py、recommend.py、multi_agent.py、artifacts.py 及 clarification_utils.py),与本文互为补充。


0. 速读

  • 三个圆桌 Router(intent / recommend / multi_agent)通过 httpx ASGITransport 进程内直调本机 ASGI app,复用既有 /api/threads、/api/agents、/api/threads/{id}/runs/stream 路由——不走 TCP / 不经过网络栈。所有 client 都由 multi_agent._make_loopback_client(request) 工厂统一构造。
  • 历史包袱:早期实现走 httpx.AsyncClient(trust_env=False) + 127.0.0.1:<port>,遇到开发机 HTTP_PROXY / HTTPS_PROXY 时仍可能把回环走代理(症状 headers_in=42.160s)。改用 ASGITransport 后此问题不再存在,相关 trust_env=False workaround 全部移除。
  • 席位 agent 持久注册(磁盘 .deer-flow/agents/ + _sync_legacy_agents),协调智能体也是持久单例(id 固定为 roundtable-coordinator,与 intent/recommender 同样走 _roundtable_seed.ensure_roundtable_functional_agents() 缺失自动落地)。2026-05 之前曾按 roundtable-coordinator-{timestamp36} 每会话新建并把动态席位列表写进 SOUL,导致 .deer-flow/agents/ 累积大量壳目录;改造后 SOUL 是静态通用版本,「本次可调度席位列表」由 multi_agent.init 在 coordinator thread 创建后以 human 消息追加到对话历史,leader run 每轮都能看到。
  • .deer-flow/ 整个目录被 git ignore(仓库根 .gitignore 第 64 / 74 行),git clone 后内置 agent 目录在部署机上不会存在。但功能型 agent 现在通过 _roundtable_seed.ensure_roundtable_functional_agents() 自动落地(首次任何 intent/recommend/multi-agent 路由调用都会触发),因此新部署不再需要手动准备 roundtable-intent / roundtable-recommender / roundtable-coordinator 三个目录——它们都是 seeder 自动写盘的。互联网 demo 仍需要 6 个示例研讨席位(roundtable-intelligence 等),内网部署用客户自有 agent 替代这些席位,无需打包。详见 §11.7。
  • init 是 SSE 而非一次性 JSON:用 asyncio.as_completed 按真实完成顺序推送 seat_ready,前端因此能逐个点亮席位。
  • init_done.thread_ids 包含协调智能体自身(agent_threads[main_agent_name] = coord_thread),前端 Step 3 列文件时需自行过滤。
  • 澄清协议统一:Step 1 与 Step 2 leader 都借 ask_clarification + 终态 clarification 帧驱动同一套选项卡片组件;allow_multiple 默认 false(单选、点击即发送)。
  • leader 共识 = status: [] 空数组;澄清 = status: "clarification"(字符串);派活 = status: [[name, task], ...]。这三者互斥不混用。
  • 后端 picks clamp 2–6,前端用户可手动加到 8。
  • 不写死任何具体模型名:intent / recommend / multi_agent 三个 router 的 model_name = str(payload.get("model") or ""),空串会被下游 lead_agent._resolve_model_name() 自动回退到 config.yaml 的 models[0](与 /api/ai-writing 行为一致)。内网部署只需配 config.yaml,前后端均无需改硬编码。
  • 草稿/推荐历史持久化(2026-06):圆桌页的「历史草稿」从浏览器 localStorage 迁到后端 MySQL(/api/roundtable-drafts,按登录用户隔离);新增会话级推荐历史让推荐弹窗回显上次结果。两张表 roundtable_drafts / roundtable_recommend_history,启动 create_all 自动建表,详见 §7A。

1. 功能定位与三步数据流

对应前端页面:/page/workspace/roundtable/planning(RoundtablePlanningPage.tsx)的三个步骤。

Step 1  任务意图理解
  POST /api/intent/init          → 建 thread,绑定内置 roundtable-intent
  POST /api/intent/stream (多轮)  → 澄清对话,终态 [INTENT_READY] + summary

        ↓ intentReady { objective, constraints, assumptions }

推荐弹窗(Step 1 → Step 2 之间,可选)
  POST /api/recommend/stream      → 从候选池选 2–6 个席位,[RECOMMEND_READY] + picks

        ↓ selectedAgents[]   // 前端可再手动调整到 2-8

Step 2  多智能体圆桌研讨
  POST /api/multi-agent/init (SSE) → 协调智能体 + 各席位 thread(asyncio.as_completed 流式推 seat_ready)
  POST /api/multi-agent/run/stream → leader 派活 / special 执行 / 澄清

        ↓ lastLeaderContent、各席位交付、hasConsensus

Step 3  最终方案展示
  GET   /api/threads/{tid}/artifacts          → 列出 outputs/ 产出文件
  GET   /api/threads/{tid}/artifacts/{path}   → 预览或下载
  PATCH /api/threads/{tid}/artifacts/{path}   → 文本类产物原地编辑(可选)
  (总控结论、席位文本主要来自 Step 2 前端内存缓存,一般不再调 run/stream)
步骤 前端 currentStep 专属 Router 内置 Agent
1 1 intent.py roundtable-intent(固定 id,复用)
1→2 推荐弹窗 recommend.py roundtable-recommender(每请求新建 thread)
2 2 multi_agent.py 协调智能体(每次新建)+ 用户选定的 roundtable-* 席位
3 3 artifacts.py(通用) 无新 agent

所有接口需 Authorization: Bearer <JWT>。Loopback 调用一律由 _make_loopback_client(request) 构造(httpx.ASGITransport(app=request.app),base_url 占位 http://loopback),不经 TCP,因此不再受本机 HTTP_PROXY 干扰。


2. 涉及的后端文件

offline-backend-20260512/backend/
├── app/gateway/
│   ├── app.py                          # include_router: multi_agent, intent, recommend (artifacts 已存在)
│   └── routers/
│       ├── multi_agent.py              # Step 2:init(SSE) + run/stream + 共享的 loopback 工具
│       ├── intent.py                   # Step 1:init + stream(import multi_agent 工具)
│       ├── recommend.py                # Step 1→2:推荐 stream(import multi_agent 工具)
│       ├── artifacts.py                # Step 3:列出/下载/编辑 thread 产出(通用,非圆桌专用)
│       └── roundtable_drafts.py        # 持久化:草稿 CRUD + 推荐历史(§7A,2026-06 新增)
├── packages/harness/deerflow/persistence/roundtable_drafts/
│   ├── model.py                        # ORM:RoundtableDraftRow + RoundtableRecommendHistoryRow
│   ├── sql.py                          # RoundtableDraftRepository(CRUD + 历史)
│   └── __init__.py                     # make_roundtable_draft_store(sf)
├── packages/harness/deerflow/tools/builtins/
│   └── clarification_utils.py          # resolve_allow_multiple + coerce_bool(Step1/Step2 共享)
└── .deer-flow/agents/
    ├── roundtable-intent/              # Step 1 任务理解(SOUL 含完整工作流,可不挂 Skill)
    ├── roundtable-recommender/         # 推荐智能体
    ├── roundtable-intelligence/        # 默认可选席位(6 个,前端可替换)
    ├── roundtable-environment/
    ├── roundtable-solution-design/
    ├── roundtable-risk-review/
    ├── roundtable-execution-plan/
    ├── roundtable-summary/
    └── roundtable-coordinator-<id>/    # 协调智能体壳,每次会话新建,长期累积

内置 agent 注册:各 roundtable-* 目录通过 _sync_legacy_agents()(app/gateway/routers/agents.py)在下次 /api/agents 请求时 upsert 进 agents 表,user_id IS NULL 表示内置。recommend.py 不依赖此表,前端会过滤 roundtable-coordinator-* 出候选池。

Router 间共享导出:intent.py 与 recommend.py 从 multi_agent.py import _auth_headers / _create_thread / _flatten_content / _loopback_base / _parse_last_messages / _run_payload / _stream_upstream 等底层工具,不要在 intent / recommend 重复实现 loopback 逻辑。

共享工具逻辑:clarification_utils.py 的 resolve_allow_multiple(args) 与 coerce_bool(value, default) 被 intent.py / multi_agent.py / ClarificationMiddleware 共用:

  • coerce_bool 把 LLM tool args 里可能出现的 bool / 数字 / 字符串("true"、"1"、"yes"、"y"、"on" / "false"、"0"、"no"、"n"、"off")统一成 bool;
  • resolve_allow_multiple 仅 args["allow_multiple"] 显式为真时返回 True,缺省 / null 都视为 False(单选)。

3. 接口总览

方法 路径 步骤 用途
POST /api/intent/init 1 创建意图澄清 thread
POST /api/intent/stream 1 SSE,一轮用户对话
POST /api/recommend/stream 1→2 SSE,一次性推荐席位
POST /api/multi-agent/init 2 SSE,按完成顺序推 seat_ready,最后 init_done
POST /api/multi-agent/run/stream 2 SSE,leader 或 special 一轮
GET /api/threads/{thread_id}/artifacts 3 列出该 thread 的 outputs 文件
GET /api/threads/{thread_id}/artifacts/{path} 3 预览/下载单个产出(含 .skill/ 内文件提取)
PATCH /api/threads/{thread_id}/artifacts/{path} 3 编辑文本类产物(白名单后缀)
GET /api/roundtable-drafts 持久化 列出当前用户草稿(轻量元数据)
POST /api/roundtable-drafts 持久化 新建草稿
GET /api/roundtable-drafts/{id} 持久化 取草稿全量(含 step1/step2 快照)
PUT /api/roundtable-drafts/{id} 持久化 局部更新(title / furthest_step / step1 / step2)
DELETE /api/roundtable-drafts/{id} 持久化 删除草稿
GET /api/roundtable-drafts/{id}/recommendations 持久化 列出该草稿的推荐历史(倒序)
POST /api/roundtable-drafts/{id}/recommendations 持久化 追加一次推荐记录

字段级请求/响应见各 step 的 API 文档;下文只写后端实现语义。圆桌草稿/推荐历史持久化见 §7A。


4. Step 1:任务理解(intent.py)

4.1 定位

轻量包装 Lead Agent 运行时,固定跑内置 roundtable-intent(INTENT_AGENT_ID 常量)。不做 broadcast、不派活;每轮用户消息对应一次 /stream。

4.2 POST /api/intent/init

  1. _create_thread() → LangGraph thread
  2. 返回固定 agent_id: "roundtable-intent"(不每次新建 agent)

请求体:{"model": "deepseek-chat"} — model 可选,此处不写入 thread,每轮 /stream 自己带 model。

与 Step 2 /init 的区别:无协调智能体、无 agent_names 列表。

4.3 POST /api/intent/stream

入参:{thread_id, message, model?},缺 thread_id 或 message → 400。

上游 run_payload:见 §6.6;excluded_tools 屏蔽除 ask_clarification 外的几乎全部工具,硬清单:

[
  "web_search",
  "present_files",
  "view_image",
  "agent_orchestration",
  "bash", "ls", "read_file", "write_file", "str_replace",
  "memory", "hindsight_recall", "hindsight_reflect", "hindsight_retain",
  "task", "write_todos",
]

写死黑名单而不是「只允许 ask_clarification」的原因:SOUL 文字约束不可靠,硬排除更稳;防止模型偏离去写方案文档或派活。

流式:透传 LangGraph SSE → _parse_last_messages(captured) 从倒数 values 快照拿 messages。

终态帧判定顺序(重要,勿调整):

  1. [INTENT_READY] 优先:从最新消息向前扫描每条 AIMessage,正则 _INTENT_READY_PATTERN = /\[INTENT_READY\]\s*```json\s*(\{.*?\})\s*```/s 命中后 json.loads 解析
    • 命中 → status: "done",summary 为 JSON、content 为含标记的整段
    • 优先于 ask_clarification:模型可能在较早的 AIMessage 里已 emit [INTENT_READY],但本轮残留 ask_clarification 的 tool_call;不先扫 READY 前端会卡在澄清 UI
  2. 否则用 _find_dispatch_message(messages) 找最后一条带 tool_calls 的 AIMessage,检查是否含 ask_clarification
    • 命中 → status: "clarification",附结构化字段:
      {
        "status": "clarification",
        "content": "<follow-up 或 preamble 或 question>",
        "question": "...",
        "clarification_type": "missing_info",
        "clarification_context": "...",
        "options": ["..."],
        "allow_custom": true,
        "allow_multiple": false,
        "summary": null
      }
      
    • content 取 _find_trailing_ai_text(messages, exclude_text=preamble):ClarificationMiddleware 常在 tool 后还有一条纯文本 AIMessage,比 tool_call 旁的短 preamble 更适合作为气泡正文
    • allow_multiple 由 resolve_allow_multiple(cargs) 决定(缺省 false)
  3. 否则 → status: "asking",content 为 _flatten_content(dispatch_msg.content),前端继续 chat

异常:HTTPException / 其它异常都返回 {"status": "error", "content": "...", "summary": null},前端展示红色错误气泡。

summary 形状(done 时):

{
  "objective": "一句话目标",
  "constraints": ["硬约束 1", "..."],
  "assumptions": ["可选假设"]
}

4.4 内置 Agent:roundtable-intent

  • 配置:.deer-flow/agents/roundtable-intent/config.yaml + SOUL.md
  • SOUL 内写清:工作流、ask_clarification 用法、[INTENT_READY] 格式、禁止派活/写文件
  • 可不配置 skills;若配置 Skill,SOUL 要求不得偏离澄清与结束协议

5. 推荐智能体(recommend.py,Step 1 → 2)

5.1 定位

无状态单次调用:每次请求内部 _create_thread(),不暴露 /init。前端传入 Step 1 的 intent + 过滤后的 candidates(剔除 roundtable-intent、roundtable-recommender、历史 roundtable-coordinator-*)。

5.2 POST /api/recommend/stream

入参校验:

  • intent.objective 必填(trim 后非空)
  • candidates 必须是非空数组
  • 至少一个有效 agent_id(trim 后非空)
  • 三者缺一 → 400

Prompt 拼装(_build_prompt):

任务目标:<objective>
约束:<constraint1>; <constraint2>; ...
关键假设:<assumption1>; ...

候选智能体池:
- <agent_id> (<name>): <description>
- ...

请按 SOUL 中规定的格式输出推荐结果(自然语言思路 + [RECOMMEND_READY] + json 块)。

格式刻意贴近 SOUL 示例,让模型有清晰的复刻模板。

上游 run_payload:agent_name=RECOMMENDER_AGENT_ID、excluded_tools=["web_search", "present_files", "view_image", "agent_orchestration", "ask_clarification"] — 纯判断,不调任何工具。

终态分支:

  • 取最后一条非空 AIMessage 的文本(last_ai_content)
  • 正则 _RECOMMEND_READY_PATTERN = /\[RECOMMEND_READY\]\s*```json\s*(\{.*?\})\s*```/s 匹配 picks JSON
  • _validate_picks(parsed.picks, candidate_ids):
    • 跳过非 dict / 缺 agent_id / 不在候选池 / 已 seen 的项
    • 至多保留 _MAX_PICKS = 6 个
    • 不足 _MIN_PICKS = 2 → 返回 [],触发 fallback
  • 有效 picks ≥ 2 → status: "done",picks 有值
  • 否则 → status: "asking"(不是模型在追问用户,而是「网关未能产出合法 picks」,前端走 FALLBACK 默认 6 席)
{
  "status": "done",
  "content": "<自然语言推荐理由>",
  "picks": [{"agent_id": "roundtable-risk-review", "reason": "..."}]
}

picks 数量上下限的不一致(需要心里有数):

边界 后端 前端弹窗
下限 _MIN_PICKS = 2(不足返 asking) MIN_SELECTED = 2(确认按钮校验)
上限 _MAX_PICKS = 6(截断模型输出) MAX_SELECTED = 8(允许用户手加 2 个)

后端只对模型输出做 clamp,用户可手动追加候选池里其它 agent 到 8 个。

前端流式展示:自然语言理由与 JSON 由 [RECOMMEND_READY] 分界;TitleMiddleware 产生的标题增量会在客户端过滤(langgraph_node 含 TitleMiddleware / tags 含 middleware:title),避免污染推荐理由区。

5.3 与 SOUL 的契约

模型须在自然语言思路后输出:

[RECOMMEND_READY]
```json
{"picks": [{"agent_id": "...", "reason": "..."}, ...]}

`agent_id` 必须是候选池内已有的 id,`_validate_picks` 会做白名单校验。

---

## 6. Step 2:多智能体研讨(`multi_agent.py`)

### 6.1 `POST /api/multi-agent/init` — SSE,非一次性 JSON

请求体: { "agent_names": ["roundtable-intelligence", ...], // 用户选的席位 id "main_agent_name": "<可选,已废弃>", // 历史字段;2026-05 改造后后端忽略,统一用 COORDINATOR_AGENT_ID "model": "deepseek-chat" // 可选;不传由 lead_agent 回退到 config.yaml models[0] }


**协调智能体改造说明(2026-05)**:以前 `main_agent_name` 由前端用时间戳生成
(`roundtable-coordinator-${Date.now().toString(36)}`),后端 `_create_coordinator_direct`
把动态拼接的席位列表 SOUL 写盘并入 DB,导致 `.deer-flow/agents/` 越用越多。改造后:

- **id 固定**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"`(前后端常量必须保持一致);
- **缺失自动落地**:路由入口调 `ensure_roundtable_functional_agents()`,与 intent/recommender 共用同一套 seeder;
- **席位列表运行时注入**:init 创建好 coordinator thread 之后,立刻通过 `_append_thread_message` 把「【圆桌系统初始化】本次可调度席位:…」作为 human 消息追加到 thread 历史最前面,leader run 每轮都能看到;
- **`main_agent_name` 入参保留为向后兼容**:旧 client 仍可传,后端只是日志里看到不会用它创建新 agent。

**早期校验**(同步抛 HTTP 4xx,**不**进 SSE):

- `agent_names` 必须非空
- 每个 `agent_names` 元素需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好)
- 所有 id 都 `.lower()` 后比较 / 存储
- `main_agent_name` 入参不再做校验(被忽略,统一用 `COORDINATOR_AGENT_ID` 常量)

**SSE 内部流程**(2026-05 单例改造后):

ensure_roundtable_functional_agents() // 路由入口:保证 .deer-flow/agents/roundtable-coordinator/ 存在 // 缺失时由 _roundtable_seed.COORDINATOR_AGENT_SOUL 落地静态 SOUL

asyncio.as_completed(各席位 _prepare_sub_agent) 每完成一个 → data: {"event":"seat_ready","agent_name","thread_id","display_name","description"} (按真实完成顺序,先完成先发,前端因此能逐个点亮)

异常时:取消所有未完成的 task,避免泄露到 AsyncClient close 之后

协调智能体(单例): main_agent_name = COORDINATOR_AGENT_ID # 固定常量,不再每次新建 agent coord_thread = _create_thread_direct(...) # 仍然每次新 thread,会话独立 agent_threads[main_agent_name] = coord_thread // ⚠️ thread_ids 含 coordinator 自身

通过 HTTP loopback append 一条 human 消息,把本次席位清单写入对话历史:

"【圆桌系统初始化】本次研讨可调度的席位清单如下..."

leader run 每轮都能从对话历史里读到,等效于旧版动态 SOUL 的作用。

_append_thread_message(seed_client, ..., coord_thread, seat_introduction)

→ data: {"event":"coordinator_ready","main_agent_name","thread_id"}

→ data: {"event":"init_done","main_agent_name","thread_ids":{...}}

失败 → data: {"event":"error","detail":"..."}


**协调智能体单例 SOUL**(磁盘上唯一一份,见 `_roundtable_seed.COORDINATOR_AGENT_SOUL`):

通用协调规则,**不含**席位列表。模型派活时从对话历史最前面的「【圆桌系统初始化】…」消息读取本次可用 agent_name。仍然强调英文 id(避免把中文显示名拼进 `agent_name`),靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。

**席位清单注入消息形状**(init 阶段写入 coordinator thread):

【圆桌系统初始化】本次研讨可调度的席位清单如下,请严格按 agent_name 通过 agent_orchestration 技能派活,不要派给不在清单内的 agent:

  • agent_name: roundtable-intelligence(情报收集):
  • agent_name: roundtable-environment(环境评估): ...

中文显示名通过 `_get_agents_batch_direct` 一次 DB 查询取 `name`、描述取 `description`。

**单例 agent 的几个语义保障**:

1. **agent 落地幂等**:`_ensure_one` 检查目录是否存在,存在就 no-op,不覆盖运维手改的 SOUL;
2. **席位列表 append 失败不阻断**:`_append_thread_message` 包在 try/except 里,失败只记 warning,leader run 仍能跑(只是看不到席位清单,可能派活到错的 id,由 `_validate_agent_id` 在 run/stream 里报错);
3. **`_create_coordinator_direct` 已删除**:旧函数已移除(2026-05 改造),避免后续维护者误调;如果你正在 review 旧 PR 看到这个函数,那是 pre-rebase 版本。

### 6.2 `/run/stream` — `agent_type = "leader"`(`_leader_run`)

  1. 并行启动 pre_broadcast_task(不 await):向除自己外所有 thread 追加 "总控智能体收到用户消息:" human 消息

  2. _run_payload + _stream_upstream(POST /api/threads/{coordinator}/runs/stream)

    • excluded_tools: ["web_search"] // 只禁联网,其余工具(含 agent_orchestration)保留
    • skill_stop_names: {"agent_orchestration", *callers_extra} // 网关自动并入 agent_orchestration,让 SkillStopMiddleware 在工具调用后停图, // 便于本路由从 AIMessage.tool_calls 解析派活列表
  3. 透传上游 SSE,_parse_last_messages 拿 messages

  4. await pre_broadcast_task(LLM 通常已跑完,这里 await 接近 0 成本)

  5. _find_dispatch_message(messages) 找带 tool_calls 的 AIMessage

  6. 分三支:

    分支 A — ask_clarification: {"status": "clarification", "content": follow_up 或 question 或 leader_text, "question", "clarification_type", "clarification_context", "options", "allow_custom", "allow_multiple", "agent_name"} ⚠️ 空 dispatched 不等于共识,前端展示澄清卡片并暂停自动派活循环

    分支 B — 有 tool_calls(派活): _extract_dispatch(tool_call)解析每个 tool_call → (sub_name, task_text) 并行 _broadcast("总控智能体给 X 派活,内容为:Y", skip=[leader, sub_name]) 等所有派活广播完成(asyncio.gather) {"status": [[sub_name, task_text], ...], "content": leader_text, "agent_name": agent_name}

    分支 C — 无 tool_calls: {"status": [], "content": leader_text, "agent_name": agent_name} // 共识达成,前端 hasConsensus = true


**`_extract_dispatch` 双格式支持**:

1. **结构化**:`{"name": "agent_orchestration", "args": {"agent_name": "...", "task": "..."}}`
   — 大多数 LLM 在 SOUL 引导下走这条
2. **Legacy bash argv**:`{"name": "bash", "args": {"command": '... .py "X" "Y" ...'}}`
   — 正则 `_TOOL_CALL_PATTERN = /\.py\s+"([^"]*)"\s+"([^"]*)"/`,兼容旧 skill;不可移除

两条路径都会过 `_strip_display_suffix`:用 `re.split(r"[^A-Za-z0-9_-]", agent_name, maxsplit=1)[0]` 截断第一个非合法 id 字符之后的内容,把 `roundtable-x(中文)` 还原为 `roundtable-x`;剥离后若仍违规,原样返回让上游 422。

**时序日志**(grep `[leader-timing]`):

payload_built — 构造请求体耗时(broadcast 已经在后台跑) first_upstream_line — 上游首行(LangGraph 启动 + LLM 首字节) upstream_stream_complete — 整条 SSE 透传完成 pre_broadcast_await — 必须 await 才能 yield 终态;>0 表示广播比 LLM 还慢 dispatch_broadcasts_await — 派活广播并行 gather 耗时


### 6.3 `/run/stream` — `agent_type = "special"`(`_special_run`)

  1. _run_payload + _stream_upstream(POST /api/threads/{sub}/runs/stream)
    • excluded_tools: ["web_search", "ask_clarification", "present_files", "view_image", "agent_orchestration"] // 子智能体禁派活/澄清/文件呈现/图片;只做交付
  2. 透传 SSE,_parse_last_messages 拿最后一条 AIMessage 的 content
  3. 启动 post_broadcast_task("子智能体 X 完成 Y 工作,交付内容为:..."),不 await
  4. yield {"status": "子智能体X完成X工作", "content": "<交付正文>", "agent_name": "..."} // ⚠️ 在 yield 之后再 await broadcast,让前端先收到终态、关 loading
  5. await post_broadcast_task(SSE 连接稍微多保持一会;前端早就拿到结果了)

**时序日志**(grep `[special-timing]`):

payload_built first_upstream_line upstream_stream_complete post_broadcast_total — yield 之后 await broadcast 的总耗时


### 6.4 `_broadcast` 与 `_append_thread_message` 机制

```python
async def _append_thread_message(client, base, headers, thread_id, content):
    # GET /api/threads/{tid}/state → state
    # state["values"]["messages"].append({"type": "human", "content": content})
    # POST /api/threads/{tid}/state
    # 失败仅打 warning,不阻断主流程

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),
                        return_exceptions=True)

目的:各 thread 对话历史里保留「谁派活、谁交付」的群聊上下文;skip 通常含发起方与接收方,避免回声。

三种触发:

触发点 内容 skip
leader pre_broadcast 总控智能体收到用户消息:<msg> [leader]
leader 派活 总控智能体给 X 派活,内容为:<task> [leader, sub_name]
special 后广播 子智能体 X 完成 Y 工作,交付内容为:<content> [sub]

注意:广播是 best-effort,对方 LLM 真正干活仍需前端显式 /run/stream 调到对应席位。

6.5 终态帧协议(leader / special / clarification)

前端 streamMultiAgent 用 isFinalStatusFrame() 识别:同时存在 status 与 content。

status 类型 含义
[["agent","task"],...] 派活列表(数组);[] = 共识
"clarification" 总控追问,需用户回复后再 leader
"子智能体 X 完成..." special 完成(字符串)
"error" 异常

与 Step 1 的 clarification 帧字段对齐(question、options、allow_multiple 等),便于复用选项卡片组件。

6.6 _run_payload 完整字段

{
  "input": {"messages": [{"type": "human",
                          "content": [{"type": "text", "text": new_message}],
                          "additional_kwargs": {}}]},
  "config": {"recursion_limit": 1000},
  "context": {
    "agent_name": <coordinator 或 sub agent id>,
    "model_name": "deepseek-chat",
    "mode": "pro",
    "reasoning_effort": "medium",
    "thinking_enabled": True,
    "is_plan_mode": False,
    "subagent_enabled": False,
    "thread_id": <thread_id>,
  },
  "stream_mode": ["messages-tuple", "values"],
  "stream_subgraphs": True,
  "stream_resumable": True,
  "assistant_id": "lead_agent",
  "on_disconnect": "continue",
  "excluded_tools": <按 router 不同>,
  "skill_stop_names": <leader 强制并入 agent_orchestration>,
}

字段含义:

  • recursion_limit: 1000 — 防止 LangGraph 循环深度兜底(圆桌实际很难达到)
  • thinking_enabled: True — 开启模型的扩展思考(CoT),需 model 支持
  • is_plan_mode: False、subagent_enabled: False — 圆桌内禁用 TodoList / subagent 中间件,避免与多智能体协调冲突
  • stream_mode: ["messages-tuple", "values"] — 同时需要逐 token 增量与最后一次完整 values 快照
  • assistant_id: "lead_agent" — 走 Lead Agent 工厂
  • on_disconnect: "continue" — 客户端断连后上游继续跑完(不丢 token 但客户端拿不到)

6.7 上游 SSE 透传与 _parse_last_messages

_stream_upstream 逐行 yield 上游 /api/threads/{tid}/runs/stream 的 SSE。返回元组:

(line, None)        — 原样转发给浏览器(含 event: / data: 行)
(None, captured)    — 流结束,captured 是所有 data: 行的字符串列表

日志 [upstream-timing] <agent> headers_in=<> / first_line=<>:排查上游首字节延迟;改 ASGITransport 后通常 headers_in < 10ms,长尾大概率出在路由 handler 内部(不再是网络/代理问题)。

_parse_last_messages(captured) 从倒数往前扫,跳过 trailing data: null end 帧,找第一条解析为 dict 且含 messages key 的 frame;取 messages 列表。无可解析帧抛 502。

6.8 三个 AIMessage 搜索辅助

函数 用途
_find_dispatch_message(messages) 从后向前找带 tool_calls 的 AIMessage(跳过 SkillStop / Clarification 注入的 ToolMessage);都没有则返回最后一条 AIMessage
_find_trailing_ai_text(messages, exclude_text) 从后向前找无 tool_calls 的 AIMessage 的纯文本(用于澄清后的 follow-up,排除等于 preamble 的项)
_flatten_content(content) 把 str / 结构化 content blocks [{"text": ...}] 拍平成单串

7. Step 3:产出文件(artifacts.py,通用 Thread API)

Step 3 没有 单独的 multi_agent 路由;使用既有 artifacts 能力,圆桌只是消费者之一。

7.1 GET /api/threads/{thread_id}/artifacts

  • 鉴权:@require_permission("threads", "read", owner_check=True)
  • 扫描:get_paths().sandbox_outputs_dir(thread_id, user_id=get_effective_user_id())
    • 解析为 .deer-flow/users/{user_id}/threads/{thread_id}/user-data/outputs/
  • collect_outputs_listing(outputs_dir):
    • 递归 rglob("*"),过滤非文件
    • 软链接安全检查(resolved.relative_to(outputs_root),越界丢弃)
    • 按 modified_at 倒序
  • 目录不存在 → files: [](非 404)
{
  "thread_id": "...",
  "files": [
    {
      "path": "mnt/user-data/outputs/report.md",
      "name": "report.md",
      "size_bytes": 12480,
      "mime_type": "text/markdown",
      "modified_at": "2026-05-23T14:32:30.123Z"
    }
  ]
}

前端 listSessionArtifacts(threadId) 封装此接口;Step 3 对协调 thread 与各席位 thread 分别拉取,合并为下载清单(实际只列子席位)。

7.2 GET /api/threads/{thread_id}/artifacts/{path}

  • 鉴权同上
  • 虚拟路径前缀 mnt/user-data/outputs/...,经 resolve_thread_virtual_path 转物理路径并防穿越
  • 特殊:.skill/ 压缩包透视:若 path 含 .skill/,从 ZIP 提取内部文件(如 xxx.skill/SKILL.md)。_extract_file_from_skill_archive 同时支持「直接路径命中」与「带顶层目录前缀」两种压缩包结构。结果带 Cache-Control: private, max-age=300 头避免重复解压
  • ACTIVE_CONTENT_MIME_TYPES = {"text/html", "application/xhtml+xml", "image/svg+xml"} 始终强制 Content-Disposition: attachment(防止 XSS 与脚本执行)
  • 文本类(mime 以 text/ 开头)→ PlainTextResponse
  • 后缀判定不到、但 8KB 内无 null 字节 → 当文本返回(is_text_file_by_content)
  • 其它 → FileResponse 或 Response 内嵌
  • ?download=true 对所有类型强制附件下载

7.3 PATCH /api/threads/{thread_id}/artifacts/{path}

  • 鉴权:@require_permission("threads", "write", owner_check=True)
  • 请求体:{"content": "<新内容>"}
  • 限制:
    • .skill/ ZIP 内文件只读(400)
    • ACTIVE_CONTENT_MIME_TYPES 拒绝(400 active artifacts are read-only)
    • 文件后缀需在 EDITABLE_TEXT_SUFFIXES = {.md, .markdown, .txt, .json, .yaml, .yml, .csv, .xml, .log} 之内,或 mime 以 text/ 开头
  • 用 UTF-8 覆写原文件,返回 PlainTextResponse(body.content)

圆桌 Step 3 当前主要消费 GET,PATCH 用于「在线修订交付稿」类场景,前端默认未启用。

7.4 与 Step 2 内存的关系

总控结论、各席位最终段落:主要来自 Step 2 前端缓存的 lastLeaderContent、step2RoundtableDialogues(filter confidence === "子智能体交付"),不为 Step 3 新增聚合接口;如后续需要后端聚合,可加 sessions/summary 草案路由。


7A. 圆桌草稿与推荐历史持久化(roundtable_drafts.py,2026-06)

圆桌规划页的「历史记录」此前只存浏览器 localStorage(前端 utils/drafts.ts),换设备/清缓存即丢、大对话还会撞 5MB 配额。2026-06 迁到后端 MySQL,按登录用户隔离、跨设备保留;并新增会话级推荐历史,让推荐弹窗能回显上次推荐结果。

7A.1 数据模型(deerflow.persistence.roundtable_drafts)

仿 user_prompts(最简 CRUD)+ ai_writing_sessions(大 JSON 用 PortableLongText 存)两个现有模式。两张表:

  • roundtable_drafts(会话主表,一条 = 一次完整圆桌会话)
    • id(PK, str64) / user_id(idx) / title(512) / furthest_step(int, 1|2|3)
    • step1 / step2:前端 DraftStep{1,2}Snapshot 的 JSON,列类型 PortableLongText(MySQL LONGTEXT),避免 Step 2 长对话 + seatStubMessages 撞 TEXT 64KB 上限
    • created_at / updated_at:BeijingDateTime(项目规范,存北京时间且 tz-aware)
  • roundtable_recommend_history(推荐历史,按 draft_id 归属)
    • id(PK) / user_id(idx) / draft_id(idx) / objective(512) / status(32: done|fallback|asking|error) / model(128)
    • rationale / picks(JSON) / candidates(JSON 候选池快照):均 PortableLongText
    • created_at:BeijingDateTime

JSON 列在 repo(sql.py)里用 json.dumps/loads 手动序列化(与 ai_writing_sessions.transcript 同法)。两个 Row 已登记进 persistence/models/__init__.py,启动时 Base.metadata.create_all 自动建表——新部署无需手动建表(需 mysql 账号有建库/表权限)。

7A.2 路由(app/gateway/routers/roundtable_drafts.py,前缀 /api/roundtable-drafts)

方法 路径 说明
GET `` list_drafts → 轻量元数据列表(不含 step 大 blob),按 updated_at 倒序
POST `` create_draft,可带客户端 id(稳定 id 跨 create+update)
GET /{id} get_draft 全量;非属主 → 404
PUT /{id} update_draft,model_dump(exclude_unset=True) → 只更新客户端实际发的字段
DELETE /{id} 204;非属主 → 404
GET /{id}/recommendations 推荐历史倒序
POST /{id}/recommendations 追加一次推荐(先校验草稿属主,404 拦截越权)

鉴权/属主:用 _current_user_id(request)(登录态取 request.state.user.id,无登录回退 get_effective_user_id() 的 "default"),与 user_prompts 一致;所有 repo 读写按 user_id 过滤,跨用户访问一律 404。store 在 deps.py 接为 app.state.roundtable_draft_store(make_roundtable_draft_store(sf)),app.py include_router。

7A.3 与前端的契约要点

  • PUT 不带 title 时不改标题:前端自动保存只发 furthest_step/step1/step2,不发 title,因此不会覆盖用户手动重命名;标题只在 create(派生)与显式重命名(只发 title)时改。
  • 推荐时机:前端在打开推荐弹窗前先 ensureDraft 落地草稿拿到 draft_id,推荐完成后 POST 一条历史;重开弹窗 GET 历史回显最近一次,可「重新分析」再 POST。
  • 字段命名:后端 snake_case(furthest_step/created_at),前端 api/drafts.ts 在边界映射成 camelCase。

7A.4 设计取舍

  • 整存 JSON 而非拆表:step1/step2 形状宽且常变(前端 hook 里是 loose 类型),整存 JSON 避免每次改前端快照都要迁库;代价是后端不校验嵌套字段(与 ai_writing_sessions.transcript 同取舍)。
  • 推荐历史绑定草稿而非全局:语义是「这个会话上次推荐了什么」,故按 draft_id 归属;这也要求推荐前必有 draft_id(前端 ensureDraft 保证)。
  • 本地 dev 走 sqlite:config.yaml database.backend 本地默认 sqlite,走同一套 SQLAlchemy 代码,PortableLongText/JSON 各自降级;生产切 mysql 用同一份代码。

7B. 业务链条持久化(roundtable_chains.py,2026-06)

「业务链条」是用户在配置页编排的可复用、命名、有序的智能体编排模板;选中后 Step 2 由总控按链条顺序派活(前端编排,后端零改动)。链条持久化仿 §7A,新增一张表 + 一组 CRUD 路由。完整前后端设计见 frontend-web/docs/multi-agent-business-chain-dev.md。

7B.1 数据模型(deerflow.persistence.roundtable_chains)

  • 表 roundtable_chains:id(PK str64) / user_id(idx) / title(512) / description(512, nullable) / seats(PortableLongText JSON:有序 [{agent_id,name,description}, ...],seats[0] 先发言) / created_at / updated_at(BeijingDateTime)。
  • ORM RoundtableChainRow 登记进 persistence/models/__init__.py,启动 Base.metadata.create_all 自动建表;store make_roundtable_chain_store(sf) 接到 app.state.roundtable_chain_store(deps.py)。

7B.2 路由(app/gateway/routers/roundtable_chains.py,前缀 /api/roundtable-chains)

方法 路径 说明
GET `` 当前用户全部链条(含 seats,按 updated_at 倒序)
POST `` 新建(可带客户端 id);校验席位 2 ≤ N ≤ 8 + 去重
GET /{id} 取单条;非属主 → 404
PUT /{id} partial 更新(exclude_unset);带 seats 时同样校验
DELETE /{id} 204;非属主 → 404
  • 鉴权/属主用 _current_user_id(request),跨用户一律 404,与 roundtable_drafts 一致。
  • 席位约束 MIN_SEATS=2 / MAX_SEATS=8 与前端 CHAIN_MIN/MAX_SEATS、推荐弹窗 MIN/MAX_SELECTED 对齐;前端配置页保存前先校验,后端 _validate_seats 兜底(400)。
  • 前端 api/chains.ts 直接打这组路由(snake↔camel 边界映射);测试 tests/test_roundtable_chains.py(CRUD + 部分更新 + 跨用户隔离)。

8. 内置席位 SOUL 设计要点

每个 roundtable-*(除 intent / recommender)遵循统一结构:角色定位、核心职责、输出风格(emoji + 简短结论)、边界(只产出本席位、等总控派活)。

席位 agent_id emoji 核心职责
情报收集 roundtable-intelligence 📢 同类案例 + 历史风险
环境评估 roundtable-environment 🌍 硬件容量 + 承载力
方案设计 roundtable-solution-design 📐 主备方案 + 关键路径
风险审查 roundtable-risk-review ⚠️ 合规红线 + 收敛建议
执行规划 roundtable-execution-plan 📅 里程碑 + 资源
总结陈述 roundtable-summary 🎤 归纳 + 冲突标记

子智能体不安装 agent_orchestration;仅协调智能体安装,保证单调度源。


9. 关键设计决策

9.1 ASGITransport 进程内 loopback(替代旧的 HTTP loopback)

三个 router 通过 httpx.ASGITransport(app=request.app) 直调本机 ASGI app,不走 TCP / 不开 socket / 不经网络栈,但仍然完整复用 FastAPI 路由分发、鉴权 middleware、Pydantic 校验、SSE 流式响应。代价仅是路由层的少量 dispatch 开销(< 1ms),相对 LLM run 本身的数百 ms~数十秒可忽略。

统一入口:multi_agent._make_loopback_client(request),返回:

httpx.AsyncClient(
    transport=httpx.ASGITransport(app=request.app),
    base_url="http://loopback",   # 占位 host,ASGITransport 不在意
    timeout=None,                 # SSE 长连接仍需 None
)

已废止的 trust_env=False workaround:旧版本走 127.0.0.1:<port> 真实 HTTP,需要 trust_env=False 阻止 httpx 读 HTTP_PROXY 把回环也走代理(症状日志 headers_in=42.160s)。改 ASGITransport 后这个问题在物理上已不存在——根本没走网络栈。

9.2 协调智能体单例(2026-05 改造)

历史实现 main_agent_name = roundtable-coordinator-{timestamp36},会话隔离,但长期会累积 agent 目录(仓库里曾积累几十个 roundtable-coordinator-mp* 壳目录)。2026-05 改造选用上方决策 §9.2 选项 C 的本质:把 coordinator 实例化为「内置 + 静态 SOUL + 运行时注入席位列表」,固定 id roundtable-coordinator,与 roundtable-intent / roundtable-recommender 同样走 ensure_roundtable_functional_agents() 缺失自动落地。

实现要点:

  • 磁盘 SOUL 是通用版本:不含本次席位列表,只规定协调规则、工具用法、终止条件;
  • 席位列表运行时注入:init 创建 coordinator thread 后立刻 _append_thread_message 写入一条 human 消息("【圆桌系统初始化】…"),位置在对话历史最前面,leader run 每轮自然读到;
  • _create_coordinator_direct 已删除:不再有"每次创建"路径。

遗留 roundtable-coordinator-* 目录的处理:仓库可能仍有几十个历史时间戳壳目录。改造后不会再生成新的,但旧目录目前不自动清理(避免破坏可能仍存在的旧草稿引用)。运维可通过:

  • 在 admin agents 管理页逐个 DELETE;
  • 或写一次性脚本扫描 .deer-flow/agents/roundtable-coordinator-* 并删除目录 + DB 记录(注意不要删除 roundtable-coordinator 本体)。

9.3 澄清协议统一

Step 1 与 Step 2 leader 均通过 ask_clarification + 终态 clarification 帧驱动选项卡片;[INTENT_READY] / 派活列表 / [] 各自表示阶段结束语义,互不混用。

9.4 选项单选默认

resolve_allow_multiple():未传则 false(单选、点击发送);仅显式 allow_multiple: true 时多选。降低模型把 missing_info 全标成多选导致 UX 混乱的概率。coerce_bool 容忍 LLM 返回的字符串/数字 bool。

9.5 SSE 末尾状态帧

LangGraph 原生流不含「派活结果 / 意图 summary / 推荐 picks」;各 router 在流末追加 JSON data: 帧,前端用 isFinalStatusFrame() / isFinalIntentFrame() / isFinalRecommendFrame() 与 LangGraph 帧区分解析。

9.6 SkillStopMiddleware 配合 leader 派活

leader run 自动并入 skill_stop_names = {"agent_orchestration", ...},让 SkillStopMiddleware 在 agent_orchestration 工具调用后立刻停图。这样网关能从 AIMessage.tool_calls 直接读出派活列表,不必等 LLM 真把任务执行完。

9.7 init agent_threads 含 coordinator 自身

init_done.thread_ids 是 {agent_name: thread_id} 形式,其中 main_agent_name 自己的 thread 也在里面。前端 Step 3 列文件时需自行过滤掉 coordinator thread,否则会多调一次 listSessionArtifacts(虽然结果会是空,但有冗余 HTTP)。


10. 性能特征

阶段 已做优化 典型耗时
Step 2 /init 席位 asyncio.as_completed + 流式 seat_ready + 去掉 intro 广播 约 0.5–1.2s(视席位数)
LLM /run/stream — 数百 ms~数十秒(主因)
leader pre_broadcast 与上游 LLM run 并发启动 通常已在 LLM 完成前结束
leader 派活广播 asyncio.gather 并行 < 200ms(同 thread state R/W)
special post_broadcast yield 之后才 await,前端先关 loading 不阻塞 UX
Step 1/推荐 单 thread,无 broadcast 与模型相关

时序日志关键字(开发自带):

grep "init-timing"      .runtime/logs/*.log   # 仅前端打,作参考
grep "upstream-timing"  .runtime/logs/*.log   # _stream_upstream 内 headers_in / first_line
grep "leader-timing"    .runtime/logs/*.log   # payload_built / first_upstream_line / dispatch_broadcasts_await ...
grep "special-timing"   .runtime/logs/*.log   # payload_built / first_upstream_line / post_broadcast_total

仍存在的开销:GET /api/agents/{id} 触发 legacy sync 扫目录、GET/POST /state 广播等。未来可考虑 loopback 改内部函数调用。


11. 依赖与扩展点

11.1 agent_orchestration 技能

协调智能体派活依赖此技能;正则 _TOOL_CALL_PATTERN 从 tool 输出提取 agent_name 与 task。技能缺失时 leader 易变成「无 tool_calls → 空 status → 前端误判共识」。

11.2 模型

model 写入 run 的 context.model_name;前端可按席位覆盖 special 调用的模型(见前端文档 §5.5 Persona 抽屉)。

绝不在路由里硬编码具体模型名。三个 router 一律 str(payload.get("model") or ""):

  • 前端不传 / 传空串 → 下游 lead_agent._resolve_model_name("") 命中 falsy 分支,返回 app_config.models[0].name(config.yaml 配置顺序里的第一项)
  • 前端传了一个不在 config.yaml 里的 model 名 → 同样回退到首项,并 logger.warning("Model 'x' not found ... fallback to ...")
  • 前端传了合法 model 名 → 直接使用

这是 /api/ai-writing 与圆桌三路由的统一行为,让本地 / 内网部署只需要改 config.yaml,不需要触前端常量或路由代码。详见 deerflow/agents/lead_agent/agent.py:_resolve_model_name。

11.3 新增席位

  1. .deer-flow/agents/roundtable-xxx/ 增加 config.yaml + SOUL.md
  2. 前端候选池 / 推荐逻辑纳入该 id
  3. Step 2 agent_names 传入即可 — multi_agent 路由无需改

11.7 内置 agent 部署持久化(必读,关系到内网部署能否启动)

11.7.1 问题背景

圆桌功能涉及的 agent 都靠磁盘文件激活(.deer-flow/agents/<id>/{config.yaml,SOUL.md}),但仓库根 .gitignore 第 64 / 74 行有:

offline-backend-20260512/backend/.deer-flow/*

整个 .deer-flow/ 被 git 排除。新机器 git clone 后 .deer-flow/agents/ 不存在 → 圆桌相关 agent 缺失 → 进入 /api/intent/init 后 _create_thread 本身能成功,但下一步 /api/intent/stream 真正调用 LangGraph 时会因为 agent 解析失败抛 500(典型表现:前端 Step 1 init 一次成功,下一句话发出去就 500)。

11.7.1.1 哪些 agent 必须打包,哪些不需要

圆桌涉及的 agent 分两类,部署侧需求完全不同:

Agent 角色 是否必须打包发布
roundtable-intent 功能型:Step 1 任务理解,所有部署都用它 ✅ seeder 自动落地(无需手工打包)
roundtable-recommender 功能型:Step 1→2 之间从候选池里挑推荐,所有部署都用它 ✅ seeder 自动落地(无需手工打包)
roundtable-coordinator 功能型单例:Step 2 总控协调,2026-05 起为单例(id 固定) ✅ seeder 自动落地(无需手工打包)
roundtable-intelligence 示例研讨席位(情报收集) ⚠️ 互联网 demo 用,内网不需要
roundtable-environment 示例研讨席位(环境评估) ⚠️ 同上
roundtable-solution-design 示例研讨席位(方案设计) ⚠️ 同上
roundtable-risk-review 示例研讨席位(风险审查) ⚠️ 同上
roundtable-execution-plan 示例研讨席位(执行规划) ⚠️ 同上
roundtable-summary 示例研讨席位(总结陈述) ⚠️ 同上
roundtable-coordinator-*(带后缀) 2026-05 之前每次会话动态新建的临时壳 ❌ 遗留(仓库可能仍有几十个旧目录,新版本不再生成;运维按需清理)

关键变化(2026-05):三个功能型 agent(intent / recommender / coordinator)现在都由 _roundtable_seed.ensure_roundtable_functional_agents() 自动落地——任何 intent / recommend / multi-agent 路由的入口都会触发,首次请求时缺什么补什么。所以新部署的 机器 git clone 后既不需要手工拷目录、也不需要走下文的 .gitignore 白名单方案, SOUL 改动跟着 Python 代码走 git review 即可。

关键区分:

  • roundtable-intent / roundtable-recommender 是圆桌功能本身的一部分——没有它们,整个 Step 1 + 推荐流程跑不起来,跟客户业务无关
  • 6 个 roundtable-<领域> 席位只是互联网 demo 用的示例研讨者,让访客打开页面就能看到完整三步流程。内网部署面对的是客户自己的业务场景,会把客户自有的领域 agent(行业专家、合规审查员、业务规则引擎……)注册成候选 agent;这时 6 个示例席位反而成噪声

对应的部署组合:

部署场景 打包内容
互联网 demo roundtable-intent + roundtable-recommender + 6 个示例席位
内网客户部署 roundtable-intent + roundtable-recommender(仅此 2 个,不带 6 个示例席位)
本地开发 全要(方便快速跑通)

内网部署的额外注意(去掉 6 个示例席位时):

  1. 客户自有的候选 agent 必须先存在:内网部署落地时,客户的业务 agent 应该已经注册进 .deer-flow/agents/(手工建目录或通过 POST /api/agents 创建)。否则候选池为空,推荐拿不到任何 picks。
  2. 前端 FALLBACK_AGENT_IDS 需要适配:前端 RecommendAgentsDialog.tsx 里硬编码了 6 个示例席位 id 作为 fallback;若推荐失败回退路径会拿这 6 个去 candidates 里交集,内网部署该交集恒为空,fallback 会失败。建议要么:
    • 方案 1(最稳):内网客户的候选池足够丰富 + recommender SOUL 调好,让推荐成功率 ~100%,不依赖 fallback
    • 方案 2:把 FALLBACK_AGENT_IDS 改成从后端 /api/agents 取前 6 个 published=true 的候选 agent(动态 fallback)。改动点见前端文档 §6.2 RecommendAgentsDialog
  3. 推荐 SOUL(roundtable-recommender/SOUL.md)通常无需改:SOUL 本身没写死候选 id,只描述「从给的候选池里挑」;候选池由前端传入,模型读到客户自有 agent 后会照样工作。

下文给出三种修复方案,推荐方案 A(最简单,无需写代码)。

11.7.2 方案 A:.gitignore 白名单(推荐)

让 git 只跟踪 roundtable-intent + roundtable-recommender 两个功能型 agent 的 config.yaml 与 SOUL.md,其它一律忽略。6 个示例席位不进白名单——互联网 demo 部署机如果需要它们,按 §11.7.2.A 在该分支单独追加白名单;内网部署机直接享受「干净的 .deer-flow/」。

步骤 1:编辑仓库根 .gitignore

在最后追加白名单段(位置很重要,必须在 offline-backend-20260512/backend/.deer-flow/* 之后,否则被前面的通配符吃掉):

# === 圆桌功能型 agent 跟随仓库发布(任何部署都需要) ===
# 父目录先 unignore,git 才能往下递归
!offline-backend-20260512/backend/.deer-flow/
!offline-backend-20260512/backend/.deer-flow/agents/
# 仅放行 2 个功能型 agent;6 个示例席位由互联网分支自行放行
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/SOUL.md

⚠️ 不要直接写 !offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/**——git 不会进入还在 ignore 的父目录,必须逐层 un-ignore,再精确放行文件。

步骤 2:强制 add 并提交

# 验证白名单生效
git check-ignore -v offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/config.yaml
# 期望输出无任何匹配,或最后一条匹配是上面的 ! 行

# 加入索引(只加 2 个功能型 agent)
git add offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent
git add offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender

git status   # 应只看到 2 × 2 = 4 个新文件,绝不出现 users/、threads/、data/、6 个示例席位

git commit -m "chore: 把圆桌功能型 agent(intent + recommender)纳入版本管理"

步骤 3:部署机验证

# clone 之后立刻
ls offline-backend-20260512/backend/.deer-flow/agents/
# 内网部署:应看到 roundtable-intent/ + roundtable-recommender/(+ 客户自有 agent)
# 互联网 demo:还应看到 6 个 roundtable-<领域>/ 示例席位

启动后端,前端打开圆桌规划页,Step 1 第一句话发出去能正常拿到 streaming 响应而不是 500,就说明 OK。

11.7.2.A 互联网 demo 分支:追加 6 个示例席位

互联网 demo 维护一条独立分支(或独立 .gitignore 段,用 build flag 切),在白名单后追加:

# === 仅互联网 demo 需要的 6 个示例研讨席位 ===
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-intelligence/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-environment/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-solution-design/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-risk-review/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-execution-plan/SOUL.md
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/config.yaml
!offline-backend-20260512/backend/.deer-flow/agents/roundtable-summary/SOUL.md

git add 对应 6 个目录、提交,互联网分支即拿到全部 8 个内置 agent。两条分支共享同一份 roundtable-intent / roundtable-recommender,SOUL 调整对两边都生效,避免双维护。

优点:

  • 零代码改动;新机器 git clone 即可,部署文档不用单独说「记得拷一下 .deer-flow/agents/」
  • 修改 SOUL 直接走 git 走 review 流程
  • 多人协作时 SOUL 改动可见

注意事项:

  • git status 检查时确保只有 16 个内置文件进了索引,绝对不能包括 users/、threads/、data/*.db、roundtable-coordinator-*/ 这些运行时数据
  • roundtable-coordinator-mp*/ 这类历史会话残留目录会被 ignore 规则自动忽略(白名单没放行它们),无需手动清理
  • 部署机如果之前手动建过 .deer-flow/agents/roundtable-intent/,git pull 会把仓库版本覆盖上去——这是期望行为

11.7.3 方案 B:源码内置 + 启动时种子化

如果不想让 .deer-flow/ 这种「运行时目录」出现在 git 里,可以把 SOUL/config 放进 packages/harness/ 里作为只读资源,启动时检查 .deer-flow/agents/<id>/ 不存在就从源拷过去。

步骤大纲:

  1. 在 packages/harness/deerflow/agents/builtin_personas/ 下建 8 个目录(roundtable-intent/ 等),把 SOUL.md + config.yaml 搬过去
  2. 在 app/gateway/app.py 的 lifespan 启动阶段新增 _seed_builtin_roundtable_agents():
    def _seed_builtin_roundtable_agents() -> None:
        from importlib.resources import files
        src_root = files("deerflow.agents.builtin_personas")
        dst_root = get_paths().agents_dir()  # .deer-flow/agents/
        for agent_id in BUILTIN_ROUNDTABLE_AGENT_IDS:  # 8 个常量
            dst = dst_root / agent_id
            if dst.exists():
                continue   # 已有(可能是用户改过的),不覆盖
            dst.mkdir(parents=True)
            for fname in ("config.yaml", "SOUL.md"):
                (dst / fname).write_bytes((src_root / agent_id / fname).read_bytes())
    
  3. 紧接着调一次 _sync_legacy_agents(store),把这些刚落地的目录 upsert 进 agents 表
  4. 把 BUILTIN_ROUNDTABLE_AGENT_IDS 常量也导出给 recommend.py / multi_agent.py 共享

优点:

  • .deer-flow/ 仍然作为「运行时数据目录」语义干净
  • 运维可以手动改 .deer-flow/agents/roundtable-intent/SOUL.md 临时调试,重启不被覆盖(if dst.exists(): continue)
  • 升级时如果想强制刷新,加个 --force-reseed-builtins 启动参数即可

缺点:

  • 多一段 bootstrap 代码 + 一份 pyproject.toml 的 package-data 声明(确保 SOUL.md 跟着 wheel 一起发布)
  • 「现场改 SOUL 不被覆盖」既是优点也是坑——A 改了 SOUL 但忘了同步源码,下次新机器部署又回到旧版本

11.7.4 方案 C:纯运维流程(最弱,不推荐)

不动代码、不动 .gitignore,发布说明里写一行:

部署前请将 <开发机>:offline-backend-20260512/backend/.deer-flow/agents/ 整个目录 scp 到部署机同位置。

只在「不允许改仓库结构」或「初期试跑」时用。长期维护几乎一定会有人忘记拷而部署失败。

11.7.5 方案选择决策

场景 推荐方案
单仓库多人协作、SOUL 经常调整 A(.gitignore 白名单)
SOUL 极少改,希望严格区分代码与运行时数据 B(源码 + lifespan 种子化)
一次性 POC / 即将切换到 B C

无论选哪种,部署机第一次启动后都建议跑这条 smoke test 验证:

curl -X POST http://127.0.0.1:8001/api/intent/init \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{}'
# 期望: 200 + {"status":"success","agent_id":"roundtable-intent","thread_id":"..."}

curl -X POST http://127.0.0.1:8001/api/intent/stream \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"thread_id":"<上一步返回的>","message":"测试一下"}'
# 期望: 200 + SSE 长连接,最后一帧 status 为 asking 或 clarification
# 若 500 → 看后端日志,大概率是 SOUL.md 找不到或 LLM 外网不通(看上一条排查项)

11.7.6 推荐智能体(roundtable-recommender)与研讨席位的关系(注:完整定制流程见 §11.8)

推荐路由 recommend.py 用到的 roundtable-recommender 与「研讨席位」是两件事:

  • roundtable-recommender:负责从候选池里挑 picks 的「推荐者」本身,是个 LLM agent,功能型,所有部署必须有
  • 研讨席位(无论是 6 个示例还是客户自有 agent):是「被挑选的候选」,前端 filterRecommendCandidates 过滤后送进 /api/recommend/stream 的 candidates 数组;recommender 在 SOUL 里看到候选池后才能输出 [RECOMMEND_READY] JSON

「推荐改成动态」改的是从池子里挑哪几个,不是「池子可以没有」。池子的来源在两类部署里不同:

部署 候选池来源
互联网 demo 仓库自带的 6 个 roundtable-<领域> 示例席位(+ 用户自建的)
内网客户 客户自有的业务 agent(通过 POST /api/agents 或 .deer-flow/agents/<id>/ 注册)

roundtable-recommender/SOUL.md 本身没写死任何候选 id(只描述「从给的候选池里挑」),所以内网部署即使候选池从「6 个示例」换成「客户自有 agent」,recommender 的 SOUL 无需改动,候选池由前端传入。

前端 fallback 行为差异:

互联网 demo 因为有这 6 个内置示例席位垫底,推荐失败时 FALLBACK_AGENT_IDS ∩ candidates 还能拿到回退选项;内网部署该交集恒为空,回退路径失效——所以内网部署应通过「候选池足够丰富 + recommender SOUL 调好」让推荐成功率接近 100%,或改造前端 FALLBACK_AGENT_IDS 为动态从 /api/agents 取前 N 个(见前端文档 §6.2)。

11.4 新增 Step 1 工具

若需新工具,须同步修改 intent.py 的 excluded_tools 黑名单(当前默认禁 15 个工具,仅 ask_clarification 实际可用)。

11.5 改回旧 bash 派活协议

后端已双格式兼容,前端无需变化;只要 SOUL 改成生成 bash + .py "X" "Y" argv,_extract_dispatch 仍会解析正确。但建议保持新的结构化 agent_orchestration,prompt 更稳定。

11.6 把广播改为推送

_append_thread_message 通过 GET state → mutate → POST state 实现,是为了与 LangGraph checkpoint 一致;如果将来 Gateway 提供 POST /api/threads/{id}/messages/append 这类原子接口,可换上,降低并发竞争(虽然圆桌当前不会并发广播同一 thread)。

11.8 添加 / 替换任务推荐智能体(roundtable-recommender)

推荐智能体 id 在后端是硬编码常量(recommend.py:63 → RECOMMENDER_AGENT_ID = "roundtable-recommender"),路由不接收 agent_id 参数。因此「添加推荐智能体」不是新增一个 id,而是落地这个固定 id 对应的文件(首次部署)或就地改 SOUL(定制行为)。

11.8.1 何时需要动它

场景 是否需要动 SOUL
全新部署,目录还不存在 ✅ 必须先把目录搭起来(§11.8.2)
候选池从 6 个互联网示例换成客户自有 agent ❌ 不用改——SOUL 没写死候选 id,候选池由前端传入
想改变推荐风格(更激进 / 更保守 / 偏好某领域) ✅ 改 SOUL(§11.8.4)
想让模型偏好你新加的领域 agent ⚠️ 优先改新 agent 的 description(候选池 prompt 用的就是它),其次才改 recommender SOUL
想改输出 JSON 格式 ❌ 不要——后端正则 _RECOMMEND_READY_PATTERN 写死了 [RECOMMEND_READY] + json 块;改 SOUL 输出会让后端解析失败走 fallback

11.8.2 标准目录结构

.deer-flow/agents/roundtable-recommender/
├── config.yaml      # id / name / description(只是 DB 元数据,不进 prompt)
└── SOUL.md          # 行为契约,详见 11.8.4

config.yaml 模板(id 必须严格等于 roundtable-recommender,否则 recommend.py 找不到):

description: 智能体推荐专员,基于用户的任务目标与约束,从候选智能体池中挑选最相关的 2-6 个智能体并给出推荐理由,为多智能体圆桌研讨准备最终参会名单
id: roundtable-recommender
name: 智能体推荐

字段说明:

  • id:不可改,与 RECOMMENDER_AGENT_ID 常量绑死
  • name:DB 显示名,候选池过滤时不参与(filterRecommendCandidates 按 id 排除)
  • description:DB 元数据,不进 prompt;改了不会影响推荐效果

SOUL.md 内容见 §11.8.4 模板。新增之后按 §11.7.2 把这两个文件加入 git 白名单 + commit 即可。

11.8.3 后端给推荐智能体喂的 prompt 长什么样

recommend.py:_build_prompt() 把前端传的 intent + candidates 拼成下面这段,作为唯一一条 user message 投递:

任务目标:对生产系统进行安全改造,满足等保 2.0 合规要求
约束:预算 ≤ 500 万元; 工期 ≤ 6 个月; 必须满足等保 2.0
关键假设:现有系统具备基础数字化能力; 允许预定窗口停机

候选智能体池:
- roundtable-intelligence (情报收集): 检索同类历史案例、行业最佳实践与外部情报
- roundtable-solution-design (方案设计): 产出主备方案与关键路径
- my-cost-calc (成本测算): 基于预算和资源单价计算总成本
- ...

请按 SOUL 中规定的格式输出推荐结果(自然语言思路 + [RECOMMEND_READY] + json 块)。

关键事实:

  • 候选 agent 的 name 与 description 在 prompt 里就是它的「介绍」——这是模型判断「这个 agent 适不适合本任务」的全部信息。客户自有 agent 想被推荐准确,description 必须写任务导向的能力描述,不要写 "由 XX 团队维护" 这种元信息
  • 候选池由前端过滤后传入,后端不读 DB;前端 filterRecommendCandidates 已剔除 roundtable-intent / roundtable-recommender / roundtable-coordinator-*
  • SOUL 看不到「intent agent 上一轮说了什么 / 用户原始 message」,只看到结构化的 objective / constraints / assumptions——这是设计选择,让推荐稳定

11.8.4 SOUL 设计要点(模板 + 注解)

SOUL 必须满足三件事,其它都可定制:

  1. 输出格式严格:自然语言思路 → [RECOMMEND_READY] 单独成行 → json {"picks": [{"agent_id": "...", "reason": "..."}, ...]}
  2. agent_id 严格来自候选池:模型造词出来会被 _validate_picks 过滤掉
  3. 2 ≤ picks ≤ 6:少于 2 → 整轮判 asking 走 fallback;多于 6 → 后端截断

下面是一份可直接用的最小 SOUL,重点行带注释(真正落盘时去掉 <!-- -->):

# 智能体推荐专员

## 角色定位
你是多智能体圆桌研讨的"智能体推荐专员"。任务理解专员已经把用户需求整理成
「任务目标 + 约束 + 关键假设」交给你,你的工作是从一个**候选智能体池**里,
挑出**最适合**参加这次圆桌研讨的 2-6 个智能体,并为每一位写一句推荐理由。

## 核心职责
- 仔细阅读用户的任务目标与约束
- 浏览候选池中每个智能体的 `name` 与 `description`
- 选出与本次任务**最相关**的 2-6 个智能体(数量动态,不必凑齐 6 个)
- 为每个推荐的智能体写一句**具体、可验证**的推荐理由
- 不推荐重复职能 / 不推荐与任务明显无关的智能体(宁缺毋滥)

## 数量约束
- **下限**:至少 2 个 / **上限**:最多 6 个
- 候选池不足 2 个时,有几个推几个

## 输出协议(后端用正则识别,严格执行)
1. 先用自然语言简短说明推荐思路(1-3 句话)
2. 输出标记 `[RECOMMEND_READY]` 单独成行
3. 紧接一个 ```json 代码块:

   ```json
   {
     "picks": [
       {"agent_id": "<候选池中的 id>", "reason": "<一句推荐理由>"},
       ...
     ]
   }

格式注意:

  • [RECOMMEND_READY] 单独成行,前后无其他字符
  • agent_id 严格来自候选池,不要造词
  • reason 具体,不要"很合适"这种空话

边界

  • 不调用任何工具(已被后端 excluded_tools 屏蔽)
  • 不评价候选 agent 优劣,只判断"是否适合本次任务"
  • 不发起对话、不反问用户

示例


##### 定制方向

- **偏好新加的 agent**:在「示例」里把它列进候选池并出现在推荐结果中,模型会学到这种 id 也可推荐。比单纯在「核心职责」里加文字约束有效
- **行业偏向**:在「角色定位」加一句「优先考虑能直接对接 <行业> 合规要求的智能体」
- **激进 vs 保守**:调「数量约束」——例如「能挑出强相关的 3 个就不要凑 6 个」会让 picks 缩到 3-4 个
- **不要做的事**:
  - ❌ 改 `[RECOMMEND_READY]` 为别的标记 → 解析失败
  - ❌ 把 JSON 结构改成 `{"recommendations": [...]}` 之类 → 解析失败
  - ❌ 让模型只输出 JSON 不带前面的自然语言 → 前端 `parseRecommendStreamContent` 把整段当 `rationale`,弹窗顶部为空白

#### 11.8.5 验证流程

部署完成后跑 smoke test 确认推荐路径通:

```bash
# 1. 确认 agent 文件落地
ls .deer-flow/agents/roundtable-recommender/
# 期望: SOUL.md  config.yaml

# 2. 确认 DB 已 upsert(任意 /api/agents 请求会触发 _sync_legacy_agents)
curl -H "Authorization: Bearer <TOKEN>" http://127.0.0.1:8001/api/agents | \
  python -c "import sys,json; ids=[a['id'] for a in json.load(sys.stdin).get('agents',[])]; print('roundtable-recommender' in ids)"
# 期望: True

# 3. 触发一次推荐(用最小 intent + 候选池)
curl -X POST http://127.0.0.1:8001/api/recommend/stream \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": {
      "objective": "测试推荐链路",
      "constraints": ["响应时间 < 30s"],
      "assumptions": []
    },
    "candidates": [
      {"agent_id": "agent-a", "name": "A", "description": "做 X"},
      {"agent_id": "agent-b", "name": "B", "description": "做 Y"},
      {"agent_id": "agent-c", "name": "C", "description": "做 Z"}
    ]
  }'
# 期望(SSE 流):最后一帧 data: {"status":"done","content":"...","picks":[...]}
# 若 picks 是空 → SOUL 没产生合法 [RECOMMEND_READY] / agent_id 不在候选池
# 若整轮 status=error → 看后端 _diagnose_exception 输出的 hint

11.8.6 故障排查清单

现象 排查方向
推荐永远走 fallback(status=asking) 后端日志 grep recommend stream,看 last_ai_content 是不是不含 [RECOMMEND_READY]——SOUL 没遵守协议
picks 数量被截断到 6 后端 _MAX_PICKS=6 故意限制,前端用户可再手加到 8
模型推荐了候选池里没有的 id _validate_picks 会丢掉,但若有效项不足 2 个会整轮走 fallback;改 SOUL 强调「严格来自候选池」
推荐结果不偏向客户自有 agent 先检查该 agent 的 description 是不是写得「任务导向」;再考虑在 SOUL 示例里加它
推荐稳定性差(同 intent 重跑结果飘) 推荐 model 用低 temperature(在 roundtable-recommender/config.yaml 加 model: <某个低 temp 模型> 或直接前端选 deterministic 模型)
前端弹窗顶部 rationale 区空白 SOUL 让模型只输出 JSON 没有自然语言;恢复成「先思路 + 后 JSON」

12. 故障排查

现象 可能原因 排查
Step 1 一直有澄清、无法进入 Step 2 未产出 [INTENT_READY] 或终态被 clarification 覆盖 看 SOUL、最后一帧 status;确认 intent 先扫 READY(§4.3 顺序)
选项全是多选 旧前端默认 allow_multiple !== false 确认 allow_multiple 字段与 resolve_allow_multiple(缺省 false)
Step 2 init SSE error 协调 agent 创建失败 / agent_orchestration 缺失 / _validate_agent_id 命中 Network + 后端 multi_agent 日志
Step 2 init 完全卡住(历史症状,ASGITransport 改造后已不应再出现) 旧版 httpx 走 HTTP_PROXY 拦回环 → 40s 超时 自改 ASGITransport 起此现象物理上不可能复现;若仍卡,看后端日志栈、检查路由 handler 是否阻塞
leader 立即 status: [] 未调用 orchestration 技能 / SkillStopMiddleware 没拦到 / SOUL 没声明派活 换模型或加强 SOUL;检查 skill_stop_names 是否包含 agent_orchestration
dispatch 失败但 leader 文本看着正常 模型把 roundtable-x(中文) 整段塞进 agent_name _strip_display_suffix 会兜底;若仍报 400,强化 SOUL 的「严格英文 id」
推荐 picks 为空 未输出 [RECOMMEND_READY] 或 picks 不在候选池 看 recommend 终态帧;检查 candidate_ids 集合
Step 3 无文件 子 agent 未写 outputs / 错路径 GET .../artifacts 对子 thread_id 逐个查;确认 sandbox outputs 路径
seat_ready 顺序不固定 asyncio.as_completed 按真实完成顺序推送 这是设计行为,不是 bug;前端不应按顺序依赖 agent_name

日志关键字

grep "intent stream"       .runtime/logs/*.log
grep "multi_agent\|multi-agent"  .runtime/logs/*.log
grep "recommend stream"    .runtime/logs/*.log
grep "dispatch failed"     .runtime/logs/*.log
grep "upstream-timing"     .runtime/logs/*.log
grep "leader-timing"       .runtime/logs/*.log
grep "special-timing"      .runtime/logs/*.log
grep "headers_in="         .runtime/logs/*.log   # 上游首字节延迟;ASGITransport 改造后通常 < 10ms

13. 测试建议

backend/tests/test_intent.py          # init、INTENT_READY 优先级、clarification 字段
backend/tests/test_recommend.py       # picks 校验、候选池过滤、clamp 2-6
backend/tests/test_multi_agent.py     # init SSE 事件顺序、leader/special 终态、clarification

可用 httpx.MockTransport 模拟 loopback,不依赖真实 Gateway:

def mock_handler(request: httpx.Request) -> httpx.Response:
    if request.url.path == "/api/threads":
        return httpx.Response(200, json={"thread_id": "t-1"})
    ...

transport = httpx.MockTransport(mock_handler)
client = httpx.AsyncClient(transport=transport, base_url=...)

针对 SSE 上游可用 respx 或直接 mock _stream_upstream 的字符串列表。


14. 路径速查

资源 路径
Step 1 路由 offline-backend-20260512/backend/app/gateway/routers/intent.py
推荐路由 offline-backend-20260512/backend/app/gateway/routers/recommend.py
Step 2 路由 offline-backend-20260512/backend/app/gateway/routers/multi_agent.py
Step 3 列出/下载/编辑 offline-backend-20260512/backend/app/gateway/routers/artifacts.py
草稿/推荐历史路由(§7A) offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py
草稿/推荐历史持久化 offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/
store 接线 offline-backend-20260512/backend/app/gateway/deps.py(app.state.roundtable_draft_store)
路由注册 offline-backend-20260512/backend/app/gateway/app.py
澄清单选/多选 offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py
任务理解 Agent offline-backend-20260512/backend/.deer-flow/agents/roundtable-intent/
推荐 Agent offline-backend-20260512/backend/.deer-flow/agents/roundtable-recommender/
圆桌席位 offline-backend-20260512/backend/.deer-flow/agents/roundtable-*/
协调智能体壳(累积) offline-backend-20260512/backend/.deer-flow/agents/roundtable-coordinator-*/
前端 Step 1 API frontend-web/src/roundtable-planning/api/intent.ts
前端推荐 API frontend-web/src/roundtable-planning/api/recommend.ts
前端 Step 2 API frontend-web/src/roundtable-planning/api/multi-agent.ts
前端页面 frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
前端开发文档 frontend-web/docs/multi-agent-frontend-dev.md
API 速查 frontend-web/docs/multi-agent-api.md