Files
deerflow-code/web-code/components/qa/GeneralQA.mock.ts
T
2026-05-27 19:43:15 +08:00

450 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { QASingleRagTrace, QAThinkingTrace } from './QACommon';
import type { ComparePane, KnowledgeBaseChunk, Message, QAMode, SourceItem } from './GeneralQA.types';
/**
* 解析单轮 RAG 接口的 `<think>...</think>` 流式文本。
*
* 后端会把“思考过程”和“最终回答”混在同一个 answer 字段里返回,
* UI 需要分别展示在 `SingleRagAnswer` 的思考区和答案区。这个函数只做纯文本拆分,
* 不依赖 React 状态,因此既可用于模拟数据,也可作为对比模式里的兜底空 trace。
*/
export const parseSingleRagStream = (chunks: { answer: string }[]): QASingleRagTrace => {
let thinkingMarkdown = '';
let answerMarkdown = '';
let inThink = false;
let thinkingComplete = false;
const appendToken = (token: string) => {
let remaining = token;
while (remaining) {
if (inThink) {
const endIdx = remaining.indexOf('</think>');
if (endIdx === -1) {
thinkingMarkdown += remaining;
break;
}
thinkingMarkdown += remaining.slice(0, endIdx);
inThink = false;
thinkingComplete = true;
remaining = remaining.slice(endIdx + '</think>'.length);
} else {
const startIdx = remaining.indexOf('<think>');
if (startIdx === -1) {
answerMarkdown += remaining;
break;
}
answerMarkdown += remaining.slice(0, startIdx);
inThink = true;
remaining = remaining.slice(startIdx + '<think>'.length);
}
}
};
chunks.forEach((chunk) => appendToken(chunk.answer));
return {
title: '单轮 RAG 思考过程',
thinkingMarkdown,
answerMarkdown,
thinkingComplete,
};
};
/**
* 构造单轮 RAG 的演示 trace。
*
* 首屏默认需要一条完整的示例问答,帮助用户理解“思考过程 + 答案 + 引用”如何展示。
* 这里保留原组件中的演示内容,避免拆分后默认展示行为发生变化。
*/
export const createSingleRagTrace = (): QASingleRagTrace => parseSingleRagStream([
{ answer: '<think>' },
{ answer: '需要回答用户关于 **RAG 工作原理** 的问题。\n\n' },
{ answer: '先根据知识库召回内容组织核心流程:\n\n' },
{ answer: '- 检索阶段:查找相关知识片段\n' },
{ answer: '- 增强阶段:将证据注入提示词\n' },
{ answer: '- 生成阶段:模型基于上下文生成答案\n\n' },
{ answer: '回答需要保持结构清晰,并说明它为什么能降低幻觉。' },
{ answer: '</think>' },
{ answer: 'RAG(Retrieval-Augmented Generation,检索增强生成)是一种把 **知识库检索** 和 **大模型生成** 结合起来的问答方式。\n\n' },
{ answer: '### 工作流程\n\n' },
{ answer: '1. **问题理解**:系统接收用户问题并生成检索查询。\n' },
{ answer: '2. **知识检索**:从知识库中召回相关文档片段。\n' },
{ answer: '3. **上下文增强**:把检索到的内容和用户问题一起放入 Prompt。\n' },
{ answer: '4. **模型生成**:大模型基于增强后的上下文生成回答。\n\n' },
{ answer: '这种方式可以让回答更贴近已有资料,同时提升可追溯性。' },
]);
/**
* 构造多轮/深度检索的演示思考链。
*
* 真实多轮请求会通过 SSE 持续补齐 rounds;深度检索当前仍走本地模拟,
* 因此这个函数保留不同模式下的轮次差异,用于未接入真实接口时的展示。
*/
export const createThinkingTrace = (mode: QAMode): QAThinkingTrace => {
// 初始化轮次数组,第一轮总是基础检索
const rounds = [
{
id: 'round-1', // 第一轮唯一标识
title: '理解问题并拆解检索方向', // 本轮主题:理解问题并确定检索方向
thoughtMarkdown: '用户询问 **RAG 技术工作原理**。需要先明确核心链路:\n\n- 问题理解与向量化\n- 知识库召回\n- 上下文增强\n- 大模型生成\n\n第一轮优先检索基础定义和主流程。', // 模型思考过程,展示分析步骤
query: 'RAG 检索增强生成 工作原理 基础流程', // 发送给检索系统的查询词
queryShow: true,
retrievals: [ // 第一轮检索返回的资料列表
{
id: 'r1-1',
title: 'RAG技术白皮书',
content: 'RAG 将外部知识检索结果作为上下文注入生成模型,能够降低幻觉并提升答案的时效性和可追溯性。',
source: '知识库 / RAG技术白皮书.pdf',
},
{
id: 'r1-2',
title: 'LLM应用实践:检索增强生成',
content: '典型 RAG 流程包括查询改写、向量检索、片段重排、Prompt 组装和答案生成。',
source: '知识库 / LLM应用实践.docx',
},
],
},
];
// 多轮模式和深度检索模式需要第二轮检索,补充更多细节
if (mode !== 'single-round') {
// 添加第二轮检索:补充多轮检索与重排机制的差异
rounds.push({
id: 'round-2', // 第二轮的唯一标识
title: '补充多轮检索与重排机制', // 本轮检索的主题
thoughtMarkdown: '基础链路已经明确,继续检索 **多轮 RAG** 和 **深度检索** 的差异:\n\n1. 是否需要 Query 改写\n2. 是否需要对子问题分别召回\n3. 是否需要结果重排与证据融合',
query: '多轮 RAG Query 改写 子问题检索 结果重排', // 本轮发送给检索系统的查询词
// 本轮检索返回的资料列表
queryShow: true,
retrievals: [
{
id: 'r2-1',
title: '高级RAG技术指南',
content: '复杂问题通常需要分解为多个子问题,并对每个子问题分别检索,再通过重排模型筛选高相关证据。',
source: '资料库 / 高级RAG技术指南.md',
},
{
id: 'r2-2',
title: 'RAG召回与重排最佳实践',
content: '重排阶段可使用 Cross Encoder 或 LLM 评分,按相关性、覆盖度和可信度筛选最终上下文。',
source: '行业资料 / RAG最佳实践.html',
},
{
id: 'r2-3',
title: '检索增强系统评测报告',
content: '多阶段检索可提升复杂问题的召回率,但会增加延迟和计算成本,需要在效果与响应速度之间平衡。',
source: '评测库 / 检索增强系统评测报告.xlsx',
},
],
});
}
// 深度检索模式需要第三轮,综合所有证据并形成最终回答结构
if (mode === 'deep-search') {
rounds.push({
id: 'round-3', // 第三轮的唯一标识
title: '综合证据并形成回答结构', // 本轮的主题:整合前面检索到的所有证据,组织成完整的回答
thoughtMarkdown: '当前证据已覆盖定义、流程和优化机制。最后需要组织成清晰结构:\n\n- 一句话定义\n- 分阶段流程\n- 技术价值\n- 适用场景与注意事项',
query: 'RAG 技术价值 幻觉 时效性 可追溯性 应用场景', // 本轮查询:关于 RAG 价值和应用场景的信息
queryShow: true,
retrievals: [
{
id: 'r3-1',
title: '企业知识问答系统建设方案',
content: 'RAG 在企业知识问答中常用于连接内部文档、制度库和业务数据库,使回答具备来源依据。',
source: '方案库 / 企业知识问答系统建设方案.pptx',
},
{
id: 'r3-2',
title: '生成式AI落地风险控制',
content: 'RAG 不能完全消除幻觉,仍需要上下文质量控制、引用展示、权限过滤和答案审核机制。',
source: '风控库 / 生成式AI落地风险控制.pdf',
},
],
});
}
return {
title: mode === 'single-round' ? '单轮检索思考过程' : mode === 'multi-round' ? '多轮检索思考过程' : '深度检索思考过程',
rounds,
finalAnswerMarkdown: 'RAG(Retrieval-Augmented Generation,检索增强生成)是一种把 **外部知识检索** 和 **大模型生成** 结合起来的技术架构。\n\n### 核心流程\n\n1. **理解问题**:系统解析用户问题,并可能进行 Query 改写或问题拆解。\n2. **检索证据**:从知识库、文档库或业务数据源中召回相关片段。\n3. **筛选重排**:对召回片段按相关性和可信度排序,保留高质量上下文。\n4. **增强生成**:将问题和检索证据一起发送给大模型,生成回答。\n5. **引用溯源**:展示答案依据,便于用户核验。\n\n### 主要价值\n\n- 降低模型幻觉\n- 引入最新或私有知识\n- 提升回答可解释性和可追溯性\n- 适合企业知识问答、政策解读、资料分析等场景',
};
};
/**
* 构造流式多轮检索的初始空 trace。
*
* 请求刚建立时还没有真正的工具调用结果,先渲染一个“正在准备检索”的轮次,
* 可以让用户立即看到助手进入工作状态,后续 SSE 事件会继续更新这个 trace。
*/
export const createMultiRoundStreamTrace = (): QAThinkingTrace => ({
title: '多轮检索思考过程',
rounds: [{
id: `round-${Date.now()}`,
title: '正在准备检索',
thoughtMarkdown: '',
query: '',
queryShow: true,
queryLoading: true,
retrievals: [],
}],
thinkingComplete: false,
finalAnswerMarkdown: '',
});
/**
* 为对比模式创建左右面板的初始状态。
*
* 对比模式会并发运行两个不同的问答模式;每个面板需要独立维护 loading、trace 和 sources。
* 该工厂函数把不同模式的初始 trace 统一封装,避免页面组件重复判断。
*/
export const createComparePane = (mode: QAMode): ComparePane => ({
mode,
loading: true,
singleRagTrace: (mode === 'single-round' || mode === 'single-chat')
? {
title: '单轮 RAG 思考过程',
thinkingMarkdown: '',
answerMarkdown: '',
thinkingComplete: false,
}
: undefined,
thinkingTrace: (mode === 'single-round' || mode === 'single-chat') ? undefined : createMultiRoundStreamTrace(),
sources: [],
});
/**
* 首屏演示消息。
*
* 页面首次进入时,如果没有用户消息,会展示两轮预置问答,说明引用、思考过程和追问的展示形态。
* 拆分时完整保留原始 mock 数据,确保首屏体验不变。
*/
export const createMockSingleRagMessages = (): Message[] => ([
{
id: 'mock-user-1',
role: 'user',
content: 'RAG 在企业知识问答里具体怎么落地?',
timestamp: new Date(),
},
{
id: 'mock-assistant-1',
role: 'assistant',
content: '',
timestamp: new Date(),
singleRagTrace: {
title: '单轮 RAG 思考过程',
thinkingMarkdown: '先明确用户要的是“可落地方案”,所以需要回答:\n\n1. 数据准备与切片策略\n2. 检索与重排链路\n3. 答案引用与可追溯性\n\n优先给出一套可直接上线的最小闭环。',
answerMarkdown: '可以按“**数据层 → 检索层 → 生成层 → 评估层**”四步落地:\n\n1. **数据层**:把制度、FAQ、SOP 文档切片并向量化,按业务域建索引。[[1]]\n2. **检索层**:先向量召回,再用重排模型做精排,提升命中质量。[[2]]\n3. **生成层**:将 TopK 证据注入 Prompt,要求回答必须带引用编号。[[3]]\n4. **评估层**:持续监控命中率、引用准确率和用户满意度。\n\n如果你要,我可以下一步给你一份“可直接给研发执行”的字段级配置清单。',
thinkingComplete: true,
},
sources: [
{
title: '企业知识问答平台建设指南',
type: '知识库',
snippet: '建议按业务域拆分索引,并保持文档版本可追溯。',
source: '内部架构组',
time: '2026-03-18',
url: 'https://example.com/rag-enterprise-guide',
content: '企业落地 RAG 时,建议先从高频问答域切入,采用“文档切片 + 向量索引 + 重排”三层方案,确保上线初期可控。',
},
{
title: 'RAG 重排实践报告',
type: '技术报告',
snippet: '仅向量召回会引入噪声,重排可显著提升答案相关性。',
source: 'NLP 团队',
time: '2026-01-09',
url: 'https://example.com/rag-rerank-report',
content: '在 20 万条文档规模下,向量 Top50 + CrossEncoder 重排到 Top6,可使人工评测正确率提升约 12%。',
},
{
title: '可追溯生成规范',
type: '规范',
snippet: '回答输出应绑定证据编号,便于用户核验。',
source: 'AI 治理委员会',
time: '2025-12-22',
url: 'https://example.com/traceable-generation-spec',
content: '推荐在答案中输出 [[n]] 形式的引用标识,并支持点击查看证据明细、来源与时间信息。',
},
],
},
{
id: 'mock-user-2',
role: 'user',
content: '那单轮和多轮检索的差异是什么?',
timestamp: new Date(),
},
{
id: 'mock-assistant-2',
role: 'assistant',
content: '',
timestamp: new Date(),
singleRagTrace: {
title: '单轮 RAG 思考过程',
thinkingMarkdown: '用户在做方案选型,回答要突出“复杂问题覆盖率”和“时延成本”两个维度。',
answerMarkdown: '**单轮检索**更快,适合事实性问题;**多轮检索**覆盖更全,适合复杂推理问题。通常实践里会先单轮,召回不足再升级多轮。[[1]][[2]]',
thinkingComplete: true,
},
sources: [
{
title: 'RAG 模式选型对比',
type: '白皮书',
snippet: '单轮偏性能,多轮偏完整性。',
source: '智能搜索实验室',
time: '2026-02-11',
url: 'https://example.com/rag-mode-comparison',
content: '复杂查询在多轮检索中召回覆盖更高,但会增加 20%-60% 延迟,需按业务 SLA 选择。',
},
{
title: '检索系统时延评测',
type: '评测',
snippet: '多轮策略会增加查询与重排调用次数。',
source: '平台性能组',
time: '2026-02-27',
url: 'https://example.com/rag-latency-benchmark',
content: '在同等资源下,多轮检索平均时延高于单轮检索,但对于复杂问题的回答完整度更高。',
},
],
},
]);
/**
* 将多轮检索接口返回的 references 项映射为通用引用来源。
*/
export const mapReferenceToSource = (item: { title?: string; content?: string; snippet?: string }): SourceItem => ({
title: item.title || '未命名来源',
type: '参考资料',
snippet: item.snippet || item.content || '',
content: item.content || item.snippet || '',
});
/**
* 将知识库单轮接口返回的 docs 映射为页面引用来源。
*/
export const mapDocsToSources = (docs: KnowledgeBaseChunk['docs']): SourceItem[] => {
const normalizedDocs = Array.isArray(docs) ? docs : docs ? [docs] : [];
return normalizedDocs.map((item, idx) => {
console.log(item,'检索资料')
if (typeof item === 'string') {
return {
title: '',
type: '知识库',
snippet: item,
source: '',
time: '',
content: item,
};
}
return {
title: item.title || item.source || `参考资料 ${idx + 1}`,
type: '知识库',
snippet: item.snippet || item.content || item.url || '',
source: item.source || item.title || '',
time: '',
url: item.url,
content: item.content || item.snippet || '',
};
});
};
export const chatDataJSON = (chatJSON: string) => {
let chatJSONText = chatJSON.replace(/}{"/g, '},{"');
chatJSONText = '[' + chatJSONText + ']';
// console.log('解析chat',chatJSONText);
let chatData = [];
let chatTitle: any = '';
const docsList: any[] = [];
try {
chatData = JSON.parse(chatJSONText);
chatData.forEach((item: any) => {
if (item['answer']) {
chatTitle += item['answer'];
} else if (item['docs']) {
docsList.push(item);
}
});
return {
chatTitle,
docsList,
};
} catch (error) {
return {
chatTitle,
docsList,
};
}
};
/**
* 从流式文本中提取完整 JSON 对象。
*
* SSE 响应可能把一个 JSON 切成多段返回;该函数通过花括号深度和字符串状态,
* 找到已经完整闭合的对象,并把未闭合尾巴放回 `rest` 等待下一批数据拼接。
*/
export const extractJsonObjects = (raw: string): { objects: string[]; rest: string } => {
const objects: string[] = [];
let depth = 0;
let stringQuote: '"' | "'" | null = null;
let escaped = false;
let start = -1;
for (let i = 0; i < raw.length; i += 1) {
const ch = raw[i];
if (stringQuote) {
if (escaped) {
escaped = false;
} else if (ch === '\\') {
escaped = true;
} else if (ch === stringQuote) {
stringQuote = null;
}
continue;
}
if (ch === '"' || ch === "'") {
stringQuote = ch;
continue;
}
if (ch === '{') {
if (depth === 0) start = i;
depth += 1;
continue;
}
if (ch === '}') {
depth -= 1;
if (depth === 0 && start !== -1) {
objects.push(raw.slice(start, i + 1));
start = -1;
}
}
}
if (depth > 0 && start !== -1) {
return { objects, rest: raw.slice(start) };
}
return { objects, rest: '' };
};
// 处理平台数据
export const platformTypeChange = (platform: string) => {
let platformType: string = '';
if (platform.includes('fb')) {
platformType = 'facebook';
} else if (platform.includes('x')) {
platformType = 'x';
} else if (platform.includes('in')) {
platformType = 'instagram';
} else if (platform.includes('re')) {
platformType = 'reddit';
} else if (platform.includes('th')) {
platformType = 'threads';
} else {
platformType = 'facebook';
}
return platformType;
}