import type { QASingleRagTrace, QAThinkingTrace } from './QACommon'; import type { ComparePane, KnowledgeBaseChunk, Message, QAMode, SourceItem } from './GeneralQA.types'; /** * 解析单轮 RAG 接口的 `...` 流式文本。 * * 后端会把“思考过程”和“最终回答”混在同一个 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(''); if (endIdx === -1) { thinkingMarkdown += remaining; break; } thinkingMarkdown += remaining.slice(0, endIdx); inThink = false; thinkingComplete = true; remaining = remaining.slice(endIdx + ''.length); } else { const startIdx = remaining.indexOf(''); if (startIdx === -1) { answerMarkdown += remaining; break; } answerMarkdown += remaining.slice(0, startIdx); inThink = true; remaining = remaining.slice(startIdx + ''.length); } } }; chunks.forEach((chunk) => appendToken(chunk.answer)); return { title: '单轮 RAG 思考过程', thinkingMarkdown, answerMarkdown, thinkingComplete, }; }; /** * 构造单轮 RAG 的演示 trace。 * * 首屏默认需要一条完整的示例问答,帮助用户理解“思考过程 + 答案 + 引用”如何展示。 * 这里保留原组件中的演示内容,避免拆分后默认展示行为发生变化。 */ export const createSingleRagTrace = (): QASingleRagTrace => parseSingleRagStream([ { answer: '' }, { answer: '需要回答用户关于 **RAG 工作原理** 的问题。\n\n' }, { answer: '先根据知识库召回内容组织核心流程:\n\n' }, { answer: '- 检索阶段:查找相关知识片段\n' }, { answer: '- 增强阶段:将证据注入提示词\n' }, { answer: '- 生成阶段:模型基于上下文生成答案\n\n' }, { answer: '回答需要保持结构清晰,并说明它为什么能降低幻觉。' }, { answer: '' }, { 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; }