122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
|
|
/**
|
|||
|
|
* 零 LLM 上下文关键词追踪器
|
|||
|
|
* 记忆最近的产品/主题关键词,用于追问理解
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
class ContextKeywordTracker {
|
|||
|
|
constructor() {
|
|||
|
|
this.sessionKeywords = new Map();
|
|||
|
|
this.TTL = 30 * 60 * 1000;
|
|||
|
|
this.MAX_KEYWORDS = 8;
|
|||
|
|
this.keywordPatterns = [
|
|||
|
|
/(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|数字化运营|数字化经营|数字化营销|数字化创业|数字化事业)/gi,
|
|||
|
|
/(PM-FitLine|PM细胞营养素|细胞营养素|德国PM|PM公司)/gi,
|
|||
|
|
/(小红产品|大白产品|小白产品|Activize Oxyplus|Activize|Basics|Restorate|儿童倍适|Basic Power|CitrusCare|NutriSunny|Omega)/gi,
|
|||
|
|
/(肽美|艾特维|德丽|德维|宝丽|美固健|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|Q10)/gi,
|
|||
|
|
/(NTC营养保送系统|火炉原理|阿育吠陀|招商|加盟|代理|事业机会|招商加盟|合作加盟|事业合作)/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) {
|
|||
|
|
const normalized = (query || '').trim();
|
|||
|
|
const keywords = this.getSessionKeywords(sessionId);
|
|||
|
|
|
|||
|
|
if (keywords.length === 0) {
|
|||
|
|
return normalized;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isSimpleFollowUp = /^(这个|那个|它|该|这款|那款|详细|继续|怎么|为什么|适合谁|什么意思|怎么吃|怎么用|功效|成分|多少钱|哪里买|价格|副作用|正规吗|地址|电话|联系方式|区别|哪个好)/i.test(normalized);
|
|||
|
|
|
|||
|
|
if (isSimpleFollowUp) {
|
|||
|
|
const keywordStr = keywords.slice(-3).join(' ');
|
|||
|
|
console.log(`[ContextTracker] Enriching: "${normalized}" + "${keywordStr}"`);
|
|||
|
|
return `${keywordStr} ${normalized}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 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();
|