Files
bigwo/test2/KNOWLEDGE_BASE_RELEVANCE_OPTIMIZATION.md

468 lines
16 KiB
Markdown
Raw Permalink Normal View History

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