后台挂起功能修改
This commit is contained in:
1 parent
cca954d01c
commit
f0c85c4795
8 files changed
+126
-13
No files matched your search
@@ -16,6 +16,7 @@ import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMo
|
||||
import type { Step2Dialogue } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report";
|
||||
import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent";
|
||||
import { stripReasoning } from "@/roundtable-planning/lib/reasoning";
|
||||
|
||||
export interface Step3PanelProps {
|
||||
threadIds: ThreadIdMap | null;
|
||||
@@ -76,7 +77,9 @@ export function Step3Panel(props: Step3PanelProps) {
|
||||
.map((d) => ({
|
||||
sender: String(d.sender ?? ""),
|
||||
avatarType: String(d.avatarType ?? "blue"),
|
||||
content: String(d.content ?? ""),
|
||||
// 交付内容只保留模型生成的主体输出,剥离 `<think>...</think>` 推理过程
|
||||
// —— 既用于渲染各席位交付卡片,也会喂给总结/报告模型,避免推理噪声污染。
|
||||
content: stripReasoning(String(d.content ?? "")),
|
||||
}));
|
||||
|
||||
const seatNameMap = Object.fromEntries(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { splitInlineReasoning, stripReasoning } from "./reasoning";
|
||||
|
||||
test("空输入返回空正文与 null 推理", () => {
|
||||
assert.deepEqual(splitInlineReasoning(""), { content: "", reasoning: null });
|
||||
});
|
||||
|
||||
test("无 think 标签时原样返回正文", () => {
|
||||
assert.deepEqual(splitInlineReasoning("纯正文,无推理"), {
|
||||
content: "纯正文,无推理",
|
||||
reasoning: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("形态 1:完整 <think>…</think> 提取推理,保留正文", () => {
|
||||
const { content, reasoning } = splitInlineReasoning("<think>先想一下</think>这是答案");
|
||||
assert.equal(content, "这是答案");
|
||||
assert.equal(reasoning, "先想一下");
|
||||
});
|
||||
|
||||
test("形态 2:多段 think 合并推理", () => {
|
||||
const { content, reasoning } = splitInlineReasoning(
|
||||
"<think>第一段</think>正文A<think>第二段</think>正文B",
|
||||
);
|
||||
assert.equal(content, "正文A正文B");
|
||||
assert.equal(reasoning, "第一段\n\n第二段");
|
||||
});
|
||||
|
||||
test("形态 3:流式未闭合 <think> 尾段算推理", () => {
|
||||
const { content, reasoning } = splitInlineReasoning("正文<think>还在想…");
|
||||
assert.equal(content, "正文");
|
||||
assert.equal(reasoning, "还在想…");
|
||||
});
|
||||
|
||||
test("形态 4:模板注入式只吐闭合 </think>,前导内容算推理", () => {
|
||||
const { content, reasoning } = splitInlineReasoning(
|
||||
"我需要分析一下需求</think>这是最终方案",
|
||||
);
|
||||
assert.equal(content, "这是最终方案");
|
||||
assert.equal(reasoning, "我需要分析一下需求");
|
||||
});
|
||||
|
||||
test("形态 4 流式:尚未吐出 </think> 时全部当正文(后续闭合再归位)", () => {
|
||||
// 未出现任何标签 → 无从区分,先按正文渲染。
|
||||
const { content, reasoning } = splitInlineReasoning("正在推理中,还没收到闭合标签");
|
||||
assert.equal(content, "正在推理中,还没收到闭合标签");
|
||||
assert.equal(reasoning, null);
|
||||
});
|
||||
|
||||
test("安全兜底:正文里绝不残留任何 think 标签字面量", () => {
|
||||
const { content } = splitInlineReasoning("答案前</think>答案中<think>");
|
||||
assert.ok(!content.includes("<think>"));
|
||||
assert.ok(!content.includes("</think>"));
|
||||
});
|
||||
|
||||
test("stripReasoning 只返回正文", () => {
|
||||
assert.equal(stripReasoning("<think>推理</think>交付正文"), "交付正文");
|
||||
assert.equal(stripReasoning("前导推理</think>交付正文"), "交付正文");
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* 把模型流式文本里的 `<think>...</think>` 块拆出来,作为「思考过程」单独展示。
|
||||
*
|
||||
* 兼容三种形态:
|
||||
* 兼容四种形态:
|
||||
* 1) **完整** `<think>foo</think>bar` —— 提取 `foo` 作 reasoning,正文为 `bar`
|
||||
* 2) **多段** `<think>a</think>x<think>b</think>y` —— reasoning 合并为 `a\n\nb`,正文为 `xy`
|
||||
* 3) **流式未闭合** `bar<think>partial...` —— reasoning 为 `partial...`(尾段),正文为 `bar`
|
||||
* 4) **模板注入式(只吐闭合)** `foo</think>bar` —— 开标签 `<think>` 被 chat template
|
||||
* 注入到 prompt 里、模型只生成闭合标签(DeepSeek-R1 / QwQ 等推理模型的典型行为)。
|
||||
* 此时首个 `</think>` 之前没有配对的 `<think>`,把它之前的内容整体算 reasoning,正文为 `bar`。
|
||||
*
|
||||
* 实现思路与主聊天 `src/core/messages/utils.ts::splitInlineReasoning` 对齐,但
|
||||
* 这里**额外处理了未闭合的 `<think>`** —— 流式渲染时如果模型还没吐出 `</think>`,
|
||||
* 主聊天那份实现会把 `<think>` 字面量留在正文里,我们用 lastIndexOf 兜底,把尾段
|
||||
* 也算作 reasoning。
|
||||
* 这里**额外处理了未闭合的 `<think>`(形态 3)和只吐闭合的 `</think>`(形态 4)** ——
|
||||
* 主聊天靠后端 `additional_kwargs.reasoning_content` 单列推理,而圆桌多智能体流式只取
|
||||
* `content` 文本,推理标签会内联在正文里,故这里需要把这两种内联形态都兜住。
|
||||
*
|
||||
* Inline `<think>foo` 在文本中间出现(无尾闭合)被认为是流式未闭合的开头:
|
||||
* 假设之后会闭合,把它之后所有内容算 reasoning。这与 DeepSeek-R1 / QwQ 等
|
||||
@@ -18,6 +21,9 @@
|
||||
|
||||
const THINK_TAG_RE = /<think>\s*([\s\S]*?)\s*<\/think>/g;
|
||||
const OPEN_TAG = "<think>";
|
||||
const CLOSE_TAG = "</think>";
|
||||
// 兜底:正文里绝不应残留任何 think 标签字面量,清掉任何孤立的 `<think>` / `</think>`。
|
||||
const ANY_THINK_TAG_RE = /<\/?think>/gi;
|
||||
|
||||
export interface SplitReasoningResult {
|
||||
/** 正文(去掉所有 `<think>...</think>` 块以及尾段未闭合 `<think>` 之后内容)。 */
|
||||
@@ -30,14 +36,27 @@ export function splitInlineReasoning(text: string): SplitReasoningResult {
|
||||
if (!text) return { content: "", reasoning: null };
|
||||
|
||||
const reasoningParts: string[] = [];
|
||||
// 先处理所有完整 `<think>...</think>` 闭合块。
|
||||
const closed = text.replace(THINK_TAG_RE, (_, reasoning: string) => {
|
||||
|
||||
// 形态 4(模板注入式,只吐闭合):首个 `</think>` 出现在任何 `<think>` 之前(或根本
|
||||
// 没有 `<think>`),说明开标签被 chat template 注入、模型只生成了闭合标签。把第一个
|
||||
// `</think>` 之前的内容整体算作 reasoning,正文从闭合标签之后继续解析。
|
||||
let working = text;
|
||||
const firstClose = working.indexOf(CLOSE_TAG);
|
||||
const firstOpen = working.indexOf(OPEN_TAG);
|
||||
if (firstClose >= 0 && (firstOpen < 0 || firstClose < firstOpen)) {
|
||||
const lead = working.slice(0, firstClose).trim();
|
||||
if (lead) reasoningParts.push(lead);
|
||||
working = working.slice(firstClose + CLOSE_TAG.length);
|
||||
}
|
||||
|
||||
// 形态 1/2:处理所有完整 `<think>...</think>` 闭合块。
|
||||
const closed = working.replace(THINK_TAG_RE, (_, reasoning: string) => {
|
||||
const normalized = reasoning.trim();
|
||||
if (normalized) reasoningParts.push(normalized);
|
||||
return "";
|
||||
});
|
||||
|
||||
// 处理尾段未闭合 `<think>` —— 流式情况。
|
||||
// 形态 3:尾段未闭合 `<think>` —— 流式情况。
|
||||
// 如果 closed 里还残留 `<think>` 字面量,说明这一段还没收到 `</think>`,
|
||||
// 把它之后的所有内容视为 reasoning-in-progress,正文截到 `<think>` 之前。
|
||||
let finalContent = closed;
|
||||
@@ -48,8 +67,16 @@ export function splitInlineReasoning(text: string): SplitReasoningResult {
|
||||
finalContent = closed.slice(0, openIdx);
|
||||
}
|
||||
|
||||
// 安全兜底:无论上面是否完全配对,正文里都不该再出现 think 标签字面量。
|
||||
finalContent = finalContent.replace(ANY_THINK_TAG_RE, "");
|
||||
|
||||
return {
|
||||
content: finalContent.trim(),
|
||||
reasoning: reasoningParts.length > 0 ? reasoningParts.join("\n\n").trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 只要正文、丢弃推理过程的便捷封装(用于「交付内容」类只需主体输出的场景)。 */
|
||||
export function stripReasoning(text: string): string {
|
||||
return splitInlineReasoning(text).content;
|
||||
}
|
||||
@@ -79,3 +79,15 @@ test("coordinatorName 为 null 时不剔除任何席位", () => {
|
||||
const result = collectRoundtableDeliverables(dialogues, null);
|
||||
assert.equal(result.length, 2);
|
||||
});
|
||||
|
||||
test("交付正文剥离 <think>…</think> 推理过程", () => {
|
||||
const dialogues = [
|
||||
bubble({ sender: "方案设计", content: "<think>盘算一下</think>方案正文" }),
|
||||
bubble({ sender: "风险审查", content: "前导推理</think>风险正文" }),
|
||||
];
|
||||
const result = collectRoundtableDeliverables(dialogues, null);
|
||||
assert.deepEqual(result, [
|
||||
{ agentName: "方案设计", content: "方案正文", threadId: null },
|
||||
{ agentName: "风险审查", content: "风险正文", threadId: null },
|
||||
]);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Step2Dialogue } from "../hooks/useStep2Orchestration";
|
||||
import { stripReasoning } from "./reasoning";
|
||||
|
||||
/**
|
||||
* 回传父页面(无界)的单个智能体交付。
|
||||
@@ -29,7 +30,8 @@ export function collectRoundtableDeliverables(
|
||||
}
|
||||
return Array.from(lastBySeat.values()).map((d) => ({
|
||||
agentName: d.sender,
|
||||
content: d.content,
|
||||
// 回传宿主的交付正文剥离 `<think>...</think>` 推理过程,只留主体输出。
|
||||
content: stripReasoning(d.content),
|
||||
threadId: d.agentThreadId ?? null,
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user