// js/chatbot/core/streaming-multi-hop.js // 流式多轮取材 - 实时进度反馈 (function(window) { 'use strict'; /** * 估算文本的token数量(简化版) * @param {string} text - 要估算的文本 * @returns {number} 估算的token数 */ function estimateTokens(text) { if (!text || typeof text !== 'string') return 0; // 中文字符:平均1.5字符 = 1 token const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; const chineseTokens = Math.ceil(chineseChars / 1.5); // 英文单词:平均1个单词 = 0.75 token const englishWords = (text.match(/[a-zA-Z]+/g) || []).length; const englishTokens = Math.ceil(englishWords * 0.75); // 数字和符号:粗略估算 const otherChars = text.length - chineseChars - text.match(/[a-zA-Z\s]/g)?.length || 0; const otherTokens = Math.ceil(otherChars / 4); return chineseTokens + englishTokens + otherTokens; } /** * 流式多轮取材 * 使用 Generator 函数实现流式输出,每个步骤都实时反馈给UI * * @param {string} userQuestion - 用户问题 * @param {Object} docContentInfo - 文档内容信息 * @param {Object} config - 配置 * @param {Object} options - 选项 * @returns {AsyncGenerator} 异步生成器 */ async function* streamingMultiHopRetrieve(userQuestion, docContentInfo, config, options = {}) { const userSet = window.semanticGroupsSettings || {}; // 设置较大上限防止死循环,但主要由AI通过final标志决定何时结束 const maxRounds = Number(options.maxRounds ?? userSet.maxRounds) > 0 ? Number(options.maxRounds ?? userSet.maxRounds) : 10; try { const groups = Array.isArray(docContentInfo.semanticGroups) ? docContentInfo.semanticGroups : []; const hasGroups = groups.length > 0; if (!hasGroups) { console.log('[StreamingMultiHop] 没有意群数据,将只使用grep工具进行检索'); } // 1. 分析问题,获取候选意群(如果有的话) if (hasGroups) { yield { type: 'status', phase: 'analyze', message: '正在分析问题...' }; } const candidates = groups; // 2. 多轮取材循环 const fetched = new Map(); const detail = []; let contextParts = []; const gist = (window.data && window.data.semanticDocGist) ? window.data.semanticDocGist : ''; const searchHistory = []; // 记录搜索历史 // 任务追踪状态 let taskStatusHistory = { completed: [], current: '', pending: [] }; /** * 升级或获取意群内容 * @param {Set} groupIds - 意群ID集合 * @param {string} targetGranularity - 目标粒度 (summary/digest/full) */ const upgradeOrFetchGroups = async (groupIds, targetGranularity = 'digest') => { if (groupIds.size === 0) { console.warn('[StreamingMultiHop] upgradeOrFetchGroups: groupIds为空'); return; } console.log(`[StreamingMultiHop] 开始升级/获取 ${groupIds.size} 个意群到 ${targetGranularity}`); for (const groupId of groupIds) { if (window.SemanticTools && typeof window.SemanticTools.fetchGroupText === 'function') { try { const existing = fetched.get(groupId); const shouldFetch = !existing || (existing.granularity === 'summary' && targetGranularity !== 'summary') || (existing.granularity === 'digest' && targetGranularity === 'full'); if (shouldFetch) { const groupRes = window.SemanticTools.fetchGroupText(groupId, targetGranularity); if (groupRes && groupRes.text) { const isUpgrade = fetched.has(groupId); fetched.set(groupId, { granularity: groupRes.granularity, text: groupRes.text }); console.log(`[StreamingMultiHop] ${isUpgrade ? '升级' : '新增'} ${groupId}: ${existing?.granularity || '无'} → ${groupRes.granularity}`); // 更新detail和contextParts if (isUpgrade) { // 升级:替换原有内容 const idx = detail.findIndex(d => d.groupId === groupId); if (idx >= 0) detail[idx].granularity = groupRes.granularity; const ctxIdx = contextParts.findIndex(c => c.startsWith(`【${groupId}`)); if (ctxIdx >= 0) { contextParts[ctxIdx] = `【${groupId} - ${groupRes.granularity}】\n${groupRes.text}`; } } else { // 新增 detail.push({ groupId: groupId, granularity: groupRes.granularity }); contextParts.push(`【${groupId} - ${groupRes.granularity}】\n${groupRes.text}`); } } else { console.warn(`[StreamingMultiHop] fetchGroupText返回空: ${groupId}`); } } else { console.log(`[StreamingMultiHop] 跳过 ${groupId}: 已有${existing.granularity},无需升级到${targetGranularity}`); } } catch (e) { console.error(`[StreamingMultiHop] 升级${groupId}失败:`, e); } } } console.log(`[StreamingMultiHop] 升级完成,当前detail数量: ${detail.length}`); }; // 预加载策略:第一轮给AI所有意群的summary,让AI判断哪些需要详细内容 // 后续轮会清理掉AI不感兴趣的意群,只保留AI操作过的内容 let preloadedInFirstRound = false; const interestedGroups = new Set(); // 记录AI感兴趣的意群(fetch过或搜索命中) let aiRequestedMapInFinalContext = false; // AI决定是否在最终上下文中包含地图 let aiRequestedGroupListInFinalContext = false; // AI决定是否在最终上下文中包含意群简要列表 if (hasGroups && userSet && userSet.preloadFirstRound === true && groups.length <= 50) { yield { type: 'status', phase: 'preload', message: `预加载 ${groups.length} 个意群摘要供AI判断...` }; // 第一轮:预加载所有summary for (const g of groups) { if (window.SemanticTools?.fetchGroupText) { try { const res = window.SemanticTools.fetchGroupText(g.groupId, 'summary'); if (res && res.text) { fetched.set(g.groupId, { granularity: 'summary', text: res.text }); detail.push({ groupId: g.groupId, granularity: 'summary' }); contextParts.push(`【${g.groupId} - summary】\n${res.text}`); } } catch (_) {} } } preloadedInFirstRound = true; yield { type: 'status', phase: 'preload_complete', message: `已预加载 ${fetched.size} 个意群摘要,等待AI判断...` }; } for (let round = 0; round < maxRounds; round++) { yield { type: 'round_start', round, message: `第 ${round + 1} 轮取材...` }; // 地图文本:仅在AI通过 map 工具请求后注入 const listText = (typeof window._multiHopLastMapText === 'string' && window._multiHopLastMapText) ? window._multiHopLastMapText : ''; // 构造已获取内容的摘要 let fetchedSummary = '无'; if (fetched.size > 0) { const fetchedDetails = []; for (const [groupId, data] of fetched.entries()) { const preview = data.text.length > 500 ? data.text.substring(0, 500) + '...' : data.text; fetchedDetails.push(`【${groupId}】(${data.granularity})\n${preview}`); } fetchedSummary = fetchedDetails.join('\n\n'); } // 构造搜索历史提示 let searchHistoryText = ''; if (searchHistory.length > 0) { const recentSearches = searchHistory.slice(-5); // 最近5次 searchHistoryText = '\n\n【搜索历史】(避免重复搜索这些查询):\n' + recentSearches.map(s => { const status = s.resultCount > 0 ? `✓ ${s.resultCount}个结果` : '✗ 无结果'; const toolName = { 'vector_search': '向量', 'keyword_search': '关键词', 'grep': 'GREP', 'regex_search': '正则', 'boolean_search': '布尔' }[s.tool] || s.tool; return `- ${toolName}搜索 "${s.query}" → ${status}`; }).join('\n'); } const preloadedNotice = (round === 0 && fetched.size > 0) ? ` 提示:已缓存 ${fetched.size} 个意群摘要在【已获取内容】中;若需整体地图,请调用 map 工具。` : ''; // 检查文档配置 const docId = (window.data && window.data.currentPdfName) || 'unknown'; const docConfig = window.data?.multiHopConfig?.[docId]; const useSemanticGroups = docConfig?.useSemanticGroups !== false; // 默认true const useVectorSearch = docConfig?.useVectorSearch !== false; // 默认true // 根据配置动态构建工具列表说明 const vectorSearchTool = useVectorSearch ? `**推荐优先使用:** - {"tool":"vector_search","args":{"query":"语义描述","limit":15}} 用途:**智能语义搜索**(理解同义词、相关概念、隐含关系) 返回:语义最相关的chunks(每个1500-3000字) **优势**: * 理解问题的深层含义,不局限于字面匹配 * 能找到换了说法但意思相同的内容 * 适合概念性、开放性、探索性问题 * 召回率高,不会因为换词而漏掉相关内容 **你可以调整limit**:概念性问题可用limit=10-15,精确查找可用limit=5 ` : ''; // BM25搜索:无论是否有意群都可用(基于chunks) const keywordSearchTool = ` - {"tool":"keyword_search","args":{"keywords":["词1","词2"],"limit":8}} 用途:多关键词加权搜索(BM25算法) 返回:包含关键词的文档片段(按相关度评分) **使用时机**:精确查找特定关键词组合${!useVectorSearch ? '(主要搜索工具)' : ',或vector_search失败时的降级方案'} **你可以调整limit**:关键词明确可用limit=5,模糊查找可用limit=10 `; const advancedSearchTools = ` **高级匹配工具(特殊场景使用):** - {"tool":"regex_search","args":{"pattern":"\\\\d{4}年\\\\d{1,2}月","limit":10,"context":1500}} 用途:正则表达式搜索(匹配特定格式) 返回:符合正则模式的文本片段 **适用场景**: * 搜索特定格式(日期"2023年5月"、编号"公式3.2"、"Fig. 1") * 匹配复杂模式(电话、邮箱、特殊符号组合) * 数学公式编号、图表引用等 * OCR错误的容错匹配(如"注[意愈]力"可用"注.力"匹配) **注意**:pattern需要转义特殊字符(\\\\d 表示数字,\\\\. 表示点号) - {"tool":"boolean_search","args":{"query":"(CNN OR RNN) AND 对比 NOT 图像","limit":10,"context":1500}} 用途:布尔逻辑搜索(AND/OR/NOT组合) 返回:同时满足多个条件的文本片段 **适用场景**: * 复杂逻辑查询(必须包含A和B,但不包含C) * 多概念精确组合(比grep的OR更强大) * 排除干扰信息(NOT关键词) **语法**:支持 AND, OR, NOT 和括号,如 "(词1 OR 词2) AND 词3 NOT 词4" `; const mapFetchTools = useSemanticGroups ? ` ### 获取详细内容工具 - {"tool":"fetch","args":{"groupId":"group-1"}} 用途:获取指定意群详细内容(包含完整论述、公式、数据、图表) 返回:完整文本(最多8000字)+ 结构信息 **使用时机**:当搜索到的chunk片段信息不足,需要看到完整上下文时 - {"tool":"map","args":{"limit":50,"includeStructure":true}} 用途:获取文档整体结构 返回:意群地图(ID、字数、关键词、摘要、章节/图表/公式) ${preloadedNotice} ` : `${preloadedNotice} `; const sys = `你是检索规划助手,专门负责规划如何从文档中检索相关内容。 **重要:你的角色定位** - ⚠️ **你不负责回答用户问题**,你只负责规划如何检索文档内容 - ⚠️ **不要生成mermaid图表、思维导图或任何最终答案** - ✓ 你的任务:分析用户问题 → 规划使用哪些工具检索文档 → 输出JSON格式的检索计划 - ✓ 检索到的内容会交给另一个AI来回答用户问题 **你的工作流程** 1. 分析用户问题,判断需要什么类型的信息 2. 选择合适的检索工具组合 3. **规划任务清单**:将复杂问题拆解为多个检索子任务 4. 输出JSON格式的检索计划(不是答案!) ## 工具定义(JSON格式) ### 搜索工具(返回chunk内容,由你决定是否需要完整意群) ${vectorSearchTool}**精确匹配场景使用:** - {"tool":"grep","args":{"query":"具体短语","limit":20,"context":2000,"caseInsensitive":true}} 用途:字面文本搜索(适合已知精确关键词) 返回:包含该短语的原文片段(前后2000字上下文) **适用场景**: * 搜索专有名词、特定数字、固定术语 * 用户问题中明确提到某个词,需要找原文 * 你已经知道文档中的确切表达方式 **支持OR逻辑**:query可用 | 分隔多个关键词,如 "方程|公式|equation" **你可以调整limit**:需要更多结果就增大limit,只需少量结果就减小limit ${keywordSearchTool} ${advancedSearchTools} ${mapFetchTools} ## 智能决策流程 **第一步:分析问题复杂度,选择工具组合策略** **简单问题(单工具足够):** 1. 精确实体查找 - 示例:"雷曼公司何时破产?" → grep("雷曼|Lehman", limit=5) ${useVectorSearch ? `2. 单一概念解释 - 示例:"什么是注意力机制?" → vector_search("注意力机制 原理", limit=8) ` : ''}3. 特定格式查找(编号、日期) - 示例:"找出公式3.2的内容" → regex_search("公式\\\\s*3\\\\.2|式\\\\s*\\\\(3\\\\.2\\\\)", limit=5) - 示例:"2023年的相关研究" → regex_search("2023年", limit=10) 4. 复杂逻辑排除 - 示例:"讨论模型但不涉及训练的内容" → boolean_search("模型 NOT (训练 OR train)", limit=8) **复杂问题(建议多工具并用):** ${useVectorSearch && useSemanticGroups ? `1. 综合性分析(如"研究背景与意义") - 策略:**并发使用多个工具,全方位检索** - 示例:"研究背景与意义?" → 第1轮并发(推荐加入map获取整体结构): {"operations":[ {"tool":"vector_search","args":{"query":"研究背景 意义 动机","limit":10}}, {"tool":"grep","args":{"query":"背景|意义|动机|研究目的","limit":8}}, {"tool":"map","args":{"limit":30}} ],"final":false} → 第2轮根据结果决定是否需要fetch关键意群 2. 多维度对比(如"CNN和RNN的区别") - 策略:**搜索两个主体 + 对比关系** - 示例:"CNN和RNN的区别" → {"operations":[ {"tool":"vector_search","args":{"query":"CNN RNN 区别 对比","limit":12}}, {"tool":"grep","args":{"query":"CNN|RNN","limit":10}} ],"final":false} 3. 历史/因果关系(如"金融危机的原因和影响") - 策略:**语义搜索 + 关键词 + 可能需要map** - 示例:"金融危机的原因和影响" → {"operations":[ {"tool":"vector_search","args":{"query":"金融危机 原因 影响","limit":12}}, {"tool":"grep","args":{"query":"危机|原因|影响|导致","limit":8}}, {"tool":"map","args":{"limit":30}} ],"final":false} 4. 整体理解类(如"文档的主要内容") - 策略:**先map看结构,再fetch关键部分** - 示例:"文档讲了什么?" → 第1轮:{"operations":[{"tool":"map","args":{"limit":50}}],"final":false} → 第2轮:根据地图fetch重要意群 ` : `1. 多关键词搜索 - 策略:**使用grep进行关键词检索** - 示例:"研究背景与意义?" → {"operations":[ {"tool":"grep","args":{"query":"背景|意义|动机|研究目的","limit":15}} ],"final":false} 2. 特定概念搜索 - 策略:**使用keyword_search进行BM25搜索** - 示例:"什么是注意力机制?" → {"operations":[ {"tool":"keyword_search","args":{"keywords":["注意力","机制","attention"],"limit":10}} ],"final":false} `}**工具组合原则:** - **复杂问题优先多工具并用**(同一轮并发执行) ${useVectorSearch ? `- vector_search(语义)+ grep(精确)= 更高召回率和准确率 ${useSemanticGroups ? `- **综合性分析问题(如"研究背景与意义")强烈建议使用map**:map提供文档整体结构,有助于理解背景脉络 - 多维度问题建议3个工具:vector + grep + map ` : ''}` : `- grep(精确)+ keyword_search(BM25)= 提高召回率 - 多个关键词组合使用,提高搜索准确性 `}- 简单问题可以单工具,但不确定时宁可多用 - **优先级判断**: * 有明确格式(日期、编号、公式)→ 首选 regex_search * 需要排除干扰词(NOT逻辑)→ 首选 boolean_search * 普通精确词匹配 → 使用 grep * 语义理解、同义词 → 使用 vector_search - regex和boolean是**特殊场景工具**,不要过度使用,普通查询用grep/vector即可 ${useSemanticGroups ? `**第二步:判断是否需要fetch意群完整内容** - 搜索工具会返回:chunk内容 + suggestedGroups(所属意群列表) - 如果chunk片段**已包含足够信息**回答问题 → 不需要fetch,直接final=true - 如果chunk片段**信息不足**(如缺少公式细节、数据表、完整论述) → fetch相关意群 - **优先精准而非全面**:只fetch真正需要的意群,不要全部fetch ` : ''}**核心原则:提供充分、详细、准确的上下文** - 你的目标是为最终AI提供**足够回答用户问题的完整上下文** - 不要因为担心token浪费而过早结束检索 - 宁可多获取一些内容,也不要让最终AI因为信息不足而无法回答 - 【已获取内容】为空时,**绝不能**返回空操作,必须至少执行一次检索 **第三步:控制结果数量,避免噪音** ${useVectorSearch ? `- **优先用vector_search**,概念性问题用limit=10-15,精确查找用limit=5-8 - grep仅用于精确匹配场景,limit=5-10即可 ` : `- **优先用grep**,精确匹配场景用limit=10-15 `}- keyword_search作为降级方案,limit=8-10 - 避免一次性返回过多结果造成token浪费 - 如果第一次搜索结果不足,可以增加limit或换工具 ${useSemanticGroups ? `**第四步:地图信息的智能使用** - 【候选意群地图】提供了文档结构概览(如果执行过map工具) - **你可以根据任务类型自主决定是否需要地图信息辅助回答**: * 宏观任务(如"总结主要内容"、"思维导图"):地图很有用,可直接引用地图结构 * 微观任务(如"雷曼公司何时破产"):地图意义不大,依赖具体检索结果 * 混合/长难任务(如"谁做了什么经历"):地图可提供流程框架,再用fetch补充细节 - **你的决策方式**:在final=true时,【已获取内容】中包含的信息应该足够最终AI回答 * 如果认为地图有助于宏观理解,可以确保地图已在【候选意群地图】中(map工具已执行) * 如果地图无关紧要,只需确保检索到的chunks/groups足够即可 **第五步:并发与结束** ` : `**第四步:并发与结束** `} - 可以在同一轮并发执行多个操作 - 获取到足够内容后立即final=true - **严格检查【搜索历史】避免重复搜索**: * ⚠️ 如果【搜索历史】中已有完全相同的查询(query相同),**绝对不要**再次执行 * ⚠️ 如果【搜索历史】中显示某个查询"✗ 无结果",不要换工具重试相同查询(大概率还是无结果) * ✓ 应该换用不同的关键词、或使用不同策略(如用map看整体结构) - **只有当【已获取内容】真正充足时**,才返回{"operations":[],"final":true} - **如果【已获取内容】为空或不足**,必须继续检索,不能直接final=true ## 任务追踪机制(Task Status Tracking) **多轮检索时使用taskStatus追踪进度**,帮助你在复杂问题中保持目标清晰: **taskStatus字段说明**(可选,但复杂问题强烈推荐): { "operations": [...], "final": false, "taskStatus": { "completed": ["✓ 已获取研究背景(vector_search+grep, 找到18个chunks)"], "current": "→ 正在获取方法论详细描述", "pending": ["待检索实验设计", "待检索结论和展望"] } } - **completed**: 已完成的检索任务(附工具和结果数) * 示例:"✓ 已获取研究动机(vector_search, 3个chunks)" * **重要**:记录已搜索的query,避免下一轮重复执行 * 作用:避免重复检索,展示进度 - **current**: 当前正在执行的任务 * 示例:"→ 正在查找公式推导过程" * 作用:明确本轮目标 - **pending**: 后续待完成的任务列表 * 示例:["待补充图表说明", "待验证时间线"] * 作用:规划下一步,防止遗漏 **使用场景**: - **简单问题**(1轮完成):可省略taskStatus - **复杂问题**(需多轮):必须使用taskStatus * 第1轮:分解任务到pending,设置current * 中间轮:更新completed,调整current和pending * 最终轮:所有任务在completed,pending为空 **完整示例:复杂问题的追踪** 问题:"分析论文的背景、方法、实验和结论" 第1轮规划: {"operations":[{"tool":"vector_search","args":{"query":"研究背景 动机","limit":10}}],"final":false,"taskStatus":{"completed":[],"current":"→ 检索研究背景和动机","pending":["待检索方法论","待检索实验","待检索结论"]}} 第2轮规划: {"operations":[{"tool":"fetch","args":{"groupId":"group-5"}}],"final":false,"taskStatus":{"completed":["✓ 已获取研究背景(vector+grep, 18个chunks)"],"current":"→ 获取方法论详细内容","pending":["待检索实验","待检索结论"]}} 第3轮(完成): {"operations":[],"final":true,"taskStatus":{"completed":["✓ 研究背景","✓ 方法论","✓ 实验结果","✓ 结论"],"current":"→ 检索完成","pending":[]}} ## 示例决策 ⚠️ **输出示例对比**: 问题:"生成思维导图" ❌ 错误输出(直接生成mermaid代码块):禁止!那是回答问题,不是规划检索。 ✓ 正确输出(规划检索+简短说明): 需要获取文档结构和主要内容,使用map工具。 {"operations":[{"tool":"map","args":{"limit":50}}],"final":false,"includeMapInFinalContext":true} 说明:你只规划"如何检索",不生成"最终答案"。另一个AI会用检索结果生成mermaid。 --- 示例1(复杂综合问题,多工具并用): 问题:"研究背景与意义?" → {"operations":[ {"tool":"vector_search","args":{"query":"研究背景 意义 动机","limit":10}}, {"tool":"grep","args":{"query":"背景|意义|动机|研究目的","limit":8}} ],"final":false} → 返回vector: 8个语义相关chunk + grep: 5个精确匹配chunk → 两者互补,语义覆盖+精确补充 → {"operations":[{"tool":"fetch","args":{"groupId":"group-1"}}],"final":true} 示例2(对比分析,多工具): 问题:"CNN和RNN的区别" → {"operations":[ {"tool":"vector_search","args":{"query":"CNN RNN 区别 对比","limit":12}}, {"tool":"grep","args":{"query":"CNN|RNN","limit":10}} ],"final":false} → vector找语义关系,grep确保两个主体都覆盖 → chunk足够,{"operations":[],"final":true} 示例3(简单精确查找,单工具足够): 问题:"雷曼公司何时破产" → {"operations":[{"tool":"grep","args":{"query":"雷曼|Lehman","limit":5}}],"final":true} → 返回3个chunk,包含"2008年9月15日申请破产" → 单工具足够 示例4(查找特定格式内容,使用正则): 问题:"找出文中所有的公式编号" → {"operations":[{"tool":"regex_search","args":{"pattern":"公式\\\\s*\\\\d+\\\\.\\\\d+|式\\\\s*\\\\(\\\\d+\\\\)","limit":20}}],"final":true} → 正则匹配"公式3.2"、"式(15)"等格式 → 比grep更精确,避免误匹配 示例5(复杂逻辑查询,使用布尔搜索): 问题:"提到CNN但不涉及图像的内容" → {"operations":[{"tool":"boolean_search","args":{"query":"CNN AND (网络 OR 模型) NOT (图像 OR 视觉)","limit":10}}],"final":false} → 找到讨论CNN网络结构但不涉及图像应用的段落 → 比单独用grep的OR更精确 示例6(查找日期或时间信息,用正则): 问题:"论文发表时间" → {"operations":[ {"tool":"regex_search","args":{"pattern":"\\\\d{4}年|\\\\d{4}-\\\\d{2}|20\\\\d{2}","limit":10}}, {"tool":"grep","args":{"query":"发表|出版|published","limit":5}} ],"final":true} → 正则找日期格式 + grep找相关词汇 示例7(宏观理解,map+fetch): 问题:"生成思维导图" → {"operations":[{"tool":"map","args":{"limit":50}}],"final":false} → 获取意群地图 → {"operations":[ {"tool":"fetch","args":{"groupId":"group-1"}}, {"tool":"fetch","args":{"groupId":"group-5"}}, {"tool":"fetch","args":{"groupId":"group-10"}} ],"final":true,"includeMapInFinalContext":true} → fetch关键意群 + 包含地图 示例8(❌ 错误:重复搜索): 问题:"找电影《猜火车》的引用" 第1轮:{"operations":[{"tool":"grep","args":{"query":"TRAINSPOTTING|猜火车","limit":20}}],"final":false} → 搜索历史显示:GREP搜索 "TRAINSPOTTING|猜火车" → ✓ 4个结果 ❌ 第2轮错误做法:{"operations":[{"tool":"grep","args":{"query":"TRAINSPOTTING|猜火车","limit":20}}],"final":true} (完全相同的query,禁止重复!) ✓ 第2轮正确做法: - 如果4个结果已足够 → {"operations":[],"final":true} - 如果需要更多上下文 → {"operations":[{"tool":"fetch","args":{"groupId":"group-2"}}],"final":true} (fetch包含这些结果的意群,获取完整上下文) ## 限制与原则 - 每轮最多5个操作 - **复杂问题优先多工具并用**(在同一轮operations数组中并发执行) - vector_search擅长语义理解,grep擅长精确匹配,**两者结合效果最佳** - 简单精确查找可以只用grep,但综合性/分析性问题**必须多工具** - 搜索limit不要超过20(避免噪音) - 只fetch真正需要的意群(2-5个为宜) - 优先chunk片段,确实不足才fetch意群 ## 返回格式要求(严格遵守) ⚠️ **你只能返回JSON格式的检索计划,但可以在JSON前后添加简短说明** **允许的输出格式**: 1. 纯JSON(推荐) 2. 简短说明 + JSON(可选,用于解释检索策略) 例如:"需要获取研究背景信息,使用向量搜索和关键词检索。\n{...JSON...}" **禁止输出的内容**: * ❌ mermaid图表、思维导图代码 * ❌ 对用户问题的直接回答(如"该研究的背景是...") * ❌ 详细的论述或分析 * ✓ 可以:简短的检索策略说明(1-2句话) **正确格式**: { "operations": [...], "final": true/false, "taskStatus": { // 可选,复杂问题建议使用 "completed": ["已完成的任务..."], "current": "当前任务", "pending": ["待做任务..."] }, "includeMapInFinalContext": true/false, "includeGroupListInFinalContext": true/false } **给最终AI的上下文自主控制**(可选字段,默认false): - **includeMapInFinalContext**: 是否包含完整地图结构 * 宏观任务(思维导图、总结全文)→ true * 微观任务(查找具体事实)→ false - **includeGroupListInFinalContext**: 是否包含所有意群的简要列表(ID+字数+摘要) * 需要全局视角但不需要详细地图 → true * 只需要精确检索结果 → false - **你fetch的意群**: 会自动包含在最终上下文中(full/digest粒度) **灵活组合示例**: - 微观查询:"公式7是什么" → fetch(group-5) + 无地图无列表 - 宏观总结:"生成思维导图" → map + includeMapInFinalContext:true + includeGroupListInFinalContext:true - 混合任务:"分析影响因素" → 搜索 + fetch若干意群 + includeGroupListInFinalContext:true(提供背景) 示例JSON: - 示例1(继续检索,带任务追踪): {"operations":[{"tool":"fetch","args":{"groupId":"group-1"}}],"final":false,"taskStatus":{"completed":["✓ 已搜索背景"],"current":"→ 获取方法论","pending":["待查实验"]}} - 示例2(完成,仅fetch内容): {"operations":[],"final":true} - 示例3(完成,需要地图+列表): {"operations":[],"final":true,"includeMapInFinalContext":true,"includeGroupListInFinalContext":true} - 示例4(完成,带任务总结): {"operations":[],"final":true,"taskStatus":{"completed":["✓ 背景","✓ 方法","✓ 实验","✓ 结论"],"current":"→ 完成","pending":[]}} **taskStatus字段说明**(详见上方"任务追踪机制"章节)`; // 构建任务状态提示文本 let taskStatusText = ''; if (round > 0 && (taskStatusHistory.completed.length > 0 || taskStatusHistory.current || taskStatusHistory.pending.length > 0)) { const parts = []; if (taskStatusHistory.completed.length > 0) { parts.push(`已完成: ${taskStatusHistory.completed.join('; ')}`); } if (taskStatusHistory.current) { parts.push(`上轮任务: ${taskStatusHistory.current}`); } if (taskStatusHistory.pending.length > 0) { parts.push(`待完成: ${taskStatusHistory.pending.join('; ')}`); } taskStatusText = '\n\n【任务追踪状态】\n' + parts.join('\n'); } let content = `文档总览:\n${gist}\n\n用户问题:\n${String(userQuestion || '')}${searchHistoryText}${taskStatusText}\n\n${listText ? '【候选意群地图】:\n' + listText + '\n\n' : ''}【已获取内容】:\n${fetchedSummary}`; // 调用LLM规划(支持重试) yield { type: 'status', phase: 'planning', round, message: 'LLM规划中...' }; const apiKey = config.apiKey; let plannerOutput = null; let retryCount = 0; const maxRetries = 2; // 最多重试2次 let parseSuccess = false; let plan = null; // 重试循环:如果JSON解析失败,给AI反馈并重试 while (!parseSuccess && retryCount <= maxRetries) { if (retryCount > 0) { yield { type: 'info', message: `JSON格式错误,正在重试 (${retryCount}/${maxRetries})...` }; } plannerOutput = await window.ChatbotCore.singleChunkSummary(sys, content, config, apiKey); // 统计规划器token使用 const plannerInputTokens = estimateTokens(sys) + estimateTokens(content); const plannerOutputTokens = estimateTokens(plannerOutput); const plannerTotalTokens = plannerInputTokens + plannerOutputTokens; yield { type: 'token_usage', phase: 'planner', round, tokens: { input: plannerInputTokens, output: plannerOutputTokens, total: plannerTotalTokens } }; // 尝试解析计划 try { let cleaned = plannerOutput .replace(/```jsonc?|```tool|```/gi,'') .replace(/[\u0000-\u001f]/g, ' ') .trim(); if (!cleaned) { yield { type: 'warning', message: '规划输出为空,使用后备策略' }; break; } // 如果输出不是JSON(以中文或非{开头),尝试提取JSON if (!cleaned.startsWith('{') && !cleaned.startsWith('[')) { // 尝试提取JSON块 const jsonMatch = cleaned.match(/\{[\s\S]*\}/); if (jsonMatch) { cleaned = jsonMatch[0]; console.log('[streamingMultiHop] 从文本中提取JSON:', cleaned.substring(0, 100)); } else { // 尝试从文本中提取operations和final信息 console.log('[streamingMultiHop] 尝试从自然语言提取结构:', cleaned.substring(0, 200)); // 检查是否明确表达"已完成"、"足够"等含义 if (cleaned.match(/已.*足够|无需.*操作|不需要.*继续|已经.*完成|内容.*充足/i)) { plan = { operations: [], final: true }; console.log('[streamingMultiHop] 识别为完成信号,设置final=true'); } else { // 完全没有JSON且无法解析,返回空操作+final console.warn('[streamingMultiHop] LLM输出非JSON格式,自动结束:', cleaned.substring(0, 100)); yield { type: 'warning', message: `第 ${round + 1} 轮LLM返回非JSON格式,已获取 ${fetched.size} 个意群,使用已有内容` }; break; } } } if (!plan) { try { plan = JSON.parse(cleaned); } catch (parseErr) { // 尝试各种清理策略 let normalized = cleaned // 移除所有控制字符和特殊空白 .replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ') // 修复中文引号 .replace(/[""]/g, '"') .replace(/['']/g, "'") // 修复JSON中的常见错误 .replace(/"(\w+)"\s*:\s*'([^']*)'/g, '"$1":"$2"') // 单引号改双引号 .replace(/(\w+):/g, '"$1":') // 无引号键名加引号 .replace(/,\s*}/g, '}') // 移除对象尾随逗号 .replace(/,\s*]/g, ']') // 移除数组尾随逗号 // 修复 ," "final": 这种错误 .replace(/,\s*"\s+"(\w+)"\s*:/g, ',"$1":') // 修复 "operations" " 这种空格 .replace(/"(\w+)"\s+"/g, '"$1":') // 修复键名周围的多余空格 .replace(/"\s+([\w-]+)"\s*:/g, '"$1":') // 修复值周围的多余空格 .replace(/:\s*"\s+/g, ':"') .replace(/\s+"/g, '"') // 修复final前的空格 .replace(/"\s*final\s*"/gi, '"final"') // 修复operations .replace(/"operations"\s*"/i, '"operations":') // 移除注释 .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/.*/g, '') // 压缩空格 .replace(/\s+/g, ' ') .trim(); console.log('[streamingMultiHop] 清理后的JSON:', normalized.substring(0, 200)); plan = JSON.parse(normalized); } } yield { type: 'plan', round, data: { operations: plan.operations || [], final: plan.final, taskStatus: plan.taskStatus || null // 传递任务状态 } }; // 捕获AI的任务状态更新 if (plan.taskStatus) { // 更新任务追踪历史 if (Array.isArray(plan.taskStatus.completed)) { taskStatusHistory.completed = plan.taskStatus.completed; } if (typeof plan.taskStatus.current === 'string') { taskStatusHistory.current = plan.taskStatus.current; } if (Array.isArray(plan.taskStatus.pending)) { taskStatusHistory.pending = plan.taskStatus.pending; } // 输出任务状态到UI yield { type: 'task_status', round, status: { completed: taskStatusHistory.completed, current: taskStatusHistory.current, pending: taskStatusHistory.pending } }; console.log('[StreamingMultiHop] 任务状态更新:', taskStatusHistory); } // 捕获AI关于地图包含的决策 if (plan.includeMapInFinalContext === true) { aiRequestedMapInFinalContext = true; console.log('[StreamingMultiHop] AI请求在最终上下文中包含地图概览'); } // 捕获AI关于意群列表包含的决策 if (plan.includeGroupListInFinalContext === true) { aiRequestedGroupListInFinalContext = true; console.log('[StreamingMultiHop] AI请求在最终上下文中包含意群简要列表'); } parseSuccess = true; // 成功解析,退出重试循环 } catch (e) { console.error('[streamingMultiHop] 解析计划失败:', e.message); console.error('[streamingMultiHop] LLM原始输出:', plannerOutput); retryCount++; if (retryCount <= maxRetries) { // 还有重试机会,构造错误提示 const errorFeedback = `\n\n【上次输出解析失败】\n错误信息: ${e.message}\n你的输出: ${plannerOutput.substring(0, 300)}\n\n请注意:\n1. 必须输出严格的JSON格式\n2. 字符串中的特殊字符需要转义(如 $ 应写成 \\$ 或避免使用)\n3. 不要在JSON字符串值中使用 | $ \\ 等特殊字符,或使用中文替代\n4. 示例正确格式:{"operations":[{"tool":"grep","args":{"query":"公式 模型 回归","limit":10}}],"final":false}\n\n请重新输出正确的JSON:`; content = content + errorFeedback; console.log(`[streamingMultiHop] 准备第 ${retryCount} 次重试,已添加错误提示`); } else { // 重试耗尽,执行原有的fallback逻辑 yield { type: 'error', phase: 'parse_plan', message: `解析计划失败(已重试${maxRetries}次): ${e.message}`, raw: plannerOutput }; // 如果已经获取到内容,直接使用,不要丢弃 if (fetched.size > 0) { yield { type: 'warning', message: `第 ${round + 1} 轮规划失败,但已获取 ${fetched.size} 个意群,使用已有内容` }; break; // 结束for循环,使用已fetch的内容 } // 只有在完全没有内容时才使用后备策略 const fallback = buildFallbackSemanticContext(userQuestion, groups); if (fallback) { yield { type: 'fallback', reason: 'parse-error', context: fallback.context }; return fallback; } break; // 结束for循环 } } } // 结束重试while循环 // 如果重试循环结束但没有成功解析,跳过这一轮 if (!parseSuccess) { console.log('[streamingMultiHop] 解析失败且已耗尽重试次数,跳过本轮'); continue; // 继续下一轮round } let ops = Array.isArray(plan.operations) ? plan.operations : []; // 若首轮规划为空且尚无任何已获取内容,自动注入一次默认检索,避免空转 if (round === 0 && ops.length === 0 && fetched.size === 0) { const buildDefaultOpsFromQuestion = (q, preferVector) => { try { const raw = String(q || '').toLowerCase(); // 去掉无信息量的泛化词 const stop = /(用通俗语言|通俗解释|解释一下|解释|概述|总结|主要内容|全文|请|如何|是什么|简单|大致|大概|说明|讲一讲|讲一下|阐述|简介|介绍|说明一下)/g; const cleaned = raw.replace(stop, ' ').replace(/[\p{P}\p{S}]+/gu, ' ').replace(/\s+/g, ' ').trim(); const tokens = cleaned.split(/\s+/).filter(w => w && w.length > 1); const query = tokens.slice(0, 6).join(' '); if (!query) return []; if (preferVector) { return [{ tool: 'vector_search', args: { query, limit: 8 } }]; } else { return [{ tool: 'grep', args: { query, limit: 12, context: 2000, caseInsensitive: true } }]; } } catch (_) { return []; } }; // 判断是否可用向量(来自前面的配置判断) const docIdTmp = (window.data && window.data.currentPdfName) || 'unknown'; const docCfgTmp = window.data?.multiHopConfig?.[docIdTmp]; const preferVector = docCfgTmp?.useVectorSearch !== false; const injected = buildDefaultOpsFromQuestion(userQuestion, preferVector); if (injected && injected.length > 0) { ops = injected; yield { type: 'info', message: '规划为空,已自动注入默认检索' }; } } // 如果operations为空但已有内容,说明AI认为当前内容已足够 if (ops.length === 0) { if (fetched.size > 0) { yield { type: 'info', message: `AI判断已有 ${fetched.size} 个意群的内容足够回答问题,结束取材` }; break; // 直接使用已有内容,不触发fallback } // 只有在完全没有内容时才使用后备策略 yield { type: 'warning', message: '规划无操作且无已获取内容,使用后备策略' }; const fallback = buildFallbackSemanticContext(userQuestion, groups); if (fallback) { yield { type: 'fallback', reason: 'empty-ops', context: fallback.context }; return fallback; } break; } // 3. 执行工具 for (const [opIndex, op] of ops.entries()) { yield { type: 'tool_start', round, opIndex, tool: op.tool, args: op.args }; try { // 检查文档配置 const docId = (window.data && window.data.currentPdfName) || 'unknown'; const docConfig = window.data?.multiHopConfig?.[docId]; const useSemanticGroups = docConfig?.useSemanticGroups !== false; const useVectorSearch = docConfig?.useVectorSearch !== false; if (op.tool === 'vector_search' && op.args) { if (!useVectorSearch) { throw new Error('向量搜索功能已禁用,vector_search不可用'); } // 向量语义搜索chunks const query = String(op.args.query || userQuestion); const limit = Math.min(Number(op.args.limit) || 15, 30); if (window.SemanticVectorSearch && window.EmbeddingClient?.config?.enabled) { // 获取enrichedChunks const chunks = window.data?.enrichedChunks || []; const res = await window.SemanticVectorSearch.search(query, chunks, { topK: limit, threshold: 0.3 }); // 记录搜索结果(包括无结果的情况) const resultCount = (Array.isArray(res) && res.length) ? res.length : 0; searchHistory.push({ tool: 'vector_search', query, resultCount }); if (resultCount > 0) { // 只添加搜索到的chunks完整文本,不自动fetch意群 res.forEach((r, idx) => { const chunkText = r.text || ''; if (chunkText) { contextParts.push(`【向量搜索片段${idx + 1}】(chunk-${r.chunkId}, 来自${r.belongsToGroup}, 相似度${r.score.toFixed(3)})\n${chunkText}`); } }); // 提取所属意群信息,供AI判断 const groupIds = new Set(); res.forEach(r => { if (r.belongsToGroup) { groupIds.add(r.belongsToGroup); interestedGroups.add(r.belongsToGroup); // 标记AI感兴趣的意群 } }); console.log(`[StreamingMultiHop] 向量搜索命中 ${resultCount} 个chunks,所属意群: [${Array.from(groupIds).join(', ')}]`); // 计算返回内容的token数 const returnedText = res.map(r => r.text).join('\n'); const contentTokens = estimateTokens(returnedText); yield { type: 'tool_result', round, opIndex, tool: 'vector_search', result: res.map(r => ({ chunkId: r.chunkId, belongsToGroup: r.belongsToGroup, score: r.score, rerankScore: r.rerankScore, // 重排分数(如果有) originalScore: r.originalScore, // 原始向量分数(如果有) preview: r.text.substring(0, 500) })), suggestedGroups: Array.from(groupIds), // 提示AI可以fetch这些意群 tokens: contentTokens // 返回内容的token数 }; } else { // 明确返回空结果,便于UI闭环 yield { type: 'tool_result', round, opIndex, tool: 'vector_search', result: [] }; } } } else if (op.tool === 'keyword_search' && op.args) { // 关键词精确匹配 const keywords = Array.isArray(op.args.keywords) ? op.args.keywords : [String(op.args.keywords || '')]; const limit = Math.min(Number(op.args.limit) || 8, 30); if (window.SemanticBM25Search) { // 使用BM25进行关键词搜索chunks(不做n-gram拆分) const chunks = window.data?.enrichedChunks || []; const res = window.SemanticBM25Search.searchChunksKeywords(keywords, chunks, { topK: limit, threshold: 0.0 }); // 记录搜索结果(包括无结果的情况) const resultCount = (Array.isArray(res) && res.length) ? res.length : 0; searchHistory.push({ tool: 'keyword_search', query: keywords.join(','), resultCount }); if (resultCount > 0) { // 只添加搜索到的chunks完整文本,不自动fetch意群 res.forEach((r, idx) => { const chunkText = r.text || ''; if (chunkText) { const matched = keywords.filter(kw => chunkText.includes(kw)).join(', '); contextParts.push(`【关键词搜索片段${idx + 1}】(chunk-${r.chunkId}, 来自${r.belongsToGroup}, 匹配词: ${matched})\n${chunkText}`); } }); // 提取所属意群信息,供AI判断 const groupIds = new Set(); res.forEach(r => { if (r.belongsToGroup) { groupIds.add(r.belongsToGroup); interestedGroups.add(r.belongsToGroup); // 标记AI感兴趣的意群 } }); console.log(`[StreamingMultiHop] 关键词搜索命中 ${resultCount} 个chunks,所属意群: [${Array.from(groupIds).join(', ')}]`); // 计算返回内容的token数 const returnedText = res.map(r => r.text).join('\n'); const contentTokens = estimateTokens(returnedText); yield { type: 'tool_result', round, opIndex, tool: 'keyword_search', result: res.map(r => ({ chunkId: r.chunkId, belongsToGroup: r.belongsToGroup, preview: r.text.substring(0, 500), matchedKeywords: keywords.filter(kw => r.text.includes(kw)) })), suggestedGroups: Array.from(groupIds), // 提示AI可以fetch这些意群 tokens: contentTokens }; } else { // 明确返回空结果,便于UI闭环 yield { type: 'tool_result', round, opIndex, tool: 'keyword_search', result: [] }; } } } else if ((op.tool === 'fetch' || op.tool === 'fetch_group') && op.args) { if (!useSemanticGroups) { throw new Error('意群功能已禁用,fetch不可用'); } const id = op.args.groupId; const gran = (op.args.granularity || (op.tool === 'fetch' ? 'full' : 'digest')); interestedGroups.add(id); // 标记AI感兴趣的意群 const existing = fetched.get(id); // 如果已经有了,检查是否需要升级 if (existing) { const needUpgrade = (existing.granularity === 'summary' && gran !== 'summary') || (existing.granularity === 'digest' && gran === 'full'); if (needUpgrade) { // 需要升级,执行fetch(优先详细接口) let res = null; if (op.tool === 'fetch' && typeof window.SemanticTools?.fetchGroupDetailed === 'function') { res = window.SemanticTools.fetchGroupDetailed(id); } else if (typeof window.SemanticTools?.fetchGroupText === 'function') { res = window.SemanticTools.fetchGroupText(id, gran); } if (res && res.text) { fetched.set(id, { granularity: res.granularity, text: res.text }); const idx = detail.findIndex(d => d.groupId === id); if (idx >= 0) detail[idx].granularity = res.granularity; const ctxIdx = contextParts.findIndex(c => c.startsWith(`【${id}`)); if (ctxIdx >= 0) { contextParts[ctxIdx] = `【${id} - ${res.granularity}】\n${res.text}`; } yield { type: 'tool_result', round, opIndex, tool: op.tool, result: { groupId: id, granularity: res.granularity, preview: res.text.slice(0, 200), action: 'upgraded' }, tokens: estimateTokens(res.text) }; } } else { // 已有更高级别的,跳过 yield { type: 'tool_skip', round, opIndex, tool: op.tool, reason: existing.granularity === gran ? 'already_fetched' : 'higher_granularity_exists', groupId: id, existingGranularity: existing.granularity }; } } else { // 不存在,执行fetch let res = null; if (op.tool === 'fetch' && typeof window.SemanticTools?.fetchGroupDetailed === 'function') { res = window.SemanticTools.fetchGroupDetailed(id); } else if (typeof window.SemanticTools?.fetchGroupText === 'function') { res = window.SemanticTools.fetchGroupText(id, gran); } if (res && res.text) { fetched.set(id, { granularity: res.granularity, text: res.text }); detail.push({ groupId: id, granularity: res.granularity }); contextParts.push(`【${id} - ${res.granularity}】\n${res.text}`); yield { type: 'tool_result', round, opIndex, tool: op.tool, result: { groupId: id, granularity: res.granularity, preview: res.text.slice(0, 200) }, tokens: estimateTokens(res.text) }; } } } else if (op.tool === 'map') { if (!useSemanticGroups) { throw new Error('意群功能已禁用,map不可用'); } // 生成候选意群地图(结构+摘要) const limit = Math.min(Number(op.args?.limit) || 50, candidates.length); const includeStructure = op.args?.includeStructure !== false; const items = candidates.slice(0, limit).map(g => { const struct = g.structure || {}; const parts = [`【${g.groupId}】 ${g.charCount || 0}字`]; if (includeStructure && struct.orderedElements && struct.orderedElements.length > 0) { const typeIcons = { 'title': '📌', 'section': '▸', 'keypoint': '•', 'figure': '🖼', 'table': '📊', 'formula': '📐' }; struct.orderedElements.forEach(elem => { const icon = typeIcons[elem.type] || '-'; parts.push(` ${icon} ${elem.content}`); }); } else { if (g.keywords && g.keywords.length > 0) parts.push(`关键词: ${g.keywords.join('、')}`); const s = g.structure || {}; if (s.sections && s.sections.length > 0) parts.push(`章节: ${s.sections.join('; ')}`); if (s.keyPoints && s.keyPoints.length > 0) parts.push(`要点: ${s.keyPoints.join('; ')}`); } if (g.summary) parts.push(`摘要: ${g.summary}`); return parts.join('\n'); }); const mapText = items.join('\n\n'); window._multiHopLastMapText = mapText; // 供下一轮规划使用 // 记录已生成地图(用于最终汇总时判断是否需要包含地图) window._multiHopHasMap = true; window._multiHopMapSummary = { text: mapText, count: items.length, includeStructure: includeStructure }; // 自动策略:AI主动调用map工具,说明需要地图信息,自动包含到最终上下文 aiRequestedMapInFinalContext = true; console.log('[StreamingMultiHop] AI调用map工具,自动将地图包含到最终上下文'); yield { type: 'tool_result', round, opIndex, tool: 'map', result: { count: items.length }, tokens: estimateTokens(mapText) }; } else if (op.tool === 'grep' && op.args) { // 传统文本搜索,支持多关键词OR查询(用|分隔) const q = String(op.args.query || '').trim(); const limit = Math.min(Number(op.args.limit) || 20, 100); const ctxChars = Math.min(Number(op.args.context) || 2000, 4000); // 默认提升到2000字符 const caseInsensitive = !!op.args.caseInsensitive; const chunks = window.data?.enrichedChunks || []; const hits = []; if (q) { // 支持OR逻辑:将 "方程|公式|k-ε" 拆分为多个关键词 const keywords = q.includes('|') ? q.split('|').map(k => k.trim()).filter(k => k) : [q]; console.log(`[StreamingMultiHop] GREP搜索关键词: [${keywords.join(', ')}]`); // 1) 整篇文档 const docText = String(docContentInfo.translation || docContentInfo.ocr || ''); if (docText) { const hay = caseInsensitive ? docText.toLowerCase() : docText; for (const keyword of keywords) { const needle = caseInsensitive ? keyword.toLowerCase() : keyword; let from = 0; while (from < hay.length) { const pos = hay.indexOf(needle, from); if (pos < 0) break; const end = pos + keyword.length; const s = Math.max(0, pos - ctxChars); const e = Math.min(docText.length, end + ctxChars); const snippet = docText.slice(s, e); hits.push({ preview: snippet, matchOffset: pos, matchLength: keyword.length, matchedKeyword: keyword // 记录匹配的关键词 }); from = end; if (hits.length >= limit) break; } if (hits.length >= limit) break; } } // 2) chunks 级(补充) if (hits.length < limit && Array.isArray(chunks) && chunks.length > 0) { for (const keyword of keywords) { const needle = caseInsensitive ? keyword.toLowerCase() : keyword; for (const chunk of chunks) { const text = String(chunk.text || ''); if (!text) continue; const hay = caseInsensitive ? text.toLowerCase() : text; const pos = hay.indexOf(needle); if (pos >= 0) { const end = pos + keyword.length; const s = Math.max(0, pos - ctxChars); const e = Math.min(text.length, end + ctxChars); const snippet = text.slice(s, e); hits.push({ chunkId: chunk.chunkId, belongsToGroup: chunk.belongsToGroup, preview: snippet, matchOffset: pos, matchLength: keyword.length, matchedKeyword: keyword }); if (hits.length >= limit) break; } } if (hits.length >= limit) break; } } } if (hits.length > 0) { // 添加grep搜索到的片段(精确匹配的上下文) console.log(`[StreamingMultiHop] GREP命中 ${hits.length} 个片段,准备添加到contextParts`); hits.forEach((h, idx) => { const src = h.belongsToGroup ? `chunk-${h.chunkId}, 来自${h.belongsToGroup}` : '全文'; const contextItem = `【GREP片段${idx + 1}】(${src})\n${h.preview}`; contextParts.push(contextItem); console.log(`[StreamingMultiHop] 添加GREP片段${idx + 1},长度: ${contextItem.length}字符`); }); // 提取所属意群信息,供AI判断 const groupIds = new Set(); hits.forEach(h => { if (h.belongsToGroup) { groupIds.add(h.belongsToGroup); interestedGroups.add(h.belongsToGroup); // 标记AI感兴趣的意群 } }); if (groupIds.size > 0) { console.log(`[StreamingMultiHop] GREP搜索命中 ${hits.length} 个片段,所属意群: [${Array.from(groupIds).join(', ')}]`); } else { console.log(`[StreamingMultiHop] GREP搜索命中 ${hits.length} 个片段(来自全文,无所属意群)`); } } else { console.log(`[StreamingMultiHop] GREP搜索"${q}"无结果`); } // 记录搜索结果(包括无结果的情况) searchHistory.push({ tool: 'grep', query: q, resultCount: hits.length }); const groupIds = new Set(); hits.forEach(h => { if (h.belongsToGroup) groupIds.add(h.belongsToGroup); }); // 计算返回内容的token数 const returnedText = hits.map(h => h.preview).join('\n'); const contentTokens = estimateTokens(returnedText); yield { type: 'tool_result', round, opIndex, tool: 'grep', result: hits.map(h => ({ preview: h.preview.substring(0, 500), belongsToGroup: h.belongsToGroup, chunkId: h.chunkId })), suggestedGroups: Array.from(groupIds), // 提示AI可以fetch这些意群 tokens: contentTokens }; } else if (op.tool === 'regex_search' && op.args) { // 正则表达式搜索 if (!window.AdvancedSearchTools) { throw new Error('AdvancedSearchTools 模块未加载'); } const pattern = String(op.args.pattern || ''); const limit = Math.min(Number(op.args.limit) || 10, 50); const ctxChars = Math.min(Number(op.args.context) || 1500, 4000); const caseInsensitive = op.args.caseInsensitive !== false; const docText = String(docContentInfo.translation || docContentInfo.ocr || ''); const chunks = window.data?.enrichedChunks || []; if (!docText && chunks.length === 0) { throw new Error('没有可搜索的文本内容'); } try { const results = window.AdvancedSearchTools.regexSearch( pattern, docText, { limit, context: ctxChars, caseInsensitive } ); if (results.length > 0) { // 添加正则搜索结果到上下文 results.forEach((r, idx) => { contextParts.push(`【正则搜索片段${idx + 1}】(匹配: "${r.match}")\n${r.preview}`); }); // 尝试关联到chunks和意群 const groupIds = new Set(); results.forEach(r => { // 查找包含此匹配的chunk const matchingChunk = chunks.find(chunk => chunk.text && chunk.text.includes(r.match) ); if (matchingChunk?.belongsToGroup) { groupIds.add(matchingChunk.belongsToGroup); interestedGroups.add(matchingChunk.belongsToGroup); } }); console.log(`[StreamingMultiHop] 正则搜索命中 ${results.length} 个片段`); const returnedText = results.map(r => r.preview).join('\n'); const contentTokens = estimateTokens(returnedText); yield { type: 'tool_result', round, opIndex, tool: 'regex_search', result: results.map(r => ({ match: r.match, preview: r.preview.substring(0, 500), groups: r.groups })), suggestedGroups: Array.from(groupIds), tokens: contentTokens }; } else { yield { type: 'tool_result', round, opIndex, tool: 'regex_search', result: [] }; } } catch (regexError) { throw new Error(`正则表达式错误: ${regexError.message}`); } // 记录搜索历史 searchHistory.push({ tool: 'regex_search', query: pattern, resultCount: results.length || 0 }); } else if (op.tool === 'boolean_search' && op.args) { // 布尔逻辑搜索 if (!window.AdvancedSearchTools) { throw new Error('AdvancedSearchTools 模块未加载'); } const query = String(op.args.query || ''); const limit = Math.min(Number(op.args.limit) || 10, 50); const ctxChars = Math.min(Number(op.args.context) || 1500, 4000); const caseInsensitive = op.args.caseInsensitive !== false; const docText = String(docContentInfo.translation || docContentInfo.ocr || ''); const chunks = window.data?.enrichedChunks || []; if (!docText && chunks.length === 0) { throw new Error('没有可搜索的文本内容'); } try { const results = window.AdvancedSearchTools.booleanSearch( query, docText, { limit, context: ctxChars, caseInsensitive } ); if (results.length > 0) { // 添加布尔搜索结果到上下文 results.forEach((r, idx) => { const terms = r.matchedTerms.join(', '); contextParts.push(`【布尔搜索片段${idx + 1}】(匹配词: ${terms}, 相关度: ${r.relevanceScore})\n${r.preview}`); }); // 尝试关联到chunks和意群 const groupIds = new Set(); results.forEach(r => { const matchingChunk = chunks.find(chunk => chunk.text && r.matchedTerms.some(term => chunk.text.includes(term)) ); if (matchingChunk?.belongsToGroup) { groupIds.add(matchingChunk.belongsToGroup); interestedGroups.add(matchingChunk.belongsToGroup); } }); console.log(`[StreamingMultiHop] 布尔搜索命中 ${results.length} 个片段`); const returnedText = results.map(r => r.preview).join('\n'); const contentTokens = estimateTokens(returnedText); yield { type: 'tool_result', round, opIndex, tool: 'boolean_search', result: results.map(r => ({ preview: r.preview.substring(0, 500), matchedTerms: r.matchedTerms, relevanceScore: r.relevanceScore })), suggestedGroups: Array.from(groupIds), tokens: contentTokens }; } else { yield { type: 'tool_result', round, opIndex, tool: 'boolean_search', result: [] }; } } catch (boolError) { throw new Error(`布尔查询错误: ${boolError.message}`); } // 记录搜索历史 searchHistory.push({ tool: 'boolean_search', query, resultCount: results.length || 0 }); } } catch (toolError) { yield { type: 'tool_error', round, opIndex, tool: op.tool, error: toolError.message }; } } // 第一轮结束后,清理不相关的预加载内容 // 保护机制: // 1. 只有当找到足够多(>=3)的感兴趣意群时才清理,避免在"大海捞针"场景中误删 // 2. 如果第一轮没有进行任何搜索,不清理(可能AI还在探索阶段) const hasSearch = searchHistory.some(h => h.tool === 'vector_search' || h.tool === 'keyword_search' || h.tool === 'grep'); console.log(`[StreamingMultiHop] 第${round + 1}轮结束,hasSearch=${hasSearch}, interestedGroups.size=${interestedGroups.size}, contextParts.length=${contextParts.length}`); if (round === 0 && preloadedInFirstRound && interestedGroups.size >= 3 && hasSearch) { // 清理fetched:只保留AI感兴趣的意群 for (const [groupId] of fetched.entries()) { if (!interestedGroups.has(groupId)) { fetched.delete(groupId); } } // 清理detail:只保留AI感兴趣的意群 const filteredDetail = detail.filter(d => interestedGroups.has(d.groupId)); detail.length = 0; detail.push(...filteredDetail); // 清理contextParts:只保留搜索片段和AI感兴趣的意群 const filteredContextParts = contextParts.filter(part => { // 保留搜索片段 if (part.startsWith('【向量搜索片段') || part.startsWith('【关键词搜索片段') || part.startsWith('【GREP片段') || part.startsWith('【正则搜索片段') || part.startsWith('【布尔搜索片段')) { return true; } // 保留AI感兴趣的意群 for (const groupId of interestedGroups) { if (part.startsWith(`【${groupId}`)) { return true; } } return false; }); contextParts.length = 0; contextParts.push(...filteredContextParts); const removedCount = groups.length - interestedGroups.size; if (removedCount > 0) { yield { type: 'info', message: `已清理 ${removedCount} 个不相关意群,保留 ${interestedGroups.size} 个AI感兴趣的内容` }; } } // 检查是否final:优先尊重AI的final判断 const hadContextThisRound = contextParts.length > 0 || detail.length > 0; console.log(`[StreamingMultiHop] 第${round + 1}轮结束检查:contextParts=${contextParts.length}, detail=${detail.length}, final=${plan.final}`); // 若首轮没有任何搜索也没有上下文,发出提示,继续下一轮(已在执行阶段注入默认检索) if (round === 0 && !hasSearch && !hadContextThisRound) { yield { type: 'warning', message: '首轮未产生上下文,将继续并执行默认检索' }; } // 优先判断:AI明确说final=true,直接结束 if (plan.final === true) { yield { type: 'round_end', round, final: true, message: 'AI判断内容已充分,取材完成' }; break; } // 次要判断:AI说final=false,但已经到最后一轮了,强制结束 if (round === maxRounds - 1 && hadContextThisRound) { yield { type: 'round_end', round, final: true, message: '已达最大轮次,使用已获取内容' }; break; } // 继续下一轮 yield { type: 'round_end', round, final: false, message: '继续下一轮取材...' }; } // 4. 汇总上下文 - 智能分层策略 // 先构建AI请求的上下文组件(地图和意群列表) let mapOverview = ''; let groupListOverview = ''; // 1. 地图概览(如果AI请求) if (aiRequestedMapInFinalContext && window._multiHopLastMapText) { mapOverview = `【📋 文档整体结构地图】\n${window._multiHopLastMapText}\n\n`; console.log(`[StreamingMultiHop] AI请求包含地图,已添加地图概览 (${window._multiHopLastMapText.length}字)`); } // 2. 意群简要列表(如果AI请求) if (aiRequestedGroupListInFinalContext && groups && groups.length > 0) { const simplifiedList = groups.map(g => { const charCount = g.charCount || 0; const summary = g.summary || '无摘要'; const keywords = (g.keywords && g.keywords.length > 0) ? ` [${g.keywords.slice(0, 3).join('、')}]` : ''; return `【${g.groupId}】${charCount}字${keywords} - ${summary}`; }).join('\n'); groupListOverview = `【📑 所有意群简要列表】(共${groups.length}个意群)\n${simplifiedList}\n\n`; console.log(`[StreamingMultiHop] AI请求包含意群列表,已添加 ${groups.length} 个意群的简要信息`); } // 检查是否有任何有用内容:搜索片段 OR fetch的意群 OR 地图 OR 意群列表 const hasAnyContent = contextParts.length > 0 || mapOverview || groupListOverview; if (!hasAnyContent) { yield { type: 'warning', message: '未获取到任何上下文,使用后备策略' }; const fallback = buildFallbackSemanticContext(userQuestion, groups); if (fallback) { yield { type: 'fallback', reason: 'empty-context', context: fallback.context }; return fallback; } return null; } // 分层组织:地图概要(可选) + 搜索片段(最精确) + 重点意群(digest) + 背景意群(summary) const searchFragments = []; // 搜索到的chunk完整文本 const summaryParts = []; // summary粒度的意群 const detailParts = []; // digest/full粒度的意群 // 分类contextParts contextParts.forEach(part => { if (part.startsWith('【向量搜索片段') || part.startsWith('【关键词搜索片段') || part.startsWith('【GREP片段') || part.startsWith('【正则搜索片段') || part.startsWith('【布尔搜索片段')) { searchFragments.push(part); } }); // 分类detail中的意群 const granularityCount = { full: 0, digest: 0, summary: 0 }; detail.forEach(d => { const data = fetched.get(d.groupId); const part = `【${d.groupId} - ${d.granularity}】\n${data.text}`; granularityCount[d.granularity] = (granularityCount[d.granularity] || 0) + 1; if (d.granularity === 'summary') { summaryParts.push(part); } else { detailParts.push(part); } }); // 构建分层上下文 - AI自主控制的组件顺序 let selectedContext = ''; const layers = []; // 1. 地图概览(AI请求时) if (mapOverview) { layers.push(mapOverview); } // 2. 意群简要列表(AI请求时,提供全局视角) if (groupListOverview) { layers.push(groupListOverview); } // 3. 搜索片段(最精确的检索结果) if (searchFragments.length > 0) { layers.push(`【🎯 搜索片段】(${searchFragments.length}个精确匹配的chunk,最优先使用)\n${searchFragments.join('\n\n')}`); } // 4. 重点意群(AI fetch的详细内容) if (detailParts.length > 0) { layers.push(`【📖 重点意群】(${detailParts.length}个详细内容,提供上下文)\n${detailParts.join('\n\n')}`); } // 5. 背景意群(summary粒度) if (summaryParts.length > 0) { layers.push(`【📋 背景意群】(${summaryParts.length}个简要摘要,提供全局视角)\n${summaryParts.join('\n\n')}`); } selectedContext = layers.join('\n\n---\n\n'); // 统计最终给AI的总token数 const finalContextTokens = estimateTokens(selectedContext); const stats = { totalGroups: detail.length, searchFragments: searchFragments.length, focusGroups: detailParts.length, backgroundGroups: summaryParts.length, hasMap: aiRequestedMapInFinalContext, hasGroupList: aiRequestedGroupListInFinalContext, finalContextTokens // 最终上下文的token数 }; if (searchFragments.length > 0 || detailParts.length > 0) { const msg = []; if (searchFragments.length > 0) msg.push(`${searchFragments.length}个搜索片段`); if (detailParts.length > 0) { const parts = []; if (granularityCount.full > 0) parts.push(`${granularityCount.full}个full`); if (granularityCount.digest > 0) parts.push(`${granularityCount.digest}个digest`); msg.push(`${detailParts.length}个重点意群(${parts.join('+') || 'mixed'})`); } if (summaryParts.length > 0) msg.push(`${summaryParts.length}个背景意群(summary)`); yield { type: 'info', message: `三层上下文:${msg.join(' + ')}` }; } yield { type: 'complete', context: selectedContext, summary: { groups: detail.map(d => d.groupId), granularity: 'mixed', detail, contextLength: selectedContext.length, stats } }; return { groups: detail.map(d => d.groupId), granularity: 'mixed', detail, context: selectedContext, stats }; } catch (error) { yield { type: 'error', phase: 'unknown', message: error.message, stack: error.stack }; return null; } } // 后备策略(同步版本) function buildFallbackSemanticContext(userQuestion, groups) { try { if (!Array.isArray(groups) || groups.length === 0) return null; let picks = []; try { if (window.SemanticGrouper && typeof window.SemanticGrouper.quickMatch === 'function') { picks = window.SemanticGrouper.quickMatch(String(userQuestion || ''), groups) || []; } } catch (_) {} if (!picks || picks.length === 0) { picks = groups.slice(0, Math.min(3, groups.length)); } const unique = new Set(); const detail = []; const parts = []; picks.forEach(g => { if (!g || unique.size >= 3 || unique.has(g.groupId)) return; unique.add(g.groupId); let fetched = null; try { if (window.SemanticTools && typeof window.SemanticTools.fetchGroupText === 'function') { fetched = window.SemanticTools.fetchGroupText(g.groupId, 'digest'); } } catch (_) {} const text = (fetched && fetched.text) || g.digest || g.summary || g.fullText || ''; if (!text) return; const gran = (fetched && fetched.granularity) || 'digest'; parts.push(`【${g.groupId}】\n关键词: ${(g.keywords || []).join('、')}\n内容(${gran}):\n${text}`); detail.push({ groupId: g.groupId, granularity: gran }); }); if (parts.length === 0) return null; return { groups: Array.from(unique), granularity: 'mixed', detail, context: parts.join('\n\n') }; } catch (e) { console.warn('[buildFallbackSemanticContext] 失败:', e); return null; } } // 导出 window.streamingMultiHopRetrieve = streamingMultiHopRetrieve; console.log('[StreamingMultiHop] 流式多轮取材已加载'); })(window);