feat(roundtable): xdfx 行动名喂入 summary/抽取 + 第三步意图理解增强

- xdfx:把本次「行动名称」喂进 summary 材料(buildMaterials 加 actionName) 与结构化抽取
  材料(flow_extract._material 加【本次行动】抬头),让报告/流程图都贴着该行动展开
- 第三步意图识别(classify_flow_intent)增强:新增 can_ingest 上下文 + ingest 意图;重写
  _INTENT_SYS 走语义理解——「介绍/解释/为什么/总结一下」等对内容的提问→qa(文字答);
  「帮我入库」(可入库时)→ingest→直接入库;能判断就给具体意图不轻易 unclear;
  不可入库时误判 ingest 自动降级 qa。前端 classifyFlowIntent 传 canIngest,ask 新增 ingest 分支

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-20 20:39:24 +08:00
1 parent fba417918f
commit ae2706dad3
3 files changed
+73 -21

No files matched your search

@@ -12,7 +12,7 @@ import { getBackendBaseURL } from "@/core/config";
import type { FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
/** 第三步输入框意图分类结果(与后端 /intent 对应)。 */
export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "qa" | "unclear";
export type FlowIntent = "regenerate_report" | "modify_flow" | "regenerate_flow" | "ingest" | "qa" | "unclear";
/**
* 识别第三步输入框消息的意图(一次模型调用)。失败回退 "unclear"(前端发起澄清)。
@@ -22,6 +22,8 @@ export async function classifyFlowIntent(req: {
message: string;
hasReport: boolean;
hasFlow: boolean;
/** 当前是否可入库(入库按钮已出现、未入库);为真才允许识别出 ingest 意图。 */
canIngest?: boolean;
model?: string;
signal?: AbortSignal;
}): Promise<{ intent: FlowIntent; reason: string }> {
@@ -33,6 +35,7 @@ export async function classifyFlowIntent(req: {
message: req.message,
has_report: req.hasReport,
has_flow: req.hasFlow,
can_ingest: req.canIngest ?? false,
model: req.model,
}),
signal: req.signal,
@@ -180,10 +180,19 @@ function buildMaterials(opts: {
seatDeliveries: ReportSeatDelivery[];
consensusText: string;
coordinatorPrompt?: string | null;
/** xdfx:本次「行动名称」,作为材料抬头喂进总结(报告围绕该行动展开)。 */
actionName?: string | null;
}): string {
const { intentReady, seatDeliveries, consensusText, coordinatorPrompt } = opts;
const { intentReady, seatDeliveries, consensusText, coordinatorPrompt, actionName } = opts;
const parts: string[] = [];
const an = actionName?.trim();
if (an) {
parts.push("# 本次行动");
parts.push(an);
parts.push("");
}
const cp = coordinatorPrompt?.trim();
if (cp) {
parts.push("# 业务链总控编排提示(最高优先级,按它的关注点/结构/口径来组织报告)");
@@ -295,6 +304,7 @@ function buildSummaryPrompt(opts: {
seatDeliveries: ReportSeatDelivery[];
consensusText: string;
coordinatorPrompt?: string | null;
actionName?: string | null;
flowMode?: boolean;
}): string {
const parts: string[] = [];
@@ -1262,6 +1272,9 @@ export function useStep3Summary(
seatDeliveries,
consensusText,
coordinatorPrompt,
// xdfx(7BF):把本次行动名称喂进总结材料,让报告围绕该行动展开。
actionName:
ingestBusinessCodeRef.current === "7BF" ? readActionDetail()?.actionName ?? null : null,
flowMode,
});
await streamOnce({ bubbleId, prompt, aborter, flowRound: flowMode });
@@ -1518,10 +1531,17 @@ export function useStep3Summary(
if (!intent) {
// **立刻**显示「智能体调度中」气泡(不等意图接口返回),消除连接卡顿时的"卡住"感。
dispatchBubbleId = appendStreamingDialogue({ sender: "智能体", avatarType: "blue" });
// 入库可用(按钮已出现、未入库 / 未在跑)时把 canIngest 传给意图识别——让「帮我入库」被识别为
// ingest 而不是落到 qa/unclear(之前主分类器不懂入库,只有「稍后入库待命」时才识别入库,是个缺口)。
const canIngestNow =
ingestAvailableRef.current &&
ingestStatusRef.current !== "running" &&
ingestStatusRef.current !== "done";
const r = await classifyFlowIntent({
message: text,
hasReport: !!currentMdRef.current.trim(),
hasFlow: !!flowGraphRef.current,
canIngest: canIngestNow,
model: selectedModel || undefined,
});
intent = r.intent;
@@ -1533,6 +1553,12 @@ export function useStep3Summary(
setPendingFlowClarification(text); // 组件渲染选项让用户自己定
return;
}
if (intent === "ingest") {
// 识别为入库(后端已保证仅 can_ingest 时才会返回 ingest)→ 直接入库;复用调度气泡。
if (dispatchBubbleId != null) finalizeDialogue(dispatchBubbleId, "正在入库…", "入库中");
await runIngest();
return;
}
if (intent === "regenerate_flow" || intent === "modify_flow") {
const flowAborter = new AbortController();
aborterRef.current = flowAborter;
@@ -78,11 +78,14 @@ class FlowIntentRequest(BaseModel):
message: str = ""
has_report: bool = False
has_flow: bool = False
# 当前是否可入库(结构化抽取智能体已判定有适配本场景的入库技能、入库按钮已出现且未入库)。
# 为真时才允许识别出 ingest 意图——否则「帮我入库」回退为 qa/unclear。
can_ingest: bool = False
model: str | None = Field(default=None)
# 意图枚举(与前端路由一一对应)。
_INTENTS = {"regenerate_report", "modify_flow", "regenerate_flow", "qa", "unclear"}
# 意图枚举(与前端路由一一对应)。ingest 仅在 can_ingest=true 时可用。
_INTENTS = {"regenerate_report", "modify_flow", "regenerate_flow", "ingest", "qa", "unclear"}
# ── flow-json 容错解析(Python 版,对齐前端 lib/flow-graph.ts)──────────────
@@ -434,13 +437,16 @@ _STRUCTURE = (
def _material(req: FlowExtractRequest) -> str:
# xdfx:把本次「行动名称」作为材料抬头喂进去(根节点就是它),让抽取贴着这条行动展开。
action = (req.root_label or "").strip()
prefix = f"【本次行动】{action}\n\n" if action else ""
report = (req.report_md or "").strip()
if report:
return "===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 ====="
return prefix + "===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 ====="
seats = [s for s in req.seat_deliveries if (s.content or "").strip()]
if seats:
body = "\n\n".join(f"## 席位{i + 1}:{s.sender or '(未具名席位)'}\n{s.content.strip()}" for i, s in enumerate(seats))
return "===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + body + "\n===== 交付结束 ====="
return prefix + "===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + body + "\n===== 交付结束 ====="
return ""
@@ -703,23 +709,33 @@ def _parse_intent_json(text: str) -> dict[str, Any] | None:
_INTENT_SYS = (
"你是圆桌「方案总结」第三步输入框的**意图识别器**。第三步有两类产物:方案总结报告(Markdown) 和 结构化流程图。"
"用户在输入框发了一条消息,请判断他的**真实意图**,只输出一个 JSON 对象(不要解释、不要 markdown 代码块、用英文双引号):\n"
"你是圆桌「方案总结」第三步对话的**意图识别器**,也是个能帮用户的助手。第三步已产出:方案总结报告(Markdown)、"
"结构化流程图,可能还可把成果入库到知识库。用户在对话框发了一条消息,请**按语义理解他真正想干什么**"
"(不要只做关键词匹配),只输出一个 JSON 对象(不要解释、不要 markdown 代码块、用英文双引号):\n"
'{"intent":"<枚举>","reason":"<一句话依据>"}\n'
"intent 只能取其一:\n"
"- regenerate_report:想重新分析 / 重写【方案总结报告】(如「重新生成报告」「报告再写一版」「重新分析一下」);\n"
"- modify_flow:想修改**当前流程图**的某些节点/连线(如「把目的1改成X」「删掉第3个行为体」「加个验收节点」「流程图这里调整下」);\n"
"intent 取值:\n"
"- regenerate_report:想**重做/重写整份**方案总结报告(如「重新生成报告」「报告再写一版」「重新分析一下」);\n"
"- modify_flow:想**修改当前流程图**的节点/连线(如「把目的1改成X」「删掉第3个行为体」「加个验收节点」「这一层调整下」);\n"
"- regenerate_flow:想**重新生成整张**流程图(如「重新画流程图」「流程图重来」「重新结构化输出」);\n"
"- qa:只是提问 / 咨询,不需要改任何产物(如「为什么选 OTA 平台」「这个目的什么意思」);\n"
"- unclear:意图不明确、无法判断要改报告还是改流程图还是提问(如只说「改一下」「不对」「重来」而没指明对象)。\n"
"判断要点:结合下面给出的上下文(是否已有报告 / 是否已有流程图);当前已有流程图时,含「节点/连线/这一层/这个目的/行为体…改/删/加」之类多半是 modify_flow;"
"明确提到「报告」才是 regenerate_report;只有完全无法判断对象时才用 unclear。\n"
"- ingest:想把本次成果**入库/沉淀/存进知识库**(如「帮我入库」「入库吧」「把方案存进知识库」「沉淀一下」);\n"
"- qa:**提问 / 要求解释、介绍、概述、答疑**,只需用文字回答、不改任何产物(如「介绍一下这报告的内容」"
"「这方案主要讲了啥」「为什么选 OTA 平台」「风险最高的是哪条」「帮我看看有没有遗漏」「总结成几句话」);\n"
"- unclear:**实在**判断不出他想做哪件事(如只说「改一下」「不对」「重来」「再说」且无任何指向)。\n"
"判断要点:\n"
"1. **优先识别出明确意图,不要动不动就 unclear**——只要能合理判断就给具体意图;尤其『介绍/讲讲/解释/说明/"
"为什么/是什么/有没有/帮我看/概述/总结一下』这类是**对内容的提问** → 一律 qa(用文字答),不要误判成重做报告。\n"
"2. 含『节点/连线/这一层/这个目的/行为体/流程图…的 改/删/加/换』 → modify_flow(前提是已有流程图)。\n"
"3. 明确说『报告』要重写/重做才是 regenerate_report;只是问报告内容是 qa。\n"
"4. **入库类意图只有在下文上下文标注「可入库=是」时才允许返回 ingest**;若标注不可入库,用户提入库就归 qa"
"(用文字说明当前不可入库/无适配技能)。\n"
"示例:\n"
'「再帮我生成一次报告」→ {"intent":"regenerate_report","reason":"明确要求重新生成报告"}\n'
'「把目的1的名称改短点」→ {"intent":"modify_flow","reason":"要求修改流程图某节点"}\n'
'「流程图重新画一张」→ {"intent":"regenerate_flow","reason":"要求重画整张流程图"}\n'
'「OTA 平台为什么是直接行为体」→ {"intent":"qa","reason":"纯提问不改产物"}\n'
'「改一下」→ {"intent":"unclear","reason":"没说改报告还是流程图"}'
'「帮我介绍一下这报告中的内容」→ {"intent":"qa","reason":"要求介绍报告内容,属提问答疑"}\n'
'「这方案为什么这么定」→ {"intent":"qa","reason":"询问方案理由"}\n'
'「帮我入库」(可入库=是) → {"intent":"ingest","reason":"明确要求入库且当前可入库"}\n'
'「再帮我生成一次报告」→ {"intent":"regenerate_report","reason":"明确要求重做报告"}\n'
'「把目的1的名称改短点」→ {"intent":"modify_flow","reason":"修改流程图某节点"}\n'
'「流程图重新画一张」→ {"intent":"regenerate_flow","reason":"重画整张流程图"}\n'
'「改一下」→ {"intent":"unclear","reason":"没说改什么"}'
)
@@ -729,7 +745,11 @@ async def classify_flow_intent(request: Request, payload: FlowIntentRequest = Bo
msg = (payload.message or "").strip()
if not msg:
return {"intent": "unclear", "reason": "空消息"}
ctx = f"当前上下文:{'已生成报告' if payload.has_report else '尚无报告'};{'已生成流程图' if payload.has_flow else '尚无流程图'}。"
ctx = (
f"当前上下文:{'已生成报告' if payload.has_report else '尚无报告'};"
f"{'已生成流程图' if payload.has_flow else '尚无流程图'};"
f"可入库={'是' if payload.can_ingest else '否'}。"
)
try:
model = create_chat_model(name=_resolve_model(payload.model), thinking_enabled=False)
res = await model.ainvoke(
@@ -740,6 +760,9 @@ async def classify_flow_intent(request: Request, payload: FlowIntentRequest = Bo
intent = _as_str(obj.get("intent")).lower()
if intent not in _INTENTS:
intent = "unclear"
# 不可入库时模型若误判 ingest → 降级为 qa(用文字说明当前不可入库),不要触发入库。
if intent == "ingest" and not payload.can_ingest:
intent = "qa"
return {"intent": intent, "reason": _as_str(obj.get("reason"))}
except Exception: # noqa: BLE001 — 分类失败不致命,回退澄清让用户自己定
logger.warning("flow intent classify failed", exc_info=True)