第三步结构化输出优化
This commit is contained in:
1 parent
fca39eeaee
commit
d3880f51b3
10 files changed
+1095
-127
No files matched your search
@@ -40,7 +40,8 @@ import type {
|
||||
Step2Dialogue,
|
||||
StubAiMessage,
|
||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import { detectFlowIntent, hasFlowFence } from "@/roundtable-planning/lib/flow-graph";
|
||||
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
||||
import { streamFlowExtract } from "@/roundtable-planning/api/flow-extract";
|
||||
import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
|
||||
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
|
||||
@@ -79,6 +80,14 @@ export interface UseStep3SummaryReturn {
|
||||
error: string | null;
|
||||
/** taskId/深链模式:md 报告生成后自动续跑流程图(组件据此让流程图画布自动聚焦)。 */
|
||||
autoFlow: boolean;
|
||||
/** 后端「抽取+校验」得到的校验后结构化流程图(Phase 1)。done 时据此绘制。 */
|
||||
flowGraph: FlowGraphData | null;
|
||||
/** 流程图生成阶段:idle / extracting(抽取流式) / validating(校验中) / done / error。 */
|
||||
flowPhase: "idle" | "extracting" | "validating" | "done" | "error";
|
||||
/** 仅重新生成流程图(复用当前 md/席位交付,不重跑报告)——模型挂了切模型后重试用。 */
|
||||
regenerateFlow: () => void;
|
||||
/** 手动编辑流程图(Phase 3:画布上改/删节点后回写 + 持久化)。 */
|
||||
setFlowGraph: (next: FlowGraphData) => void;
|
||||
summaryThreadId: string | null;
|
||||
dialogues: Step2Dialogue[];
|
||||
seatStubMessages: Record<string, StubAiMessage[]>;
|
||||
@@ -220,61 +229,9 @@ const FLOW_STRUCTURE_RULE =
|
||||
"⑥ **只画**这些层级实体节点;**不要**把『综合分析』『核心洞察』『分层传播策略 / 执行路线 / 建议』『结论』" +
|
||||
"以及『阶段①②③④』等章节名 / 阶段名 / 汇总段落画成节点。";
|
||||
|
||||
/** 把各席位交付拼成一段内联材料(无报告时作流程图的数据来源)。 */
|
||||
function inlineSeatDeliveries(seatDeliveries?: { sender: string; content: string }[]): string {
|
||||
const list = (seatDeliveries ?? []).filter((s) => (s.content ?? "").trim());
|
||||
if (list.length === 0) return "";
|
||||
return list
|
||||
.map((s, i) => `## 席位${i + 1}:${s.sender || "(未具名席位)"}\n${s.content.trim()}`)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* taskId/深链模式:md 报告生成完后自动续跑的「结构化输出」prompt。
|
||||
*
|
||||
* 数据基准(用户拍板):**有报告就基于报告**(报告已把各席位交付清晰总结过,直接提取实体、
|
||||
* 不要再自己重新总结一遍);没有报告则基于**各席位交付产物**。两种来源都**整段内联**进 prompt
|
||||
* (不依赖线程上下文),让模型逐条对着抽取节点。层级顺序优先用总控提示词里写明的层级,否则 chainLevels。
|
||||
*/
|
||||
function buildAutoFlowPrompt(opts: {
|
||||
chainLevels?: string[];
|
||||
coordinatorPrompt?: string | null;
|
||||
reportMd?: string;
|
||||
seatDeliveries?: { sender: string; content: string }[];
|
||||
}): string {
|
||||
const levels = (opts.chainLevels ?? []).map((s) => s.trim()).filter(Boolean);
|
||||
const cp = opts.coordinatorPrompt?.trim();
|
||||
const report = opts.reportMd?.trim() ?? "";
|
||||
const seatMaterial = report ? "" : inlineSeatDeliveries(opts.seatDeliveries);
|
||||
const parts: string[] = [];
|
||||
if (report) {
|
||||
parts.push(
|
||||
"本轮只画一张流程图。**数据来源 = 下面这份你已写好的《方案总结报告》**(它已把各席位交付清晰总结过)——" +
|
||||
"请**逐条对着报告抽取**已有的层级条目作为流程图节点,**不要再自己重新总结 / 概括 / 提炼**,也不要凭空新增报告里没有的内容。",
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
"本轮只画一张流程图。**数据来源 = 下面各席位的交付产物**——请**逐条对着这些交付抽取**已有的层级条目作为流程图节点,不要自己另起总结。",
|
||||
);
|
||||
}
|
||||
parts.push(FLOW_ONLY_RULE);
|
||||
// 整段内联数据来源(报告全文 / 席位交付),用分隔线包起来便于模型对照抽取。
|
||||
if (report) {
|
||||
parts.push("===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 =====");
|
||||
} else if (seatMaterial) {
|
||||
parts.push("===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + seatMaterial + "\n===== 交付结束 =====");
|
||||
}
|
||||
parts.push(FLOW_JSON_SHAPE);
|
||||
parts.push(FLOW_STRUCTURE_RULE);
|
||||
if (cp) {
|
||||
parts.push("层级顺序优先采用「业务链总控编排提示」里写明的层级链(如它写的 任务 → 目的 → 行为体 → …)。");
|
||||
} else if (levels.length > 0) {
|
||||
parts.push(`层级顺序(自上而下):任务 → ${levels.join(" → ")}。`);
|
||||
}
|
||||
// 收尾再强调(首尾夹击,对抗线程里 coordinatorPrompt 的报告锚定)。
|
||||
parts.push("再次强调:本轮不写任何 Markdown 报告正文 / 章节,整个回复除一句话说明外,只有一个完整的 ```flow-json 代码块。");
|
||||
return parts.join("\n");
|
||||
}
|
||||
// 注:原 buildAutoFlowPrompt(在总结线程里续问吐 flow-json)已废弃 —— Phase 1 起自动结构化
|
||||
// 输出改走后端独立端点 /api/flow-extract/stream(streamFlowExtract),抽取+校验在后端做。
|
||||
// FLOW_ONLY_RULE / FLOW_STRUCTURE_RULE / FLOW_JSON_SHAPE 仍由手动 FLOW_ASK_INSTRUCTION 引用。
|
||||
|
||||
/** 首轮总结 prompt。`flowMode`=编排提示里明确要求了结构化输出/流程图 → 跳过 md 文件,直出流程图。 */
|
||||
function buildSummaryPrompt(opts: {
|
||||
@@ -452,6 +409,14 @@ export function useStep3Summary(
|
||||
|
||||
const [phase, setPhase] = useState<SummaryPhase>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// 后端「抽取+校验」流程图(Phase 1):与 SummaryPhase 解耦(md 报告完成后才跑流程图)。
|
||||
const [flowGraph, setFlowGraphState] = useState<FlowGraphData | null>(null);
|
||||
const [flowPhase, setFlowPhase] = useState<"idle" | "extracting" | "validating" | "done" | "error">("idle");
|
||||
// 镜像 flowGraph 供 ask()(问答修改)在 render 外读到最新图作 edit 入参。
|
||||
const flowGraphRef = useRef<FlowGraphData | null>(null);
|
||||
useEffect(() => {
|
||||
flowGraphRef.current = flowGraph;
|
||||
}, [flowGraph]);
|
||||
const [summaryThreadId, setSummaryThreadId] = useState<string | null>(null);
|
||||
const [dialogues, setDialogues] = useState<Step2Dialogue[]>([]);
|
||||
const [seatStubMessages, setSeatStubMessages] = useState<
|
||||
@@ -1010,6 +975,110 @@ export function useStep3Summary(
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Phase 1:后端「抽取+校验」生成流程图。独立调 `/api/flow-extract/stream`(不进总结线程,
|
||||
* 避开 coordinator_prompt 的报告锚定):抽取阶段把 JSON 流式吐进对话气泡(用户实时看),
|
||||
* 校验完成后用校验后的结构化图绘制画布(done)。模型沿用第三步所选(selectedModel)。
|
||||
*/
|
||||
const runFlowExtract = useCallback(
|
||||
async (
|
||||
aborter: AbortController,
|
||||
opts?: { instruction?: string; currentGraph?: FlowGraphData | null },
|
||||
): Promise<void> => {
|
||||
const isEdit = !!opts?.instruction && !!opts?.currentGraph;
|
||||
setFlowPhase("extracting");
|
||||
if (!isEdit) setFlowGraphState(null); // 编辑模式保留旧图直到新图就绪(防画布闪空)
|
||||
// 两个气泡:抽取/修改 + 校验——把过程(思考 `<think>` + 文字 + flow-json 代码块)都留着展示,
|
||||
// 由 Step2Message 渲染(思考折叠块 + markdown 代码块),不折叠成一句话。
|
||||
const extractBubbleId = appendStreamingDialogue({
|
||||
sender: isEdit ? "结构化流程图 · 修改" : "结构化流程图 · 抽取",
|
||||
avatarType: "blue",
|
||||
});
|
||||
let validateBubbleId: number | null = null;
|
||||
try {
|
||||
const graph = await streamFlowExtract({
|
||||
reportMd: currentMdRef.current,
|
||||
seatDeliveries,
|
||||
chainLevels,
|
||||
coordinatorPrompt,
|
||||
model: selectedModel || undefined,
|
||||
currentGraph: opts?.currentGraph ?? null,
|
||||
instruction: opts?.instruction,
|
||||
signal: aborter.signal,
|
||||
onExtractDelta: (t) => appendToDialogue(extractBubbleId, t),
|
||||
onValidating: () => {
|
||||
setFlowPhase("validating");
|
||||
// 抽取气泡收尾(保留已流出的内容),另起校验气泡。
|
||||
finalizeDialogue(extractBubbleId, "", "抽取完成");
|
||||
validateBubbleId = appendStreamingDialogue({ sender: "结构化流程图 · 校验", avatarType: "blue" });
|
||||
},
|
||||
onValidateDelta: (t) => {
|
||||
if (validateBubbleId != null) appendToDialogue(validateBubbleId, t);
|
||||
},
|
||||
});
|
||||
if (aborter.signal.aborted) return;
|
||||
setFlowGraphState(graph);
|
||||
setFlowPhase("done");
|
||||
flowActiveRef.current = true;
|
||||
// 收尾两个气泡(保留过程内容)。校验后的图序列化进草稿快照 closing,恢复时重建画布。
|
||||
if (validateBubbleId != null) finalizeDialogue(validateBubbleId, "", "流程图完成");
|
||||
else finalizeDialogue(extractBubbleId, "", "流程图完成");
|
||||
onSummarySavedRef.current?.({
|
||||
md: currentMdRef.current,
|
||||
threadId: liveThreadRef.current ?? "",
|
||||
generatedAt: new Date().toISOString(),
|
||||
model: selectedModel || "",
|
||||
closing: serializeFlowGraph(graph),
|
||||
});
|
||||
} catch (err) {
|
||||
if (aborter.signal.aborted) return;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setFlowPhase("error");
|
||||
finalizeDialogue(
|
||||
validateBubbleId ?? extractBubbleId,
|
||||
`(流程图生成失败) ${msg}\n模型可能不稳定——可在上方切换模型后点「重新生成流程图」重试。`,
|
||||
"流程图失败",
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
seatDeliveries,
|
||||
chainLevels,
|
||||
coordinatorPrompt,
|
||||
selectedModel,
|
||||
appendStreamingDialogue,
|
||||
appendToDialogue,
|
||||
finalizeDialogue,
|
||||
],
|
||||
);
|
||||
|
||||
/** 仅重新生成流程图(不重跑 md 报告):模型挂了切模型后重试 / 手动重画。 */
|
||||
const regenerateFlow = useCallback(() => {
|
||||
if (!currentMdRef.current.trim() && seatDeliveries.length === 0) return;
|
||||
aborterRef.current?.abort();
|
||||
const aborter = new AbortController();
|
||||
aborterRef.current = aborter;
|
||||
void runFlowExtract(aborter);
|
||||
}, [runFlowExtract, seatDeliveries]);
|
||||
|
||||
/** Phase 3:画布手动改/删节点后回写图 + 持久化(序列化进草稿快照)。 */
|
||||
const updateFlowGraph = useCallback(
|
||||
(next: FlowGraphData) => {
|
||||
setFlowGraphState(next);
|
||||
setFlowPhase("done");
|
||||
flowActiveRef.current = true;
|
||||
const serialized = serializeFlowGraph(next);
|
||||
onSummarySavedRef.current?.({
|
||||
md: currentMdRef.current,
|
||||
threadId: liveThreadRef.current ?? "",
|
||||
generatedAt: new Date().toISOString(),
|
||||
model: selectedModel || "",
|
||||
closing: serialized,
|
||||
});
|
||||
},
|
||||
[selectedModel],
|
||||
);
|
||||
|
||||
const run = useCallback(async () => {
|
||||
if (runningRef.current) return;
|
||||
runningRef.current = true;
|
||||
@@ -1021,6 +1090,8 @@ export function useStep3Summary(
|
||||
currentMdRef.current = "";
|
||||
liveThreadRef.current = null;
|
||||
setSeatStubMessages({});
|
||||
setFlowGraphState(null);
|
||||
setFlowPhase("idle");
|
||||
|
||||
const aborter = new AbortController();
|
||||
aborterRef.current = aborter;
|
||||
@@ -1044,33 +1115,16 @@ export function useStep3Summary(
|
||||
});
|
||||
await streamOnce({ bubbleId, prompt, aborter, flowRound: flowMode });
|
||||
|
||||
// taskId/深链模式:报告生成完,**自动**续跑一轮结构化输出(流程图)。
|
||||
// 数据来源:有报告则内联报告全文、否则内联各席位交付(buildAutoFlowPrompt 处理)。
|
||||
// 故触发条件放宽为「有报告 **或** 有席位交付」——保证「无报告也能据席位交付出图」。
|
||||
// 复用同一真线程,与手动「结构化输出」走同一渲染链路。
|
||||
// taskId/深链模式:报告生成完,**自动**用后端「抽取+校验」端点生成流程图(Phase 1)。
|
||||
// 不再在总结线程里续问吐 flow-json(那会被 coordinator_prompt 的报告锚定带偏、不稳);
|
||||
// 改走独立端点:流式抽取(实时显示) → 校验修复 → 用校验后结构化图绘制。
|
||||
// 有报告或有席位交付即触发(无报告也能据席位交付出图)。
|
||||
if (
|
||||
autoFlow &&
|
||||
!aborter.signal.aborted &&
|
||||
liveThreadRef.current &&
|
||||
(currentMdRef.current.trim() || seatDeliveries.length > 0)
|
||||
) {
|
||||
const flowBubbleId = appendStreamingDialogue({
|
||||
sender: "方案总结报告智能体",
|
||||
avatarType: "blue",
|
||||
});
|
||||
const flowPrompt = buildAutoFlowPrompt({
|
||||
chainLevels,
|
||||
coordinatorPrompt,
|
||||
reportMd: currentMdRef.current,
|
||||
seatDeliveries,
|
||||
});
|
||||
await streamOnce({
|
||||
bubbleId: flowBubbleId,
|
||||
prompt: flowPrompt,
|
||||
aborter,
|
||||
seedThreadId: liveThreadRef.current,
|
||||
flowRound: true,
|
||||
});
|
||||
await runFlowExtract(aborter);
|
||||
}
|
||||
} catch (err) {
|
||||
if (aborter.signal.aborted) return;
|
||||
@@ -1087,11 +1141,11 @@ export function useStep3Summary(
|
||||
seatDeliveries,
|
||||
consensusText,
|
||||
coordinatorPrompt,
|
||||
chainLevels,
|
||||
autoFlow,
|
||||
appendStreamingDialogue,
|
||||
finalizeDialogue,
|
||||
streamOnce,
|
||||
runFlowExtract,
|
||||
]);
|
||||
|
||||
const ask = useCallback(
|
||||
@@ -1106,6 +1160,25 @@ export function useStep3Summary(
|
||||
abort();
|
||||
if (wasBusy) freezeStreaming("已打断");
|
||||
|
||||
// 流程图相关消息 → 走后端「抽取/编辑 + 校验」端点(流式显示过程 → 校验后绘制),
|
||||
// 不再在总结线程吐 flow-json 做实时绘制。判定:
|
||||
// · 已有流程图 且(显式 flow 关键词 或 上一轮主产物是流程图)→ **问答修改**(带当前图+指令,Phase 2);
|
||||
// · 还没有流程图 但显式要流程图 → **首次生成**。
|
||||
// 普通报告修改 / 问答(无 flow 上下文)仍走下方总结线程路径。
|
||||
const flowIntentNow = detectFlowIntent(text);
|
||||
const curGraph = flowGraphRef.current;
|
||||
if (flowIntentNow || (curGraph && flowActiveRef.current)) {
|
||||
appendUserDialogue(text);
|
||||
const flowAborter = new AbortController();
|
||||
aborterRef.current = flowAborter;
|
||||
if (curGraph) {
|
||||
await runFlowExtract(flowAborter, { instruction: text, currentGraph: curGraph });
|
||||
} else {
|
||||
await runFlowExtract(flowAborter);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 意图引导分三档(智能体按语义最终裁决,前端只给上下文,不做硬分类):
|
||||
// ① 显式命中结构化/流程图关键词 → 直出 flow-json、不写 md;
|
||||
// ② 没命中关键词但上一轮主产物是流程图 → 流程图上下文引导(笼统修改默认改图,太模糊则反问);
|
||||
@@ -1219,6 +1292,7 @@ export function useStep3Summary(
|
||||
appendUserDialogue,
|
||||
appendStreamingDialogue,
|
||||
streamOnce,
|
||||
runFlowExtract,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1247,6 +1321,11 @@ export function useStep3Summary(
|
||||
(snap: DraftStep3SummarySnapshot) => {
|
||||
abort();
|
||||
runningRef.current = false;
|
||||
// 恢复流程图:快照 closing 里若存了 flow-json,解析成结构化图填进 flowGraph 状态,
|
||||
// 让画布直接用它绘制(也供 Phase 3 手动编辑)。解析不出则回退到对话气泡解析。
|
||||
const restoredGraph = snap.closing ? extractFlowGraph(snap.closing) : null;
|
||||
setFlowGraphState(restoredGraph);
|
||||
setFlowPhase(restoredGraph ? "done" : "idle");
|
||||
const hasMd = snap.md.trim().length > 0;
|
||||
const realThreadId = snap.threadId?.trim() || null;
|
||||
const synthThreadId = realThreadId ?? "roundtable-summary-restored";
|
||||
@@ -1330,6 +1409,10 @@ export function useStep3Summary(
|
||||
phase,
|
||||
error,
|
||||
autoFlow: !!autoFlow,
|
||||
flowGraph,
|
||||
flowPhase,
|
||||
regenerateFlow,
|
||||
setFlowGraph: updateFlowGraph,
|
||||
summaryThreadId,
|
||||
dialogues,
|
||||
seatStubMessages,
|
||||
|
||||
Reference in new issue
Block a user