/** * 零 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();