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:
184
test2/server/services/redisClient.js
Normal file
184
test2/server/services/redisClient.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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分钟
|
||||
|
||||
// ============ 连接管理 ============
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 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,
|
||||
setKbCache,
|
||||
getKbCache,
|
||||
disconnect,
|
||||
};
|
||||
Reference in New Issue
Block a user