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模式集成测试
2026-03-26 14:30:32 +08:00
|
|
|
|
const Redis = require('ioredis');
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 配置 ============
|
|
|
|
|
|
const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
|
|
|
|
|
|
const REDIS_PASSWORD = process.env.REDIS_PASSWORD || '';
|
|
|
|
|
|
const REDIS_DB = parseInt(process.env.REDIS_DB) || 0;
|
|
|
|
|
|
const KEY_PREFIX = process.env.REDIS_KEY_PREFIX || 'bigwo:';
|
|
|
|
|
|
|
|
|
|
|
|
const HISTORY_MAX_LEN = 10; // 5轮 × 2条/轮
|
|
|
|
|
|
const HISTORY_TTL_S = 1800; // 30分钟
|
|
|
|
|
|
const KB_CACHE_HIT_TTL_S = 300; // 5分钟
|
|
|
|
|
|
const KB_CACHE_NOHIT_TTL_S = 120; // 2分钟
|
2026-04-03 10:19:16 +08:00
|
|
|
|
const SUMMARY_TTL_S = parseInt(process.env.SUMMARY_REDIS_TTL_SECONDS) || 7200; // 2小时
|
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模式集成测试
2026-03-26 14:30:32 +08:00
|
|
|
|
|
|
|
|
|
|
// ============ 连接管理 ============
|
|
|
|
|
|
let client = null;
|
|
|
|
|
|
let isReady = false;
|
|
|
|
|
|
|
|
|
|
|
|
function createClient() {
|
|
|
|
|
|
if (client) return client;
|
|
|
|
|
|
|
|
|
|
|
|
const opts = {
|
|
|
|
|
|
db: REDIS_DB,
|
|
|
|
|
|
keyPrefix: KEY_PREFIX,
|
|
|
|
|
|
retryStrategy(times) {
|
|
|
|
|
|
if (times > 10) {
|
|
|
|
|
|
console.error('[Redis] max retries reached, giving up');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return Math.min(times * 200, 3000);
|
|
|
|
|
|
},
|
|
|
|
|
|
reconnectOnError(err) {
|
|
|
|
|
|
const targetErrors = ['READONLY', 'ECONNRESET'];
|
|
|
|
|
|
return targetErrors.some(e => err.message.includes(e));
|
|
|
|
|
|
},
|
|
|
|
|
|
lazyConnect: true,
|
|
|
|
|
|
maxRetriesPerRequest: 2,
|
|
|
|
|
|
connectTimeout: 5000,
|
|
|
|
|
|
commandTimeout: 3000,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (REDIS_PASSWORD) {
|
|
|
|
|
|
opts.password = REDIS_PASSWORD;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client = new Redis(REDIS_URL, opts);
|
|
|
|
|
|
|
|
|
|
|
|
client.on('ready', () => {
|
|
|
|
|
|
isReady = true;
|
|
|
|
|
|
console.log('[Redis] connected and ready');
|
|
|
|
|
|
});
|
|
|
|
|
|
client.on('error', (err) => {
|
|
|
|
|
|
console.warn('[Redis] error:', err.message);
|
|
|
|
|
|
});
|
|
|
|
|
|
client.on('close', () => {
|
|
|
|
|
|
isReady = false;
|
|
|
|
|
|
console.warn('[Redis] connection closed');
|
|
|
|
|
|
});
|
|
|
|
|
|
client.on('reconnecting', () => {
|
|
|
|
|
|
console.log('[Redis] reconnecting...');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
client.connect().catch((err) => {
|
|
|
|
|
|
console.warn('[Redis] initial connect failed:', err.message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return client;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getClient() {
|
|
|
|
|
|
if (!client) createClient();
|
|
|
|
|
|
return client;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isAvailable() {
|
|
|
|
|
|
return isReady && client && client.status === 'ready';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 对话历史 ============
|
|
|
|
|
|
const historyKey = (sessionId) => `voice:history:${sessionId}`;
|
|
|
|
|
|
|
|
|
|
|
|
async function pushMessage(sessionId, msg) {
|
|
|
|
|
|
if (!isAvailable()) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = historyKey(sessionId);
|
|
|
|
|
|
const value = JSON.stringify({
|
|
|
|
|
|
role: msg.role,
|
|
|
|
|
|
content: msg.content,
|
|
|
|
|
|
source: msg.source || '',
|
|
|
|
|
|
ts: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
await client.lpush(key, value);
|
|
|
|
|
|
await client.ltrim(key, 0, HISTORY_MAX_LEN - 1);
|
|
|
|
|
|
await client.expire(key, HISTORY_TTL_S);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] pushMessage failed:', err.message);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getRecentHistory(sessionId, maxRounds = 5) {
|
|
|
|
|
|
if (!isAvailable()) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = historyKey(sessionId);
|
|
|
|
|
|
const items = await client.lrange(key, 0, maxRounds * 2 - 1);
|
|
|
|
|
|
if (!items || items.length === 0) return [];
|
|
|
|
|
|
// lpush 是倒序插入,lrange 取出的也是最新在前,需要 reverse 恢复时间顺序
|
|
|
|
|
|
return items
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
try { return JSON.parse(item); } catch { return null; }
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.reverse();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] getRecentHistory failed:', err.message);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function clearSession(sessionId) {
|
|
|
|
|
|
if (!isAvailable()) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.del(historyKey(sessionId));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] clearSession failed:', err.message);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 10:19:16 +08:00
|
|
|
|
// ============ 对话摘要 ============
|
|
|
|
|
|
const summaryKey = (sessionId) => `voice:summary:${sessionId}`;
|
|
|
|
|
|
|
|
|
|
|
|
async function setSummary(sessionId, summary) {
|
|
|
|
|
|
if (!isAvailable() || !summary) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = summaryKey(sessionId);
|
|
|
|
|
|
await client.set(key, summary, 'EX', SUMMARY_TTL_S);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] setSummary failed:', err.message);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getSummary(sessionId) {
|
|
|
|
|
|
if (!isAvailable()) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = summaryKey(sessionId);
|
|
|
|
|
|
return await client.get(key);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] getSummary failed:', err.message);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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模式集成测试
2026-03-26 14:30:32 +08:00
|
|
|
|
// ============ KB 缓存 ============
|
|
|
|
|
|
const kbCacheKey = (cacheKey) => `kb_cache:${cacheKey}`;
|
|
|
|
|
|
|
|
|
|
|
|
async function setKbCache(cacheKey, result) {
|
|
|
|
|
|
// 只缓存 hit 结果;no-hit 不写入 Redis,避免阻止后续 retry
|
|
|
|
|
|
if (!isAvailable() || !result.hit) return false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = kbCacheKey(cacheKey);
|
|
|
|
|
|
await client.set(key, JSON.stringify(result), 'EX', KB_CACHE_HIT_TTL_S);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] setKbCache failed:', err.message);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getKbCache(cacheKey) {
|
|
|
|
|
|
if (!isAvailable()) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const key = kbCacheKey(cacheKey);
|
|
|
|
|
|
const data = await client.get(key);
|
|
|
|
|
|
if (!data) return null;
|
|
|
|
|
|
return JSON.parse(data);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[Redis] getKbCache failed:', err.message);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============ 优雅关闭 ============
|
|
|
|
|
|
async function disconnect() {
|
|
|
|
|
|
if (client) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.quit();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
client.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
client = null;
|
|
|
|
|
|
isReady = false;
|
|
|
|
|
|
console.log('[Redis] disconnected');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
createClient,
|
|
|
|
|
|
getClient,
|
|
|
|
|
|
isAvailable,
|
|
|
|
|
|
pushMessage,
|
|
|
|
|
|
getRecentHistory,
|
|
|
|
|
|
clearSession,
|
2026-04-03 10:19:16 +08:00
|
|
|
|
setSummary,
|
|
|
|
|
|
getSummary,
|
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模式集成测试
2026-03-26 14:30:32 +08:00
|
|
|
|
setKbCache,
|
|
|
|
|
|
getKbCache,
|
|
|
|
|
|
disconnect,
|
|
|
|
|
|
};
|