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