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

13 KiB
Raw Permalink Blame History

多智能体会商 Step2 多结果线方案

背景

当前「多智能体会商 / 多智能体规划」里,一个历史草稿只保存一份 step2 快照。用户从历史记录加载草稿后,如果再次点击「智能体推荐」并在弹窗里点击「进入研讨」,新的 Step2 研讨会继续写入同一份 draft.step2,结果是第一次生成内容会被第二次生成内容覆盖,或者页面只能展示最新一次结果。

产品期望是:

  • 用户从历史记录进入已有草稿时,进入第二步后,左下角能看到第一次生成结果。
  • 用户再次点击「智能体推荐」并点击「进入研讨」时,要开启一条新的研讨结果线。
  • 第二次生成结果显示在左下角,但不能影响第一次生成结果。
  • 用户之后再次查看同一个历史草稿时,可以分别查看第一次生成结果和第二次生成结果。

因此,这个需求的核心不是简单增加一个展示区域,而是把 Step2 从「单结果」升级为「一个草稿下多条研讨 run」。

现状

相关位置:

  • src/roundtable-planning/api/drafts.ts
  • src/roundtable-planning/pages/RoundtablePlanningPage.tsx
  • src/roundtable-planning/hooks/useStep2Orchestration.ts
  • src/roundtable-planning/components/Step2Panel.tsx
  • src/roundtable-planning/components/RecommendAgentsDialog.tsx

当前 DraftStep2Snapshot 结构大致是:

export interface DraftStep2Snapshot {
  selectedAgents?: Array<{ agent_id: string; name: string }>;
  threadIds: ThreadIdMap | null;
  coordinatorName: string | null;
  step2RoundtableDialogues: unknown[];
  lastLeaderContent: string;
  hasConsensus: boolean;
  consensusPercentage: number;
  budgetLimit: number;
  seatStubMessages?: Record<string, unknown[]>;
  orchestrationMode?: "recommend" | "chain";
  chain?: { id: string; title: string } | null;
}

这意味着一个草稿只有一份:

  • 参会智能体
  • 多智能体 thread 映射
  • 研讨气泡
  • 总控最终内容
  • 共识状态
  • 沙箱产物 stub
  • 编排模式和业务链条

当前 RoundtablePlanningPage.tsx 里的 handleConfirmAgents 在用户点击「进入研讨」后会:

  1. 规范化选中的 agents。
  2. 写入 step2.selectedAgents。
  3. 调用 step2.resetInitGate()。
  4. 设置 orchestrationMode / chainMeta。
  5. 切到 currentStep = 2。

它没有先把当前 Step2 结果归档成「第一次生成结果」,也没有为新的研讨创建独立 run。因此第二次研讨天然会复用同一个 draft.step2 写入位置。

目标模型

建议引入 Step2 run 概念:一个草稿可以有多条 Step2 结果线,每条结果线保存一份完整的 Step2 快照。

interface Step2RunSnapshot {
  id: string;
  title: string;
  createdAt: string;
  source: "initial" | "recommend" | "chain" | "manual";
  selectedAgents?: Array<{ agent_id: string; name: string }>;
  threadIds: ThreadIdMap | null;
  coordinatorName: string | null;
  step2RoundtableDialogues: unknown[];
  lastLeaderContent: string;
  hasConsensus: boolean;
  consensusPercentage: number;
  budgetLimit: number;
  seatStubMessages?: Record<string, unknown[]>;
  orchestrationMode?: "recommend" | "chain";
  chain?: { id: string; title: string } | null;
  step3?: DraftStep3Snapshot | null;
}

DraftStep2Snapshot 保留现有字段作为当前激活 run 的镜像,同时新增 runs 和 activeRunId:

export interface DraftStep2Snapshot {
  activeRunId?: string;
  runs?: Step2RunSnapshot[];

  selectedAgents?: Array<{ agent_id: string; name: string }>;
  threadIds: ThreadIdMap | null;
  coordinatorName: string | null;
  step2RoundtableDialogues: unknown[];
  lastLeaderContent: string;
  hasConsensus: boolean;
  consensusPercentage: number;
  budgetLimit: number;
  seatStubMessages?: Record<string, unknown[]>;
  orchestrationMode?: "recommend" | "chain";
  chain?: { id: string; title: string } | null;
}

这样做的好处:

  • 老代码仍然可以从 draft.step2.step2RoundtableDialogues 读取当前结果。
  • 新 UI 可以从 draft.step2.runs 展示第一次、第二次、第三次生成结果。
  • 老草稿没有 runs 时,可以在 hydrate 时把原来的 step2 包装成一条默认 run。
  • 暂时不需要改数据库表结构,因为 step2 本身就是 JSON 快照。

