Files
bigwo/test2/server/services/contextKeywordTracker.js
User 9567eb7358 feat(server): KB prompt优化、字幕修复、S2S重连、助手配置API
- 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文档、系统提示词目录
2026-03-24 17:19:36 +08:00

126 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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