405 lines
13 KiB
Markdown
405 lines
13 KiB
Markdown
# 多智能体会商 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` 结构大致是:
|
||
|
||
```ts
|
||
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 快照。
|
||
|
||
```ts
|
||
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`:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
{
|
||
id: "legacy-run",
|
||
title: "第一次生成结果",
|
||
createdAt: draft.updatedAt,
|
||
source: "initial",
|
||
...draft.step2
|
||
}
|
||
```
|
||
|
||
4. hydrate 当前 active run 到 Step2 runtime。
|
||
5. 左下角展示 run 列表。
|
||
|
||
### 2. 再次点击智能体推荐进入研讨
|
||
|
||
用户在已有历史草稿中再次点击「智能体推荐」并确认进入研讨时:
|
||
|
||
1. 先把当前页面里的 Step2 状态保存为当前 active run。
|
||
2. 创建一条新的 run:
|
||
|
||
```ts
|
||
{
|
||
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
|
||
}
|
||
```
|
||
|
||
3. 将 `activeRunId` 切到新 run。
|
||
4. 清空 Step2 runtime。
|
||
5. 设置新选中的 agents / mode / chain。
|
||
6. `resetInitGate()`,进入 Step2 后重新创建一组独立 thread。
|
||
7. 自动保存草稿。
|
||
|
||
关键点:第二次 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`
|
||
|
||
新增页面级状态:
|
||
|
||
```ts
|
||
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 继续变大:
|
||
|
||
```tsx
|
||
<Step2RunSwitcher
|
||
runs={step2Runs}
|
||
activeRunId={activeStep2RunId}
|
||
onSelect={handleSelectStep2Run}
|
||
/>
|
||
```
|
||
|
||
组件职责只做展示和选择,不直接读写 draft。
|
||
|
||
### 5. `Step3Panel.tsx`
|
||
|
||
传入当前 active run 的报告:
|
||
|
||
- 如果当前 run 有 `step3`,展示它。
|
||
- 如果没有,按现有逻辑生成新报告。
|
||
- 生成完成后通过 `onReportSaved` 写回 run。
|
||
|
||
## 兼容策略
|
||
|
||
老草稿没有 `runs` 字段时,不做数据迁移,前端加载时即时包装。
|
||
|
||
判断规则:
|
||
|
||
```ts
|
||
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 信息:
|
||
|
||
```ts
|
||
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 报告,切换不同结果线时能看到各自对应的报告。
|