行为设计

1. 历史记录加载

用户点击历史记录加载草稿时:

  1. 拉取完整草稿。
  2. 如果 draft.step2.runs 存在,使用 activeRunId 找到当前结果线。
  3. 如果 draft.step2.runs 不存在,但旧字段里有 step2RoundtableDialogues,自动包装成一条 run:
{
  id: "legacy-run",
  title: "第一次生成结果",
  createdAt: draft.updatedAt,
  source: "initial",
  ...draft.step2
}
  1. hydrate 当前 active run 到 Step2 runtime。
  2. 左下角展示 run 列表。

2. 再次点击智能体推荐进入研讨

用户在已有历史草稿中再次点击「智能体推荐」并确认进入研讨时:

  1. 先把当前页面里的 Step2 状态保存为当前 active run。
  2. 创建一条新的 run:
{
  id: crypto.randomUUID(),
  title: `第 ${runs.length + 1} 次生成结果`,
  createdAt: new Date().toISOString(),
  source: result.mode,
  selectedAgents: normalizedAgents,
  threadIds: null,
  coordinatorName: null,
  step2RoundtableDialogues: [],
  lastLeaderContent: "",
  hasConsensus: false,
  consensusPercentage: 0,
  budgetLimit: DEFAULT_BUDGET,
  seatStubMessages: {},
  orchestrationMode: result.mode,
  chain: result.mode === "chain" ? result.chain ?? null : null
}
  1. 将 activeRunId 切到新 run。
  2. 清空 Step2 runtime。
  3. 设置新选中的 agents / mode / chain。
  4. resetInitGate(),进入 Step2 后重新创建一组独立 thread。
  5. 自动保存草稿。

关键点:第二次 run 必须使用新的 threadIds,不能沿用第一次 run 的 threadIds,否则后端 thread 历史会混在一起。

3. 左下角结果列表

左下角建议展示一个「生成结果」区域:

  • 第一次生成结果
  • 第二次生成结果
  • 第三次生成结果

每条展示:

  • 标题
  • 创建时间
  • 状态:研讨中 / 已达成共识 / 未完成 / 已生成报告
  • 使用模式:智能体推荐 / 业务链条

点击某条结果时:

  1. 保存当前 active run。
  2. 切换 activeRunId。
  3. hydrate 被点击的 run。
  4. 中间 Step2 对话区显示该 run 的研讨气泡。
  5. 如果该 run 已有 step3,进入 Step3 时展示对应报告。

4. Step3 结果归属

目前 draft.step3 是草稿级别的一份报告。如果只保留这一份,第二次结果生成报告后仍会覆盖第一次报告。

建议:

  • draft.step3 继续作为当前 active run 的镜像,兼容现有 Step3 页面。
  • 每条 Step2RunSnapshot.step3 保存自己的报告。
  • onReportSaved 时同时写入当前 active run 的 step3。

这样用户切换到第一次生成结果时,可以看到第一次的 Step3 报告;切换到第二次生成结果时,可以看到第二次的 Step3 报告。

建议改动点

1. api/drafts.ts

扩展类型:

  • 新增 Step2RunSnapshot。
  • DraftStep2Snapshot 增加 activeRunId?: string 和 runs?: Step2RunSnapshot[]。

同时保留旧字段,避免影响现有保存、加载和 Step3。

2. useStep2Orchestration.ts

新增或暴露两个能力:

  • getCurrentRunSnapshot():从当前 Step2 runtime 生成一条 run 快照。
  • hydrateRunSnapshot(run):把某条 run 恢复到 Step2 runtime。

hydrateFromDraft 继续存在,但内部可以优先选择 active run 来 hydrate。

3. RoundtablePlanningPage.tsx

新增页面级状态:

const [step2Runs, setStep2Runs] = useState<Step2RunSnapshot[]>([]);
const [activeStep2RunId, setActiveStep2RunId] = useState<string | null>(null);

调整几个流程:

  • getStep2Snapshot 输出 runs 和 activeRunId。
  • hydrateStep2 兼容旧草稿并恢复 runs。
  • handleConfirmAgents 在新建研讨前先归档当前 active run,再创建新 run。
  • onReportSaved 把 Step3 报告写回当前 active run。

4. Step2Panel.tsx

新增左下角结果列表展示。建议作为一个独立组件,避免 Step2Panel 继续变大:

<Step2RunSwitcher
  runs={step2Runs}
  activeRunId={activeStep2RunId}
  onSelect={handleSelectStep2Run}
/>

组件职责只做展示和选择,不直接读写 draft。

5. Step3Panel.tsx

