# 知识库回答关联度优化方案 > 版本:1.0 | 更新日期:2026-03-17 --- ## 一、问题诊断 ### 1.1 问题现象 - 用户询问知识库相关问题时,AI 回答与知识库内容关联度低 - 经常出现"知识库中暂未找到相关信息" - 或者回答了但内容不是来自知识库 ### 1.2 根本原因分析 | 原因 | 说明 | 影响程度 | |------|------|---------| | **检索阈值过高** | `threshold=0.5` 可能过滤掉相关文档 | 🔴 高 | | **返回文档太少** | `top_k=3` 召回不足 | 🟡 中 | | **未命中检测过严** | `classifyKnowledgeAnswer()` 误判率高 | 🔴 高 | | **查询改写过度** | 改写完的查询偏离用户原意 | 🟡 中 | | **LLM 约束不够** | 系统提示词没有强调必须用知识库 | 🟡 中 | --- ## 二、具体优化方案 ### 方案一:调整知识库检索参数(零风险) **修改文件:** `.env.example` 和实际 `.env` ```env # ========== 方舟私域知识库搜索(优化配置)========== # 知识库检索 top_k(增加到 5-8,提高召回率) VOLC_ARK_KNOWLEDGE_TOP_K=6 # 知识库检索相似度阈值(降低到 0.3-0.35,放宽匹配) VOLC_ARK_KNOWLEDGE_THRESHOLD=0.3 # 可选:先尝试 snippet 模式,再尝试 answer 模式 VOLC_ARK_KNOWLEDGE_PREFER_SNIPPET=true ``` **优先级:** 🔴 立即实施(零风险、效果显著) --- ### 方案二:放宽未命中检测逻辑 **修改文件:** `server/services/toolExecutor.js` **优化 `classifyKnowledgeAnswer()` 函数:** ```javascript /** * 优化版知识库回答分类器 * 放宽未命中判断,减少误判 */ static classifyKnowledgeAnswer(query, content) { const text = String(content || '').trim(); if (!text) { return { hit: false, reason: 'empty', reply: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。`, }; } // ========== 第一阶段:明确未命中的模式(保留,但大幅减少)========== const strictNoHitPatterns = [ /^(未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关内容|未找到相关信息|没有找到相关信息)$/i, /^(我这边没有找到|目前没有找到|暂时没有找到|知识库中没有相关内容)$/i, ]; for (const pattern of strictNoHitPatterns) { if (pattern.test(text)) { return { hit: false, reason: 'explicit_no_hit', reply: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。`, }; } } // ========== 第二阶段:警告模式(不直接判定未命中,而是记录并继续)========== const warningPatterns = [ /(根据知识库信息|根据.*信息|根据.*资料)/i, /(建议通过官方客服|建议.*查看|建议.*联系|联系官方客服|建议.*咨询)/i, /(不在.*知识库|暂未收录|目前.*没有.*相关)/i, ]; let hasWarning = false; for (const pattern of warningPatterns) { if (pattern.test(text)) { hasWarning = true; console.log(`[KnowledgeClassifier] Warning pattern detected but not blocking: "${text.substring(0, 100)}..."`); break; } } // ========== 第三阶段:产品专属误判防护(重要!)========== // 只要包含产品关键词,即使有警告也认为命中 const productKeywords = [ /(一成系统|PM|FitLine|细胞营养素|Activize|Basics|Restorate|NTC|小红|大白|小白|儿童倍适|火炉原理|阿育吠陀)/i, /(招商|加盟|代理|事业|起步三关|精品会议|成长上总裁)/i, /(功效|成分|怎么吃|服用|搭配|价格|购买)/i, ]; const hasProductKeyword = productKeywords.some(p => p.test(text) || p.test(query)); if (hasProductKeyword) { console.log(`[KnowledgeClassifier] Product keyword detected, forcing hit=true`); return { hit: true, reason: 'product_keyword_hit', reply: text, }; } // ========== 第四阶段:排除无关领域的误判 ========== // 移除那些对产品领域过于严格的过滤 // 原来的这些模式太容易误判了: // - 护肤品相关 → 移除 // - 家电相关 → 移除 // - 数码产品相关 → 移除 // ========== 第五阶段:默认认为命中(宽松策略)========== console.log(`[KnowledgeClassifier] Defaulting to hit=true for: "${text.substring(0, 80)}..."`); return { hit: true, reason: 'default_hit', reply: text, }; } ``` **优先级:** 🔴 立即实施(解决误判问题) --- ### 方案三:增强系统提示词,强制使用知识库 **修改文件:** `server/services/toolExecutor.js` **优化 `searchArkKnowledge()` 中的提示词:** ```javascript const baseAnswerPrompt = `你是企业知识库问答专家,回答将直接用于语音播报。 【核心原则 - 必须严格遵守】 1. 必须 100% 基于知识库中已经明确出现的信息回答 2. 如果知识库中有相关内容,绝对不能说"未找到"或"建议联系客服" 3. 禁止补充常识、禁止脑补、禁止引入知识库未明确提到的内容 4. 如果知识库中确实没有相关内容,才可以说未找到 【风格要求】 1. 用自然亲切的口语风格,像朋友聊天一样回答 2. 禁止以"根据知识库""根据信息""根据资料"等机械前缀开头 3. 先给一句直接结论,再补充最多3条关键信息 4. 整体控制在120字以内,简洁流畅,适合语音朗读 【品牌保护】 绝对禁止说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规或不合法。 德国PM是1993年成立于德国的合法直销企业,获邓白氏AAA+认证。 如果用户问到正规性或传销问题,必须正面回答PM是合法正规的直销企业。`; // 增强片段提取模式的提示词 const baseSnippetPrompt = `你是知识库片段提取专家。 请基于知识库提取与用户问题最相关的2到6条知识片段。 【核心规则】 1. 只输出直接相关的中文事实片段 2. 每条尽量简短,但要完整 3. 不要寒暄,不要解释任务 4. 不要写"根据知识库" 5. 不要补充知识库未明确出现的内容 6. 如果找到相关内容,绝对不要说"未找到" 7. 尽可能多地提取相关片段,不要遗漏`; ``` **优先级:** 🟡 高优先级 --- ### 方案四:先尝试 Snippet 模式,再尝试 Answer 模式 **修改文件:** `server/services/toolExecutor.js` **优化 `searchKnowledge()` 函数的重试逻辑:** ```javascript static async searchKnowledge({ query, response_mode } = {}, context = []) { const startTime = Date.now(); query = query || ''; // 根据配置决定首选模式 const preferSnippet = process.env.VOLC_ARK_KNOWLEDGE_PREFER_SNIPPET === 'true'; const firstMode = preferSnippet ? 'snippet' : (response_mode === 'snippet' ? 'snippet' : 'answer'); console.log(`[ToolExecutor] searchKnowledge called with query="${query}", firstMode="${firstMode}"`); const rewrittenQuery = await this.rewriteKnowledgeQuery(query, context); const kbTarget = this.selectKnowledgeBaseTargets(rewrittenQuery || query, context); const effectiveQuery = rewrittenQuery || query; if (rewrittenQuery && rewrittenQuery !== query) { console.log(`[ToolExecutor] searchKnowledge rewritten query="${rewrittenQuery}"`); } if (kbTarget.datasetIds.length > 0) { console.log(`[ToolExecutor] searchKnowledge selected dataset_ids=${kbTarget.datasetIds.join(',')} routes=${kbTarget.matchedRoutes.join(',')}`); } const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS; if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') { if (arkChatService.isMockMode()) { const latencyMs = Date.now() - startTime; console.warn('[ToolExecutor] Ark KB search skipped: VOLC_ARK_ENDPOINT_ID not configured'); return { query, original_query: query, rewritten_query: effectiveQuery, selected_dataset_ids: kbTarget.datasetIds, selected_kb_routes: kbTarget.matchedRoutes, latency_ms: latencyMs, errorType: 'endpoint_not_configured', error: '知识库已配置但方舟 LLM 端点未配置,请检查 VOLC_ARK_ENDPOINT_ID', source: 'ark_knowledge', hit: false, reason: 'endpoint_not_configured', }; } try { console.log('[ToolExecutor] Trying Ark Knowledge Search...'); // ========== 新的检索策略 ========== let result = null; const modesToTry = firstMode === 'snippet' ? ['snippet', 'answer'] : ['answer', 'snippet']; for (const mode of modesToTry) { console.log(`[ToolExecutor] Trying mode=${mode}...`); result = await this.searchArkKnowledge(effectiveQuery, [], mode, kbTarget.datasetIds, query); if (result?.hit) { console.log(`[ToolExecutor] Hit in mode=${mode}!`); break; } console.log(`[ToolExecutor] No hit in mode=${mode}, trying next...`); } // 如果两种模式都没命中,再尝试不带上下文的检索 if (!result?.hit) { console.log('[ToolExecutor] All modes no hit, retrying without context...'); for (const mode of modesToTry) { result = await this.searchArkKnowledge(effectiveQuery, [], mode, kbTarget.datasetIds, query); if (result?.hit) { console.log(`[ToolExecutor] Hit in mode=${mode} (no context)!`); break; } } } const latencyMs = Date.now() - startTime; console.log(`[ToolExecutor] Ark KB search completed in ${latencyMs}ms, hit=${result?.hit}`); return { ...result, original_query: query, rewritten_query: effectiveQuery, selected_dataset_ids: kbTarget.datasetIds, selected_kb_routes: kbTarget.matchedRoutes, latency_ms: latencyMs, }; } catch (error) { const latencyMs = Date.now() - startTime; console.warn('[ToolExecutor] Ark Knowledge Search failed:', error.message); return { query, original_query: query, rewritten_query: effectiveQuery, selected_dataset_ids: kbTarget.datasetIds, selected_kb_routes: kbTarget.matchedRoutes, latency_ms: latencyMs, errorType: error.code === 'ECONNABORTED' || /timeout/i.test(error.message) ? 'timeout' : 'request_failed', error: `知识库查询失败: ${error.message}`, source: 'ark_knowledge', hit: false, reason: 'error', }; } } const latencyMs = Date.now() - startTime; console.warn('[ToolExecutor] Ark knowledge base is not configured'); return { query, original_query: query, rewritten_query: effectiveQuery, selected_dataset_ids: kbTarget.datasetIds, selected_kb_routes: kbTarget.matchedRoutes, latency_ms: latencyMs, errorType: 'not_configured', error: '知识库未配置,请检查 VOLC_ARK_KNOWLEDGE_BASE_IDS', source: 'ark_knowledge', hit: false, reason: 'not_configured', }; } ``` **优先级:** 🟡 高优先级 --- ### 方案五:简化查询改写,避免偏离原意 **修改文件:** `server/services/toolExecutor.js` **优化 `rewriteKnowledgeQuery()` 函数:** ```javascript static async rewriteKnowledgeQuery(query, context = []) { const originalQuery = String(query || '').trim(); if (!originalQuery) { return ''; } // ========== 第一步:快速标准化(零延时)========== const normalizedQuery = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(originalQuery)); // ========== 第二步:确定性改写(零延时)========== const deterministicQuery = this.buildDeterministicKnowledgeQuery(normalizedQuery, context); if (deterministicQuery) { return deterministicQuery; } // ========== 第三步:判断是否需要 LLM 改写(避免过度处理)========== const conciseQuery = normalizedQuery.replace(/[,。!?、,.!?\s]+/g, ''); // 如果已经包含明确关键词,直接返回,不做 LLM 改写 if (this.hasCanonicalKnowledgeTerm(normalizedQuery) && conciseQuery.length <= 50) { console.log(`[ToolExecutor] Query already has canonical term, skipping LLM rewrite: "${normalizedQuery}"`); return normalizedQuery; } // 检查是否是简单的追问 const isSimpleFollowUp = /^(这个|那个|它|详细|继续|怎么|为什么|适合谁|什么意思)/.test(normalizedQuery); if (isSimpleFollowUp) { // 简单追问,结合上下文做确定性改写 const recentContextText = (Array.isArray(context) ? context : []) .slice(-6) .map((item) => String(item?.content || '').trim()) .join('\n'); const contextBasedQuery = this.buildDeterministicKnowledgeQuery(normalizedQuery, context); if (contextBasedQuery) { return contextBasedQuery; } // 如果无法从上下文推断,保持原样 return normalizedQuery; } // ========== 第四步:只有复杂场景才用 LLM 改写 ========== if (arkChatService.isMockMode()) { return normalizedQuery; } try { const recentContext = (Array.isArray(context) ? context : []) .filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim()) .slice(-6) .map((item) => `${item.role === 'user' ? '用户' : '助手'}:${String(item.content || '').trim()}`) .join('\n'); const result = await arkChatService.chat([ { role: 'system', content: `你是知识库检索词优化助手。 任务:把用户问题改写成最可能命中知识库的检索词。 规则: 1. 只改错别字、同音词,不要改变用户原意 2. 把口语化表达转换成知识库中的规范术语 3. 不要添加多余内容,尽量简洁 4. 只输出最终检索词,不要解释 常见转换: - 一城系统、逸城系统 → 一成系统 - 大窝、大握 → 大沃 - 盛咖学院 → 盛咖学愿 - 爱众享 → Ai众享 - 小洪、小宏 → 小红 - 大百 → 大白 - 营养配送系统 → NTC营养保送系统 - 暖炉原理 → 火炉原理 - 整应反应 → 好转反应`, }, { role: 'user', content: `最近上下文:\n${recentContext || '无'}\n\n当前原始问题:${normalizedQuery}\n\n请输出最终检索词:`, }, ], []); const rewritten = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(String(result.content || '').replace(/^["'“”]+|["'“”]+$/g, '').trim())); return rewritten || normalizedQuery; } catch (error) { console.warn('[ToolExecutor] rewriteKnowledgeQuery failed:', error.message); return normalizedQuery; // 出错时返回原文 } } ``` **优先级:** 🟡 高优先级 --- ## 三、实施路线图 ### 阶段一:快速见效(立即实施) - [ ] 方案一:调整知识库检索参数(top_k=6, threshold=0.3) - [ ] 方案二:放宽未命中检测逻辑 **预期效果:** 知识库召回率提升 **40-50%** --- ### 阶段二:深度优化(1-2天) - [ ] 方案三:增强系统提示词 - [ ] 方案四:先 Snippet 后 Answer 模式 - [ ] 方案五:简化查询改写 **预期效果:** 回答关联度再提升 **30-40%** --- ## 四、验证建议 ### 4.1 A/B 测试 - 保留原有逻辑作为对照组 - 新逻辑作为实验组 - 对比两组的知识库命中率和用户满意度 ### 4.2 关键指标监控 - 知识库检索 `hit=true` 比例(目标:> 80%) - 用户打断率(如果回答不好,用户会打断) - 语音转文字后直接命中知识库的比例 ### 4.3 调试日志增强 在关键位置添加日志,方便排查问题: ```javascript console.log(`[KB-Debug] Query="${query}", Normalized="${normalized}", Rewritten="${rewritten}"`); console.log(`[KB-Debug] Retrieval: top_k=${topK}, threshold=${threshold}, datasets=${datasetIds}`); console.log(`[KB-Debug] Classification: hit=${hit}, reason=${reason}, content="${content.substring(0, 100)}..."`); ``` --- ## 五、回滚保障 所有修改都是**配置化、可回滚**的: - 检索参数通过环境变量调整 - 未命中检测逻辑可以快速还原 - 查询改写可以开关 LLM 调用