Files
bigwo/test2/KNOWLEDGE_BASE_RELEVANCE_OPTIMIZATION.md

16 KiB
Raw Blame History

知识库回答关联度优化方案

版本1.0 | 更新日期2026-03-17


一、问题诊断

1.1 问题现象

  • 用户询问知识库相关问题时AI 回答与知识库内容关联度低
  • 经常出现"知识库中暂未找到相关信息"
  • 或者回答了但内容不是来自知识库

1.2 根本原因分析

原因 说明 影响程度
检索阈值过高 threshold=0.5 可能过滤掉相关文档 🔴
返回文档太少 top_k=3 召回不足 🟡
未命中检测过严 classifyKnowledgeAnswer() 误判率高 🔴
查询改写过度 改写完的查询偏离用户原意 🟡
LLM 约束不够 系统提示词没有强调必须用知识库 🟡

二、具体优化方案

方案一:调整知识库检索参数(零风险)

修改文件: .env.example 和实际 .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() 函数:

/**
 * 优化版知识库回答分类器
 * 放宽未命中判断,减少误判
 */
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() 中的提示词:

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() 函数的重试逻辑:

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() 函数:

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 调试日志增强

在关键位置添加日志,方便排查问题:

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 调用