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:
@@ -23,6 +23,7 @@ const {
|
||||
shouldForceKnowledgeRoute,
|
||||
resolveReply,
|
||||
} = require('./realtimeDialogRouting');
|
||||
const ToolExecutor = require('./toolExecutor');
|
||||
const {
|
||||
DEFAULT_VOICE_ASSISTANT_PROFILE,
|
||||
resolveAssistantProfile,
|
||||
@@ -30,6 +31,7 @@ const {
|
||||
buildVoiceGreeting,
|
||||
} = require('./assistantProfileConfig');
|
||||
const { getAssistantProfile } = require('./assistantProfileService');
|
||||
const redisClient = require('./redisClient');
|
||||
|
||||
const sessions = new Map();
|
||||
|
||||
@@ -163,6 +165,7 @@ function persistUserSpeech(session, text) {
|
||||
session.latestUserTurnSeq = (session.latestUserTurnSeq || 0) + 1;
|
||||
resetIdleTimer(session);
|
||||
db.addMessage(session.sessionId, 'user', cleanText, 'voice_asr').catch((e) => console.warn('[NativeVoice][DB] add user failed:', e.message));
|
||||
redisClient.pushMessage(session.sessionId, { role: 'user', content: cleanText, source: 'voice_asr' }).catch(() => {});
|
||||
sendJson(session.client, {
|
||||
type: 'subtitle',
|
||||
role: 'user',
|
||||
@@ -185,6 +188,7 @@ function persistAssistantSpeech(session, text, { source = 'voice_bot', toolName
|
||||
resetIdleTimer(session);
|
||||
if (persistToDb) {
|
||||
db.addMessage(session.sessionId, 'assistant', cleanText, source, toolName, meta).catch((e) => console.warn('[NativeVoice][DB] add assistant failed:', e.message));
|
||||
redisClient.pushMessage(session.sessionId, { role: 'assistant', content: cleanText, source }).catch(() => {});
|
||||
}
|
||||
sendJson(session.client, {
|
||||
type: 'subtitle',
|
||||
@@ -418,7 +422,7 @@ async function processReply(session, text, turnSeq = session.latestUserTurnSeq |
|
||||
// 防止用户质疑/纠正产品信息时S2S自由编造(如"粉末来的呀你搞错了吧")
|
||||
const KB_PROTECTION_WINDOW_MS = 60000;
|
||||
if (!isKnowledgeCandidate && session._lastKbHitAt && (Date.now() - session._lastKbHitAt < KB_PROTECTION_WINDOW_MS)) {
|
||||
const isPureChitchat = /^(喂|你好|嗨|谢谢|再见|拜拜|好的|嗯|哦|行|没事了|不用了|可以了)[,,。!?\s]*$/.test(cleanText);
|
||||
const isPureChitchat = /^(喂|你好|嗨|hi|hello|谢谢|谢谢你|谢谢啦|多谢|感谢|再见|拜拜|拜|好的|嗯|哦|行|没事了|不用了|可以了|好的谢谢|没问题|知道了|明白了|了解了|好嘞|好吧|行吧|ok|okay)[,,。!?~\s]*$/i.test(cleanText);
|
||||
if (!isPureChitchat) {
|
||||
isKnowledgeCandidate = true;
|
||||
console.log(`[NativeVoice] KB protection window active, promoting to kbCandidate session=${session.sessionId} lastKbHit=${Math.round((Date.now() - session._lastKbHitAt) / 1000)}s ago`);
|
||||
@@ -450,8 +454,14 @@ async function processReply(session, text, turnSeq = session.latestUserTurnSeq |
|
||||
isSimilar = overlap / shorter.length >= 0.45;
|
||||
}
|
||||
if (isSimilar) {
|
||||
console.log(`[NativeVoice] using KB prequery cache session=${session.sessionId} preText=${JSON.stringify(session._kbPrequeryText.slice(0, 60))}`);
|
||||
resolveResult = await session.pendingKbPrequery;
|
||||
const prequeryResult = await session.pendingKbPrequery;
|
||||
// 只复用 hit 结果;no-hit 可能因 partial 文本路由不完整,用完整文本 re-search
|
||||
if (prequeryResult && prequeryResult.delivery !== 'upstream_chat') {
|
||||
console.log(`[NativeVoice] using KB prequery cache (hit) session=${session.sessionId} preText=${JSON.stringify(session._kbPrequeryText.slice(0, 60))}`);
|
||||
resolveResult = prequeryResult;
|
||||
} else {
|
||||
console.log(`[NativeVoice] prequery no-hit, re-searching with full text session=${session.sessionId} preText=${JSON.stringify((session._kbPrequeryText || '').slice(0, 40))} finalText=${JSON.stringify(cleanText.slice(0, 40))}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[NativeVoice] KB prequery text mismatch, re-querying session=${session.sessionId} pre=${JSON.stringify(preText.slice(0, 40))} final=${JSON.stringify(finalText.slice(0, 40))}`);
|
||||
}
|
||||
@@ -469,13 +479,12 @@ async function processReply(session, text, turnSeq = session.latestUserTurnSeq |
|
||||
return;
|
||||
}
|
||||
if (delivery === 'upstream_chat') {
|
||||
// kbCandidate 但 S2S 未调工具 → 放开 S2S 自然回复
|
||||
// 依赖:1) system prompt 品牌保护指令引导 S2S 调工具 2) isBrandHarmful 流式拦截兜底
|
||||
if (isKnowledgeCandidate) {
|
||||
console.log(`[NativeVoice] processReply kb-nohit retrigger session=${session.sessionId}`);
|
||||
session.discardNextAssistantResponse = true;
|
||||
await sendExternalRag(session, [{ title: '用户问题', content: cleanText }]);
|
||||
} else {
|
||||
session.blockUpstreamAudio = false;
|
||||
console.log(`[NativeVoice] processReply kbCandidate+upstream_chat, unblock S2S session=${session.sessionId}`);
|
||||
}
|
||||
session.blockUpstreamAudio = false;
|
||||
session._lastPartialAt = 0;
|
||||
session.awaitingUpstreamReply = true;
|
||||
session.pendingAssistantSource = 'voice_bot';
|
||||
@@ -499,10 +508,8 @@ async function processReply(session, text, turnSeq = session.latestUserTurnSeq |
|
||||
session._lastKbTopic = cleanText;
|
||||
session._lastKbHitAt = Date.now();
|
||||
}
|
||||
// 直接用KB原始回答作为字幕,不依赖S2S event 351(S2S可能拆段/改写/丢失内容)
|
||||
const ragSubtitleText = ragContent.map((item) => item.content).join(' ');
|
||||
persistAssistantSpeech(session, ragSubtitleText, { source, toolName, meta: responseMeta });
|
||||
session.lastDeliveredAssistantTurnSeq = activeTurnSeq;
|
||||
// 不提前发KB原文作字幕:等S2S event 351返回实际语音文本后再更新字幕
|
||||
// 这样字幕和语音保持一致(S2S会基于RAG内容生成自然口语化的回答)
|
||||
session._pendingExternalRagReply = true;
|
||||
await sendExternalRag(session, ragContent);
|
||||
session.awaitingUpstreamReply = true;
|
||||
@@ -891,9 +898,10 @@ function handleUpstreamMessage(session, data) {
|
||||
});
|
||||
}
|
||||
}
|
||||
// 用户开口说话时立即打断所有 AI 播放(包括 S2S 默认 TTS)
|
||||
if (isDirectSpeaking || isChatTTSSpeaking) {
|
||||
console.log(`[NativeVoice] user barge-in (partial) session=${session.sessionId} direct=${isDirectSpeaking} chatTTS=${isChatTTSSpeaking}`);
|
||||
// 用户开口说话时立即打断所有 AI 播放(包括 S2S external_rag 音频)
|
||||
const isS2SAudioPlaying = !session.blockUpstreamAudio && session.currentTtsType === 'external_rag';
|
||||
if (isDirectSpeaking || isChatTTSSpeaking || isS2SAudioPlaying) {
|
||||
console.log(`[NativeVoice] user barge-in (partial) session=${session.sessionId} direct=${isDirectSpeaking} chatTTS=${isChatTTSSpeaking} s2sRag=${isS2SAudioPlaying}`);
|
||||
session.directSpeakUntil = 0;
|
||||
session.isSendingChatTTSText = false;
|
||||
session.chatTTSUntil = 0;
|
||||
@@ -902,6 +910,8 @@ function handleUpstreamMessage(session, data) {
|
||||
if (session.suppressReplyTimer || session.suppressUpstreamUntil) {
|
||||
clearUpstreamSuppression(session);
|
||||
}
|
||||
// 阻断 S2S 音频转发,防止用户打断后仍听到残留音频
|
||||
session.blockUpstreamAudio = true;
|
||||
}
|
||||
// 无论当前是否在播放,都发送 tts_reset 确保客户端停止所有音频播放
|
||||
if (!session._lastBargeInResetAt || now - session._lastBargeInResetAt > 500) {
|
||||
|
||||
Reference in New Issue
Block a user