# 业务链条模式 · 需求与分步实现文档 > 面向「智能体推荐弹窗」的扩展:在现有**智能推荐**之外,新增**业务链条选择**与**业务链条配置**两块能力;选定业务链条后,Step 2 圆桌研讨**按链条内的固定顺序**逐个发言(而非现在的总控派活)。 > > 配套阅读: > - 前端总览:`multi-agent-frontend-dev.md`(推荐弹窗 §6.2、Step 2 编排 §5.4、草稿持久化 §5.7) > - 后端总览:`multi-agent-backend-dev.md`(Step 2 §6、草稿/历史持久化 §7A) > > 本文档约定:**「席位 / 智能体」**指一个 agent;**「业务链条 / chain」**指一组有序排列的 agent 配置,是可命名、可复用、可持久化的实体。 --- ## 0. 需求原文拆解 用户原始诉求逐条拆成可实现条目: | # | 原始诉求 | 拆解 | |---|----------|------| | 1 | 推荐弹窗要有「模式选择」 | 弹窗顶部加模式切换(智能推荐 / 业务链条),两种模式互斥,确认后都产出**有序**的 `SelectedAgent[]` | | 2 | 「智能推荐」就是现在这个 | 现有 `RecommendAgentsDialog` 整体收进「智能推荐」模式,行为不变 | | 3 | 「业务链条选择」是卡片列表 | 卡片 = 标题 + 一排智能体名 tag;tag 不同颜色;tag 之间有**向左箭头**连接,表达链条次序 | | 4 | 业务链条需要一个**配置页面** | 左侧智能体列表(复用 `GET /api/agents`),右侧有序长条卡片(名称 + 描述);支持右侧内部拖拽排序、左→右拖入指定位置 | | 5 | 配置页用弹窗还是新页面? | **本文档决策:独立路由页面**(理由见 §2.1) | | 6 | 选定链条后,Step 2 用链条内智能体研讨 | 复用既有 Step 2 init/stream;**总控(leader)按链条顺序逐个派活**(§6) | | 7 | 规划成分步可实现的需求文档 | 见 §7 分阶段实现 | --- ## 1. 目标与非目标 ### 1.1 目标 - 推荐弹窗成为「**席位来源选择器**」:无论智能推荐还是业务链条,最终都向 `onConfirm` 交付一个**有序的 `{ agent_id, name }[]`** + 一个 `orchestrationMode`。 - 业务链条是**用户可创建、命名、保存、复用、删除**的有序智能体编排模板。 - Step 2 支持两种编排: - `recommend` 模式 → 现有总控**自由派活**循环(不动)。 - `chain` 模式 → 总控仍是派活方,但**被约束按链条顺序逐个派活**(新增)。 ### 1.2 非目标(本期不做,留扩展点) - 链条内每个席位的 per-seat prompt/temperature 真实覆盖(仍仅前端记录,沿用现状)。 - 链条的分支 / 条件流转(只做线性顺序链)。 - 链条跨用户共享 / 市场(先做「本人私有链条」)。 - 链条版本管理 / 协作编辑。 --- ## 2. 关键设计决策 ### 2.1 决策 A:配置页面用「独立路由页面」而非弹窗 ✅ **结论:业务链条的「编辑/管理」用独立路由页面 `/page/workspace/roundtable/chains`;业务链条的「选择」仍内嵌在推荐弹窗里(卡片列表)。** 理由: | 维度 | 弹窗 | 独立页面 ✅ | |------|------|------------| | 双栏 + 拖拽 + 长条卡片 | 弹窗宽度(`sm:max-w-3xl`)+ `max-h-[85vh]` 拖拽空间局促,左右栏挤 | 整屏宽度,拖拽手感好 | | 复用入口 | 只能从推荐弹窗进 | 可从侧栏、推荐弹窗「管理链条」按钮、深链接进入 | | 可深链接/刷新 | 不可 | `/roundtable/chains` 可直达、可分享 | | 推荐弹窗内嵌编辑器 | 弹窗套弹窗 / 模式套编辑,状态复杂 | 关注点分离:弹窗只「选」,页面只「编」 | **分工**: - **推荐弹窗**:模式选择 + 业务链条**选择**(只读卡片列表,可点「去配置」跳到页面)。 - **配置页面**:业务链条的**增删改**(双栏拖拽编辑器 + 链条列表管理)。 > 备选:若产品坚持不加路由,可改为**全屏 Dialog(`max-w-[90vw] h-[90vh]`)**承载编辑器,从推荐弹窗内打开。结构与页面版一致,仅外壳不同。本文档按「独立页面」实现,§7 标注了切换成本。 ### 2.2 决策 B:链条持久化 —— 后端 MySQL(确定) ✅ **结论:业务链条存后端 MySQL,仿 `roundtable_drafts`(§7A)新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由,按登录用户隔离、跨设备保留。** 不走 localStorage。 理由:链条是可复用、需跨设备/会话保留的模板,与草稿/推荐历史性质一致;草稿当年正是因 localStorage 换设备即丢才迁后端(§7A),链条不重复踩坑。 落地: - 表/路由设计见 §5.2,是「复制 §7A 草稿持久化、把字段换成 chain」的机械工作。 - 前端 `api/chains.ts` 直接打后端接口(snake_case↔camelCase 在这层收口,仿 `api/drafts.ts`),**不提供 localStorage 实现**。 - 后端表必须先于前端 UI 落地(§7 阶段 0 即包含后端表/路由)。 ### 2.3 决策 C:拖拽库选型 —— `@dnd-kit` 仓库**当前无任何拖拽库**(`package.json` 无 `@dnd-kit` / `react-dnd` / `sortablejs`;`@xyflow/react` 是节点图,不适合列表排序)。 | 方案 | 取舍 | |------|------| | **`@dnd-kit/core` + `@dnd-kit/sortable`**(推荐) | React 19 友好、可访问性好、跨容器拖入 + 列表排序 + 插入指示器开箱即用;新增 ~30KB gzip 依赖 | | 原生 HTML5 DnD(零依赖兜底) | 不加依赖,但跨容器插入位置指示、键盘可达性都要手写,易出 bug | **建议**:新增 `@dnd-kit/core`、`@dnd-kit/sortable`、`@dnd-kit/utilities`。若产品方禁止新增依赖,退回原生 HTML5 DnD(§7 阶段 2 标注差异)。 ### 2.4 决策 D:Step 2「总控按链条顺序派活」,后端零改动 ✅ **形态:总控(leader)仍是唯一派活方,但每一轮被前端约束「只能派给链条里的下一个席位」,逐个走完整条链,最后由总控收口。** 区别于 recommend 模式的「总控自由决定派给谁」。 为什么后端无需改动: - Step 2 后端 `streamMultiAgent({ agentType: "leader" })` 每轮返回 `dispatched = [[agent, task], ...]`;`agentType: "special"` 单独驱动某席位跑一轮。这两个原语已足够。 - 每个 special 跑完,后端 `_broadcast` 会把「子智能体 X 完成…交付内容为:…」append 进**其它所有席位 + 总控 thread** 的历史(后端 §6.4),所以链路上下文天然累积。 - chain 模式只是**改变前端怎么提示 leader、以及怎么校验它的派活**——把每轮 leader 的提示词约束成「请向链条第 i 位【X】派活」,并对返回的 `dispatched` 做「必须命中链条下一席位」的校验/纠偏。**后端路由、SOUL、表结构都不动。** → chain 模式 = 在 `useStep2Orchestration` 里新增 `runChainOrchestration`:按链条游标逐位调 leader(约束派给该位)→ 拿到 task → 调 special → 下一位 → 链尾后调一次 leader 收口。详见 §6。 > 与「前端绕过 leader 直接顺序 special」的区别:本方案**保留总控在每一步的协调/串接职责**(leader 结合前序进展为下一席位编写针对性任务、并在派活说明里承上启下),更符合「总控按链条顺序派活」的语义,圆桌可视化里总控气泡也照常出现。 --- ## 3. 数据模型与类型 新增前端类型,建议放 `frontend-web/src/roundtable-planning/lib/business-chain.ts`: ```ts /** 业务链条里的一个有序席位。 */ export interface ChainSeat { agent_id: string; /** 落库时的快照名/描述,避免 agent 改名后链条显示错乱;展示优先用实时 agent 数据,回退到快照。 */ name: string; description?: string; } /** 一条可复用的业务链条。 */ export interface BusinessChain { id: string; title: string; description?: string; /** 有序席位列表;seats[0] 是第一个发言者。 */ seats: ChainSeat[]; createdAt?: string; updatedAt?: string; } /** 推荐弹窗确认后回传给主页面的统一结果。 */ export interface SeatSelectionResult { /** 有序席位(与现有 handleConfirmAgents 入参兼容)。 */ agents: Array<{ agent_id: string; name: string }>; /** 决定 Step 2 走哪种编排。 */ mode: "recommend" | "chain"; /** chain 模式下带上来源链条(用于草稿快照 / Step 2 展示链名)。 */ chain?: Pick; } ``` **约束**:链条席位数沿用圆桌的 `2 ≤ N ≤ 8`(`MIN_SELECTED`/`MAX_SELECTED`),配置页保存时校验。 **tag 配色**:复用 `roundtable-constants.ts::getAvatarTypeFor(agentId, index)`,得到 `blue|purple|emerald|amber|pink|human`,再映射到已有的 avatar 配色 token(与圆桌可视化、Step 2 气泡同一套色板,视觉统一)。 --- ## 4. 现有代码改动点速查 | 位置 | 改动 | 说明 | |------|------|------| | `components/RecommendAgentsDialog.tsx` | 包一层「模式选择」外壳,现有内容收进「智能推荐」Tab | `onConfirm` 签名从 `agents[]` 升级为 `SeatSelectionResult`(或加可选第二参 `mode`,见 §7 阶段 3 的兼容策略) | | `pages/RoundtablePlanningPage.tsx` | `handleConfirmAgents` 接收 `mode`,存到新状态 `orchestrationMode` | 透传给 `useStep2Orchestration` | | `hooks/useStep2Orchestration.ts` | 新增 `runChainOrchestration` + `orchestrationMode` 分支 | init effect 里按 mode 选择 `runOrchestration` 或 `runChainOrchestration` | | `lib/roundtable-constants.ts` | 复用 `getAvatarTypeFor` / `SelectedAgent` | 不改,仅引用 | | `hooks/useDraftPersistence.ts` + `api/drafts.ts` | Step2 快照新增 `orchestrationMode` + `chain` 字段 | 草稿能记住「这次是链条模式」 | | `pages/WorkspaceRoutes.tsx` | 注册 `roundtable/chains` 路由 | 仿 `roundtable/planning` | | `components/page-sidebar.tsx`(如有侧栏入口) | 加「业务链条」入口(可选) | 也可只从推荐弹窗进 | | `api/chains.ts`(新增) | 链条 CRUD 客户端 | 直接打后端 `/api/roundtable-chains` | | 后端 `roundtable_chains` | 表 + 路由(MySQL) | 仿 `roundtable_drafts`,**必做** | --- ## 5. 链条持久化 API 设计 ### 5.1 前端客户端 `api/chains.ts`(直接打后端) ```ts listChains(): Promise // 当前用户全部链条 getChain(id: string): Promise createChain(input: Omit): Promise updateChain(id: string, patch: Partial): Promise deleteChain(id: string): Promise ``` - 实现用 `apiFetch` 打 `/api/roundtable-chains`(§5.2),snake_case↔camelCase 在这层收口(仿 `api/drafts.ts`)。 - 不提供 localStorage 实现;后端表是前端 UI 的硬依赖。 ### 5.2 后端表 + 路由(仿 §7A `roundtable_drafts`) - 表 `roundtable_chains`:`id`(PK) / `user_id`(idx) / `title`(512) / `description`(text) / `seats`(`PortableLongText`,存 `ChainSeat[]` JSON) / `created_at` / `updated_at`(`BeijingDateTime`)。 - 路由 `app/gateway/routers/roundtable_chains.py`,前缀 `/api/roundtable-chains`:`GET ``(列表)/ `POST ``(建)/ `GET /{id}` / `PUT /{id}`(partial,`exclude_unset`)/ `DELETE /{id}`。 - 鉴权/属主:`_current_user_id(request)`,跨用户访问一律 404,与 `roundtable_drafts` 完全一致。 - 启动 `Base.metadata.create_all` 自动建表(MySQL 生产 / sqlite 本地走同一套 SQLAlchemy 代码,`PortableLongText` 各自降级),登记进 `persistence/models/__init__.py`,store 接到 `app.state` 并 `include_router`。 > 整体是「复制 §7A 的草稿持久化、把字段换成 chain」的机械工作,风险低。 --- ## 6. Step 2 链条模式编排设计(`runChainOrchestration`) ### 6.1 与现有 `runOrchestration` 的差异 | 维度 | `runOrchestration`(recommend) | `runChainOrchestration`(chain,新增) | |------|--------------------------------|----------------------------------------| | 谁派活 | 总控 leader | 总控 leader(**不变**) | | 派给谁由谁定 | leader 自由决定 | 前端按链条游标**约束 leader 只能派给下一席位** | | 发言顺序 | leader 每轮自由派 | **链条数组的固定顺序** | | 循环结构 | `while` 直到 leader 返回 `[]` 共识 / MAX_CYCLES | 按链条游标走一趟(每位:leader 派活 → special 交付) | | 收口 | leader 返回 `[]` 即共识 | 链尾后再调一次 leader 做总结收口,产出 `lastLeaderContent` + `hasConsensus` | > **核心**:leader 始终在场,只是**每轮的派活目标被前端钉死成链条的下一席位**——前端在 leader 的 `newMessage` 里写明「本轮请通过 `agent_orchestration` 仅向【X】派活」,并对 `leaderRes.dispatched` 做命中校验/纠偏。 ### 6.2 伪代码 ``` runChainOrchestration(seedMessage, ids, coord, chainSeats): isOrchestrating = true cancel = false N = chainSeats.length for (let i = 0; i < N; i++): // 链条游标,逐位推进 if cancel: break seat = chainSeats[i] seatMeta = agentNameToRole[seat.agent_id] // ── (a) 总控派活给「链条下一席位」────────────────────────── highlightSpeaker(null) // leader 阶段不高亮任何 sub leaderBubble = appendStreamingDialogue("总控协调", confidence:"链条派活中") leaderTask = buildChainLeaderTask({ seedMessage, // 任务目标/约束(首轮) position: i, total: N, nextSeat: seat, // 「本轮请仅向【X(seat.agent_id)】派活」 isFirst: i === 0, }) leaderRes = await streamMultiAgent(leader, ids, coord, leaderTask, model=globalModel) // 约束/纠偏:取 dispatched 中命中 seat.agent_id 的那条 task; // 若 leader 没派或派错(派给非 seat、或派给多个),前端兜底: // - 命中就用它给出的 task 文本(保留总控的承上启下措辞); // - 没命中则用 leader 的正文 content 作为兜底 task,或回退到模板 task。 task = pickChainTask(leaderRes, seat) // leader 若返回 clarification → 暂停链条,走与现状一致的澄清条(§6.3) finalizeDialogue(leaderBubble, leaderRes.content, "派活说明") // ── (b) 被派席位交付(special,顺序、不并行)────────────────── markSpeakerStreaming(seatMeta.speakerId) seatBubble = appendStreamingDialogue(seatMeta.displayName, confidence:"调度中") perSeatModel = agentConfigs[seatMeta.speakerId]?.model || globalModel specRes = await streamMultiAgent(special, ids, seat.agent_id, task, model=perSeatModel) finalizeDialogue(seatBubble, specRes.content, "子智能体交付") markSpeakerIdle(seatMeta.speakerId) // 后端 _broadcast 已把本席位交付注入其余 thread 历史 → 下一席位天然可见 if cancel: return // ── (c) 总控收口:综合全链交付,产出最终结论 ────────────────── leaderBubble = appendStreamingDialogue("总控协调", confidence:"汇总链条结论") leaderRes = await streamMultiAgent(leader, ids, coord, "各链条席位已按顺序完成发言,请综合所有交付,输出最终方案,不要再派活。") finalizeDialogue(leaderBubble, leaderRes.content, "全案盖印") setLastLeaderContent(leaderRes.content) setHasConsensus(true); setConsensusPercentage(100) isOrchestrating = false ``` 要点: - **派活方仍是总控**,符合「总控按链条顺序派活」的语义;圆桌可视化里每位席位前都先出现一个总控派活气泡,再出现该席位交付气泡。 - **约束手段是 prompt + 前端校验,不动后端**:`buildChainLeaderTask` 把「本轮只能派给【X】」写进 leader 的 `newMessage`;`pickChainTask` 对 `leaderRes.dispatched` 做命中校验,派错/未派时兜底,保证链条游标严格推进。 - **复用现有三段式气泡 / 高亮 / abort 基建**(`appendStreamingDialogue` / `finalizeDialogue` / `markSpeakerStreaming` / `activeAbortersRef` / `orchestrationCancelRef`),只是驱动顺序不同。 - **special 仍是顺序**(不并行),与现状不变量一致(文件顶部注释 §1)。 - **收口 leader 调用默认开**(产出 Step 3 结论);若链条末位本身是「总结陈述」类,可关掉收口,直接拿末位交付当 `lastLeaderContent`。配置位 `chainNeedsLeaderSummary`(默认 true)。 ### 6.3 澄清 / 挂起 / 恢复 / 干预 - **澄清**:链条中途某轮 leader 返回 `clarification` → 与现状一致,暂停链条、底部弹澄清条;用户回答后从**当前游标**继续(需记录 `chainCursor`)。 - **挂起/恢复**:`isRoundTableRunning` toggle 复用现有机制;恢复时从 `chainCursor` 续跑。MVP 若不做断点续跑,可先只支持「整体停止 / 从头重跑」,UI 文案说明(扩展点,§7 阶段 4 标注)。 - **干预**:可在两席位之间插入一条用户指令,由下一轮 leader 派活前消化。MVP 同上可后置。 ### 6.4 多轮链条(扩展点,非 MVP) 单趟遍历是 MVP。若要「链条跑完后总控判断是否再来一轮」,可在收口 leader 处复用 `dispatched===[]?` 判定:收口 leader 若仍派活则回到链条头再跑一轮,直到共识或 MAX_CYCLES。MVP 不做,避免复杂度。 ### 6.5 init 复用 chain 模式的 Step 2 init **完全复用** `initMultiAgent`:`agentNames = chainSeats.map(s => s.agent_id)`(**按链条顺序**传入,使 `buildRoleTemplates` 的 seat id 顺序 = 链条顺序,且后端注入 coordinator 的「可调度席位清单」也按此序)。init effect 里按 `orchestrationMode` 决定 init 完成后调 `runOrchestration` 还是 `runChainOrchestration`。 --- ## 7. 分阶段实现计划 > 每阶段都可独立交付、独立验收。建议顺序:**数据层 → 配置页 → 弹窗模式 → 链条选择 → Step2 编排 → 持久化兼容与收尾**。 ### 阶段 0 · 后端链条表 + 脚手架(1.5 天) ✅ 已完成 **目标**:打地基(含后端持久化),不含前端 UI。 - [x] **后端**:仿 §7A `roundtable_drafts` 新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由(§5.2): - [x] `persistence/roundtable_chains/`(model + sql repo + `make_roundtable_chain_store`),登记进 `persistence/models/__init__.py`。 - [x] `app/gateway/routers/roundtable_chains.py`(GET 列表 / POST / GET{id} / PUT{id} / DELETE),鉴权属主同 `roundtable_drafts`;席位 2–8 + 去重校验。 - [x] `deps.py` 接 `app.state.roundtable_chain_store`,`app.py` `include_router`。 - [x] 后端测试 `tests/test_roundtable_chains.py`:CRUD + 部分更新 + 跨用户隔离(5 用例全过)。 - [x] **前端**:新增 `lib/business-chain.ts`(`ChainSeat` / `BusinessChain` / `SeatSelectionResult`,§3)+ `api/chains.ts`(直接打后端,§5.1)。 - [x] (决策 C)安装 `@dnd-kit/core` `@dnd-kit/sortable` `@dnd-kit/utilities`。 - **验收**:✅ 后端 5 用例通过;✅ 表注册进 `Base.metadata`、路由 `/api/roundtable-chains` 加载正常;✅ ruff 0 错;✅ 前端 typecheck 新增文件 0 错。 ### 阶段 1 · 业务链条配置页面(2–3 天) ✅ 已完成 **目标**:能创建/编辑/保存/删除链条。 - [x] 路由:`WorkspaceRoutes.tsx` 注册 `roundtable/chains`(套 `WorkspaceLayout`;本页**不需要** `ChatRuntime`,无沙箱依赖)。 - [x] 页面 `roundtable-planning/pages/BusinessChainEditorPage.tsx`: - [x] 顶部:链条列表下拉(`