fix(dashboard): 抽取 report-json 前剥离 <think> 思考过程做容错
推理模型(DeepSeek-R1/QwQ 等)输出的 <think>...</think> 思考里常含
```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) <noreply@anthropic.com>
This commit is contained in:
1 parent
ef615bbfb6
commit
30cf0fed91
4 files changed
+72
-5
No files matched your search
@@ -67,6 +67,32 @@ test("extractReportData:dependsOn 只保留真实存在的席位 id(剔除
|
||||
assert.deepEqual(d.seats[0].dependsOn, ["s2"]);
|
||||
});
|
||||
|
||||
test("extractReportData:剥离 <think> 思考过程后再抽取(思考里的假 JSON 不干扰)", () => {
|
||||
// 思考块里故意混入一个会「像」契约但不该被采用的裸 JSON,正文才是真数据。
|
||||
const withThink = [
|
||||
"<think>",
|
||||
'我先草拟一版:{"title":"草稿不要用","seats":[{"id":"x","name":"废弃"}]}',
|
||||
"再想想……",
|
||||
"</think>",
|
||||
SAMPLE,
|
||||
].join("\n");
|
||||
const d = extractReportData(withThink)!;
|
||||
assert.equal(d.title, "智能机房选址方案大屏");
|
||||
assert.equal(d.seats.length, 2);
|
||||
});
|
||||
|
||||
test("extractReportData:只吐闭合 </think>(chat template 注入式)也能抽到正文", () => {
|
||||
const onlyClose = ["模型在此思考成本与风险……", "</think>", SAMPLE].join("\n");
|
||||
const d = extractReportData(onlyClose)!;
|
||||
assert.equal(d.title, "智能机房选址方案大屏");
|
||||
});
|
||||
|
||||
test("extractReportData:流式未闭合 <think> 尾段不被误抽(返回 null)", () => {
|
||||
// 正文还没出,模型还在思考里写了半个 JSON —— 不应抽出数据。
|
||||
const streaming = '正文前导\n<think>我打算输出 {"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);
|
||||
|
||||
@@ -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;
|
||||
// 容错:先剥离模型 `<think>...</think>` 思考过程(兼容完整/多段/流式未闭合/只吐闭合等
|
||||
// 形态,见 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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user