From 30cf0fed9103196d0c99fa9fc5bfd269debad4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Fri, 19 Jun 2026 15:36:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E6=8A=BD=E5=8F=96=20report-j?= =?UTF-8?q?son=20=E5=89=8D=E5=89=A5=E7=A6=BB=20=20=E6=80=9D?= =?UTF-8?q?=E8=80=83=E8=BF=87=E7=A8=8B=E5=81=9A=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 推理模型(DeepSeek-R1/QwQ 等)输出的 ... 思考里常含 ```json/裸 {…},会被误当候选导致抽到错误/残缺数据。抽取前先剥离 think: - 前端 report-template.ts::extractReportData 开头 stripReasoning(复用 reasoning.ts, 兼容完整/多段/流式未闭合/只吐闭合四形态) - 后端 report_json.py 新增 _strip_reasoning 在 extract_report_json 开头调用 - report-template.test.ts +3 例(均通过) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend-web/.env.development | 2 +- .../lib/report-template.test.ts | 26 ++++++++++++++ .../lib/report-template.ts | 14 +++++--- .../roundtable_orchestrator/report_json.py | 35 +++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/frontend-web/.env.development b/frontend-web/.env.development index 67b77a36..cca4033a 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -8,7 +8,7 @@ VITE_USE_REAL_DATA=true VITE_MOCK_COP_TASK_DETAIL=true # 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:true 走本地样例假数据(真实接口未就绪时调试用);联调通过后置 false。 -VITE_MOCK_SQ_REPORT=false +VITE_MOCK_SQ_REPORT=true # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=http://127.0.0.1:8001 diff --git a/frontend-web/src/roundtable-planning/lib/report-template.test.ts b/frontend-web/src/roundtable-planning/lib/report-template.test.ts index cc7b5e9b..699ae537 100644 --- a/frontend-web/src/roundtable-planning/lib/report-template.test.ts +++ b/frontend-web/src/roundtable-planning/lib/report-template.test.ts @@ -67,6 +67,32 @@ test("extractReportData:dependsOn 只保留真实存在的席位 id(剔除 assert.deepEqual(d.seats[0].dependsOn, ["s2"]); }); +test("extractReportData:剥离 思考过程后再抽取(思考里的假 JSON 不干扰)", () => { + // 思考块里故意混入一个会「像」契约但不该被采用的裸 JSON,正文才是真数据。 + const withThink = [ + "", + '我先草拟一版:{"title":"草稿不要用","seats":[{"id":"x","name":"废弃"}]}', + "再想想……", + "", + SAMPLE, + ].join("\n"); + const d = extractReportData(withThink)!; + assert.equal(d.title, "智能机房选址方案大屏"); + assert.equal(d.seats.length, 2); +}); + +test("extractReportData:只吐闭合 (chat template 注入式)也能抽到正文", () => { + const onlyClose = ["模型在此思考成本与风险……", "", SAMPLE].join("\n"); + const d = extractReportData(onlyClose)!; + assert.equal(d.title, "智能机房选址方案大屏"); +}); + +test("extractReportData:流式未闭合 尾段不被误抽(返回 null)", () => { + // 正文还没出,模型还在思考里写了半个 JSON —— 不应抽出数据。 + const streaming = '正文前导\n我打算输出 {"title":"在想","seats":[{"id":"a","name":"甲"'; + assert.equal(extractReportData(streaming), null); +}); + test("extractReportData:无 fence / 坏 JSON / 空数据返回 null", () => { assert.equal(extractReportData("没有数据块"), null); assert.equal(extractReportData("```report-json\n{broken\n```"), null); diff --git a/frontend-web/src/roundtable-planning/lib/report-template.ts b/frontend-web/src/roundtable-planning/lib/report-template.ts index ac2f62d6..14a0ccf5 100644 --- a/frontend-web/src/roundtable-planning/lib/report-template.ts +++ b/frontend-web/src/roundtable-planning/lib/report-template.ts @@ -34,6 +34,7 @@ */ import darkBgSvgRaw from "@/assets/img/layout-bg-theme-dark-blue.svg?raw"; +import { stripReasoning } from "@/roundtable-planning/lib/reasoning"; // ── 契约类型 ───────────────────────────────────────────────────────────────── @@ -207,18 +208,23 @@ function arr(v: unknown): unknown[] { */ export function extractReportData(text: string | null | undefined): ReportData | null { if (!text) return null; + // 容错:先剥离模型 `...` 思考过程(兼容完整/多段/流式未闭合/只吐闭合等 + // 形态,见 reasoning.ts)。否则思考里出现的 ```json / 裸 {…} 会被误当候选,抽到错误或 + // 残缺数据(与「抽取结构化流程」同款容错)。无 think 标记时原样返回,仅去首尾空白。 + const cleaned = stripReasoning(text); + if (!cleaned) return null; const candidates: string[] = []; const rank = (lang: string) => lang === "report-json" ? 0 : lang === "json" || lang === "jsonc" ? 1 : lang === "" ? 2 : 3; - collectFencedBlocks(text) + collectFencedBlocks(cleaned) .map((b, i) => ({ b, i })) .sort((x, y) => rank(x.b.lang) - rank(y.b.lang) || y.i - x.i) .forEach(({ b }) => candidates.push(b.body)); // 未闭合围栏(流式截断):取 report-json fence 之后到结尾。 - const f = text.lastIndexOf(FENCE); - if (f >= 0) candidates.push(text.slice(f + FENCE.length).trim()); + const f = cleaned.lastIndexOf(FENCE); + if (f >= 0) candidates.push(cleaned.slice(f + FENCE.length).trim()); // 裸 JSON(无围栏 / 夹在文字里):所有顶层平衡 {…} 对象,靠后的先试,契约形状的优先。 - const objs = balancedJsonObjects(text); + const objs = balancedJsonObjects(cleaned); for (const o of objs.filter(looksLikeReportJson).reverse()) candidates.push(o); for (const o of objs.reverse()) candidates.push(o); diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/report_json.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/report_json.py index aceda701..77a4b86a 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/report_json.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/report_json.py @@ -28,6 +28,37 @@ _LOOKS_LIKE_RE = re.compile(r'"(seats|consensus|kpis|charts|timeline)"\s*:') # 收集所有 ```lang\n...``` 代码块(lang 可空)。 _FENCE_RE = re.compile(r"```([^\n`]*)\r?\n(.*?)```", re.DOTALL) +# 推理模型的 ... 思考块(剥离后再抽取,避免思考里的 ```json/裸 {} 被误当数据)。 +_THINK_BLOCK_RE = re.compile(r".*?", re.DOTALL) +_ANY_THINK_TAG_RE = re.compile(r"", re.IGNORECASE) +_OPEN_TAG = "" +_CLOSE_TAG = "" + + +def _strip_reasoning(text: str) -> str: + """剥离模型 ``...`` 思考过程,只留正文(对齐前端 ``reasoning.ts::stripReasoning``)。 + + 兼容四种形态:完整、多段、流式未闭合(尾段只有开标签)、只吐闭合(chat template 把开 + 标签注入 prompt、模型只生成 ````,DeepSeek-R1 / QwQ 等推理模型常见)。无 think + 标记时仅去首尾空白。 + """ + if not text: + return "" + working = text + # 形态4:首个 出现在任何 之前(或无 )→ 其前内容整体算推理。 + first_close = working.find(_CLOSE_TAG) + first_open = working.find(_OPEN_TAG) + if first_close >= 0 and (first_open < 0 or first_close < first_open): + working = working[first_close + len(_CLOSE_TAG) :] + # 形态1/2:移除所有完整 ... 块。 + closed = _THINK_BLOCK_RE.sub("", working) + # 形态3:尾段未闭合 (流式截断)→ 截到 之前。 + open_idx = closed.rfind(_OPEN_TAG) + if open_idx >= 0: + closed = closed[:open_idx] + # 兜底:正文不该残留任何 think 标签字面量。 + return _ANY_THINK_TAG_RE.sub("", closed).strip() + # ── 标量/容器归一化小工具(对齐前端 str/num/arr) ─────────────────────────────── @@ -231,6 +262,10 @@ def extract_report_json(text: str | None) -> str | None: 3. 夹在文字里的所有平衡 ``{…}`` 对象,契约形状的优先、靠后的先试。 每个候选 ``json.loads`` 成功后过 :func:`coerce_report_data`,命中即返回归一化 JSON。 """ + if not text: + return None + # 先剥离 ... 思考过程,否则思考里的 ```json / 裸 {…} 会被误当候选。 + text = _strip_reasoning(text) if not text: return None