传入当前 active run 的报告:

  • 如果当前 run 有 step3,展示它。
  • 如果没有,按现有逻辑生成新报告。
  • 生成完成后通过 onReportSaved 写回 run。

兼容策略

老草稿没有 runs 字段时,不做数据迁移,前端加载时即时包装。

判断规则:

function normalizeStep2Runs(step2: DraftStep2Snapshot): {
  runs: Step2RunSnapshot[];
  activeRunId: string | null;
} {
  if (step2.runs?.length) {
    return {
      runs: step2.runs,
      activeRunId: step2.activeRunId ?? step2.runs[0].id,
    };
  }

  const hasLegacyContent =
    step2.step2RoundtableDialogues?.length > 0 ||
    Boolean(step2.threadIds) ||
    Boolean(step2.lastLeaderContent);

  if (!hasLegacyContent) {
    return { runs: [], activeRunId: null };
  }

  const legacyRun: Step2RunSnapshot = {
    id: "legacy-run",
    title: "第一次生成结果",
    createdAt: new Date().toISOString(),
    source: step2.orchestrationMode ?? "initial",
    selectedAgents: step2.selectedAgents,
    threadIds: step2.threadIds,
    coordinatorName: step2.coordinatorName,
    step2RoundtableDialogues: step2.step2RoundtableDialogues,
    lastLeaderContent: step2.lastLeaderContent,
    hasConsensus: step2.hasConsensus,
    consensusPercentage: step2.consensusPercentage,
    budgetLimit: step2.budgetLimit,
    seatStubMessages: step2.seatStubMessages,
    orchestrationMode: step2.orchestrationMode,
    chain: step2.chain,
  };

  return { runs: [legacyRun], activeRunId: legacyRun.id };
}

风险点

1. 自动保存覆盖

Step2 现在会在每个气泡收尾时自动保存。如果引入多 run,自动保存必须先把当前 runtime 合并回 active run,再写入 draft.step2.runs。

否则会出现 UI 切到第二次结果,但保存时仍更新第一次 run 的情况。

2. threadIds 混用

每条 run 必须拥有自己的 threadIds。新建 run 时要清空 threadIds 并让 Step2 重新 init。

如果复用旧 thread,会导致两次研讨的后端上下文互相污染。

3. Step3 报告覆盖

如果只保存草稿级 step3,多 run 的报告无法分别查看。

建议把报告写入当前 run,同时保留草稿级 step3 作为 active run 镜像。

4. 正在运行中的 run

如果配合后台任务能力,run 还需要带 job 信息:

jobId?: string | null;
jobStatus?: JobStatus | null;

这样左下角可以显示「第二次生成结果正在运行」,并打开派活链路弹窗。

分阶段实现建议

P0:纯前端多 run 快照

目标:先解决“第一次 / 第二次结果分别可查看”。

  • 扩展 DraftStep2Snapshot 类型。
  • 页面维护 step2Runs / activeStep2RunId。
  • handleConfirmAgents 新建 run。
  • 左下角展示 run 列表。
  • 切换 run 能恢复不同 Step2 对话。

不改后端表结构。

P1:Step3 绑定 run

目标:每次研讨结果对应自己的报告。

  • Step2RunSnapshot.step3 保存报告。
  • onReportSaved 写回 active run。
  • 切换 run 时同步 Step3 显示。

P2:后台任务绑定 run

目标:和后台挂起执行方案打通。

  • run 增加 jobId / jobStatus。
  • 历史列表和左下角都能显示某条 run 正在执行。
  • 点击正在执行的 run 打开派活链路弹窗。

P3:后端 run 表

如果后续一个草稿下 run 数量很多,或者需要服务端按 run 查询、删除、重命名,再考虑新增后端表:

  • roundtable_runs
  • draft_id
  • run_id
  • title
  • status
  • step2_snapshot
  • step3_snapshot
  • created_at
  • updated_at

短期不建议一开始就上表,先用现有 step2 JSON 快照落地更快。

验收清单

  • 从历史记录加载旧草稿时,左下角出现「第一次生成结果」。
  • 点击「第一次生成结果」能看到原来的 Step2 研讨气泡。
  • 在历史草稿中重新点击「智能体推荐」并「进入研讨」后,左下角新增「第二次生成结果」。
  • 第二次研讨开始时,Step2 对话区为空或只显示新 run 初始化内容。
  • 第二次 run 使用新的 threadIds,不会复用第一次 run 的后端线程。
  • 切回「第一次生成结果」时,第一次的对话、共识状态、沙箱产物仍可查看。
  • 切回「第二次生成结果」时,第二次的对话继续显示。
  • 刷新页面后再次加载该历史草稿,两条结果线都还在。
  • 如果两次都生成了 Step3 报告,切换不同结果线时能看到各自对应的报告。