- assistantProfileConfig: KB answer prompt改为分层策略(严格产品信息+灵活常识补充) - nativeVoiceGateway: S2S upstream自动重连(最多50次)、event 351字幕debounce(800ms取最长文本) - toolExecutor: 确定性query改写增强、KB查询传递session上下文 - contextKeywordTracker: 支持KB话题记忆优先enrichment - contentSafeGuard: 新增品牌安全内容过滤服务 - assistantProfileService: 新增助手配置CRUD服务 - routes/assistantProfile: 新增助手配置API路由 - knowledgeKeywords: 扩展KB关键词词典 - fastAsrCorrector: ASR纠错规则更新 - tests/: KB prompt测试、保护窗口测试、Viking性能测试 - docs/: 助手配置API文档、系统提示词目录
126 lines
4.1 KiB
JavaScript
126 lines
4.1 KiB
JavaScript
/**
|
||
* 零 LLM 上下文关键词追踪器
|
||
* 记忆最近的产品/主题关键词,用于追问理解
|
||
*/
|
||
|
||
const { TRACKER_KEYWORD_GROUPS, buildKeywordRegex } = require('./knowledgeKeywords');
|
||
|
||
class ContextKeywordTracker {
|
||
constructor() {
|
||
this.sessionKeywords = new Map();
|
||
this.TTL = 30 * 60 * 1000;
|
||
this.MAX_KEYWORDS = 8;
|
||
this.keywordPatterns = TRACKER_KEYWORD_GROUPS.map((group) => buildKeywordRegex(group, 'gi'));
|
||
this.cleanupTimer = setInterval(() => this.cleanup(), this.TTL);
|
||
if (typeof this.cleanupTimer.unref === 'function') {
|
||
this.cleanupTimer.unref();
|
||
}
|
||
}
|
||
|
||
extractKeywords(text) {
|
||
const keywords = [];
|
||
const normalized = String(text || '').trim();
|
||
if (!normalized) {
|
||
return keywords;
|
||
}
|
||
|
||
for (const pattern of this.keywordPatterns) {
|
||
const matches = normalized.match(pattern);
|
||
if (matches && matches.length > 0) {
|
||
keywords.push(...matches);
|
||
}
|
||
}
|
||
|
||
const deduped = [];
|
||
for (const keyword of keywords) {
|
||
const normalizedKeyword = String(keyword || '').trim();
|
||
if (!normalizedKeyword) {
|
||
continue;
|
||
}
|
||
if (!deduped.some((item) => item.toLowerCase() === normalizedKeyword.toLowerCase())) {
|
||
deduped.push(normalizedKeyword);
|
||
}
|
||
}
|
||
return deduped;
|
||
}
|
||
|
||
mergeKeywords(existing, incoming) {
|
||
const merged = Array.isArray(existing) ? [...existing] : [];
|
||
for (const keyword of Array.isArray(incoming) ? incoming : []) {
|
||
const normalizedKeyword = String(keyword || '').trim();
|
||
if (!normalizedKeyword) {
|
||
continue;
|
||
}
|
||
const existingIndex = merged.findIndex((item) => String(item || '').toLowerCase() === normalizedKeyword.toLowerCase());
|
||
if (existingIndex >= 0) {
|
||
merged.splice(existingIndex, 1);
|
||
}
|
||
merged.push(normalizedKeyword);
|
||
}
|
||
return merged.slice(-this.MAX_KEYWORDS);
|
||
}
|
||
|
||
updateSession(sessionId, text) {
|
||
if (!sessionId) {
|
||
return;
|
||
}
|
||
const keywords = this.extractKeywords(text);
|
||
if (keywords.length > 0) {
|
||
const existing = this.sessionKeywords.get(sessionId)?.keywords || [];
|
||
const merged = this.mergeKeywords(existing, keywords);
|
||
this.sessionKeywords.set(sessionId, {
|
||
keywords: merged,
|
||
lastUpdate: Date.now(),
|
||
});
|
||
}
|
||
}
|
||
|
||
getSessionKeywords(sessionId) {
|
||
const data = this.sessionKeywords.get(sessionId);
|
||
if (!data) return [];
|
||
if (Date.now() - data.lastUpdate > this.TTL) {
|
||
this.sessionKeywords.delete(sessionId);
|
||
return [];
|
||
}
|
||
return data.keywords;
|
||
}
|
||
|
||
enrichQueryWithContext(sessionId, query, session = null) {
|
||
const normalized = (query || '').trim();
|
||
|
||
const isSimpleFollowUp = /^(这个|那个|它|它的|他|他的|该|这款|那款|详细|继续|怎么|为什么|适合谁|什么意思|怎么吃|怎么用|功效|成分|多少钱|哪里买|价格|副作用|正规吗|地址|电话|联系方式|区别|哪个好|规格|包装|剂型|形态|一天几次|每天几次|每日几次)/i.test(normalized);
|
||
|
||
if (!isSimpleFollowUp) {
|
||
return normalized;
|
||
}
|
||
|
||
// 优先用session的KB话题记忆(60秒内有效)
|
||
// 解决:聊了"一成系统"再聊"骨关节"后追问"这款怎么吃",应关联"骨关节"而非"一成系统"
|
||
const KB_TOPIC_TTL = 60000;
|
||
if (session?._lastKbTopic && session?._lastKbHitAt && (Date.now() - session._lastKbHitAt < KB_TOPIC_TTL)) {
|
||
console.log(`[ContextTracker] Enriching from KB topic memory: "${normalized}" + "${session._lastKbTopic}"`);
|
||
return `${session._lastKbTopic} ${normalized}`;
|
||
}
|
||
|
||
// fallback: 原有keyword tracker逻辑
|
||
const keywords = this.getSessionKeywords(sessionId);
|
||
if (keywords.length === 0) {
|
||
return normalized;
|
||
}
|
||
const keywordStr = keywords.slice(-3).join(' ');
|
||
console.log(`[ContextTracker] Enriching: "${normalized}" + "${keywordStr}"`);
|
||
return `${keywordStr} ${normalized}`;
|
||
}
|
||
|
||
cleanup() {
|
||
const now = Date.now();
|
||
for (const [sessionId, data] of this.sessionKeywords) {
|
||
if (now - data.lastUpdate > this.TTL) {
|
||
this.sessionKeywords.delete(sessionId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = new ContextKeywordTracker();
|