feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异 - 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写 - toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery - nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优 - realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式 - app.js: 健康检查新增 redis/reranker/kbRetrievalMode - 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const ToolExecutor = require('./toolExecutor');
|
||||
const db = require('../db');
|
||||
const redisClient = require('./redisClient');
|
||||
const { hasKnowledgeRouteKeyword } = require('./knowledgeKeywords');
|
||||
|
||||
function normalizeTextForSpeech(text) {
|
||||
@@ -270,14 +271,18 @@ async function resolveReply(sessionId, session, text) {
|
||||
const ragItems = fastResult.hit && Array.isArray(fastResult.results)
|
||||
? fastResult.results.filter(i => i && i.content).map(i => ({ title: i.title || '知识库结果', content: i.content }))
|
||||
: [];
|
||||
console.log(`[resolveReply] fast-path hit in ${Date.now() - _resolveStart}ms session=${sessionId} source=${fastResult.hot_answer ? 'hot_answer' : (fastResult.cache_hit ? 'cache' : 'direct')}`);
|
||||
console.log(`[resolveReply] fast-path hit in ${Date.now() - _resolveStart}ms session=${sessionId} source=${fastResult.hot_answer ? 'hot_answer' : (fastResult.cache_hit ? 'cache' : 'direct')} mode=${fastResult.retrieval_mode || 'answer'}`);
|
||||
if (ragItems.length > 0) {
|
||||
const cleanedText = normalizeTextForSpeech(replyText).replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '');
|
||||
session.handoffSummaryUsed = true;
|
||||
// raw 模式:ragItems 已包含上下文 + 多个 KB 片段,直接透传
|
||||
const isRawMode = fastResult.retrieval_mode === 'raw';
|
||||
const finalRagItems = isRawMode
|
||||
? ragItems
|
||||
: [{ title: '知识库结果', content: normalizeTextForSpeech(replyText).replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '') || replyText }];
|
||||
return {
|
||||
delivery: 'external_rag',
|
||||
speechText: '',
|
||||
ragItems: [{ title: '知识库结果', content: cleanedText || replyText }],
|
||||
ragItems: finalRagItems,
|
||||
source: 'voice_tool',
|
||||
toolName: 'search_knowledge',
|
||||
routeDecision: { route: 'search_knowledge', args: { query: originalText } },
|
||||
@@ -295,10 +300,22 @@ async function resolveReply(sessionId, session, text) {
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文加载:优先 Redis(~5ms),降级 MySQL(~100ms)
|
||||
const _dbStart = Date.now();
|
||||
const recentMessages = await db.getRecentMessages(sessionId, 10).catch(() => []);
|
||||
const _dbMs = Date.now() - _dbStart;
|
||||
if (_dbMs > 50) console.log(`[resolveReply] DB getRecentMessages took ${_dbMs}ms session=${sessionId}`);
|
||||
let recentMessages = null;
|
||||
if (process.env.ENABLE_REDIS_CONTEXT !== 'false') {
|
||||
const redisHistory = await redisClient.getRecentHistory(sessionId, 5).catch(() => null);
|
||||
if (redisHistory && redisHistory.length > 0) {
|
||||
recentMessages = redisHistory;
|
||||
const _dbMs = Date.now() - _dbStart;
|
||||
if (_dbMs > 5) console.log(`[resolveReply] Redis getRecentHistory took ${_dbMs}ms session=${sessionId} items=${redisHistory.length}`);
|
||||
}
|
||||
}
|
||||
if (!recentMessages) {
|
||||
recentMessages = await db.getRecentMessages(sessionId, 10).catch(() => []);
|
||||
const _dbMs = Date.now() - _dbStart;
|
||||
if (_dbMs > 50) console.log(`[resolveReply] DB getRecentMessages took ${_dbMs}ms session=${sessionId}`);
|
||||
}
|
||||
const scopedMessages = session?.handoffSummaryUsed
|
||||
? recentMessages.filter((item) => !/^chat_/i.test(String(item?.source || '')))
|
||||
: recentMessages;
|
||||
@@ -310,6 +327,16 @@ async function resolveReply(sessionId, session, text) {
|
||||
if (routeDecision.route === 'chat' && shouldForceKnowledgeRoute(originalText, context)) {
|
||||
routeDecision = { route: 'search_knowledge', args: { query: originalText } };
|
||||
}
|
||||
// KB保护窗口:60秒内有KB命中,当前非纯闲聊,强制走KB搜索
|
||||
// 防止追问(如"它需要漱口吗")绕过KB走S2S自由编造
|
||||
const KB_PROTECTION_WINDOW_MS = 60000;
|
||||
if (routeDecision.route === 'chat' && session?._lastKbHitAt && (Date.now() - session._lastKbHitAt < KB_PROTECTION_WINDOW_MS)) {
|
||||
const isPureChitchat = /^(喂|你好|嗨|hi|hello|谢谢|谢谢你|谢谢啦|多谢|感谢|再见|拜拜|拜|好的|嗯|哦|行|没事了|不用了|可以了|好的谢谢|没问题|知道了|明白了|了解了|好嘞|好吧|行吧|ok|okay)[,,。!?~\s]*$/i.test(originalText);
|
||||
if (!isPureChitchat) {
|
||||
routeDecision = { route: 'search_knowledge', args: { query: originalText } };
|
||||
console.log(`[resolveReply] KB protection window active, forcing KB route session=${sessionId} lastKbHit=${Math.round((Date.now() - session._lastKbHitAt) / 1000)}s ago`);
|
||||
}
|
||||
}
|
||||
let replyText = '';
|
||||
let source = 'voice_bot';
|
||||
let toolName = null;
|
||||
@@ -368,24 +395,24 @@ async function resolveReply(sessionId, session, text) {
|
||||
: []);
|
||||
|
||||
if (ragItems.length > 0) {
|
||||
let speechText = normalizeTextForSpeech(replyText);
|
||||
session.handoffSummaryUsed = true;
|
||||
if (toolName === 'search_knowledge' && speechText) {
|
||||
const cleanedText = speechText.replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '');
|
||||
return {
|
||||
delivery: 'external_rag',
|
||||
speechText: '',
|
||||
ragItems: [{ title: '知识库结果', content: cleanedText || speechText }],
|
||||
source,
|
||||
toolName,
|
||||
routeDecision,
|
||||
responseMeta,
|
||||
};
|
||||
const isRawMode = toolResult?.retrieval_mode === 'raw';
|
||||
let finalRagItems = ragItems;
|
||||
|
||||
if (toolName === 'search_knowledge' && !isRawMode) {
|
||||
// 旧模式:LLM 加工过的文本,清理后合并为单条
|
||||
const speechText = normalizeTextForSpeech(replyText);
|
||||
if (speechText) {
|
||||
const cleanedText = speechText.replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '');
|
||||
finalRagItems = [{ title: '知识库结果', content: cleanedText || speechText }];
|
||||
}
|
||||
}
|
||||
// raw 模式:ragItems 已包含上下文 + 多个 KB 片段,直接透传给 S2S
|
||||
|
||||
return {
|
||||
delivery: 'external_rag',
|
||||
speechText: '',
|
||||
ragItems,
|
||||
ragItems: finalRagItems,
|
||||
source,
|
||||
toolName,
|
||||
routeDecision,
|
||||
|
||||
Reference in New Issue
Block a user