Files
bigwo/test2/KNOWLEDGE_BASE_RELEVANCE_OPTIMIZATION.md

468 lines
16 KiB
Markdown
Raw Permalink 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.

# 知识库回答关联度优化方案
> 版本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 调用