fix(test2): 稳定语音知识库回复并补齐热门问法覆盖

This commit is contained in:
User
2026-03-18 17:43:13 +08:00
parent c0f038b9b3
commit d13084cc0f
5 changed files with 477 additions and 122 deletions

View File

@@ -0,0 +1,121 @@
/**
* 零 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();

View File

@@ -0,0 +1,142 @@
/**
* 零 LLM 极速 ASR 修正器
* 纯字典映射 + 正则,< 5ms
*/
const PHRASE_MAP = {
'一城系统': '一成系统',
'逸城系统': '一成系统',
'一程系统': '一成系统',
'易成系统': '一成系统',
'一诚系统': '一成系统',
'亦成系统': '一成系统',
'艺成系统': '一成系统',
'溢成系统': '一成系统',
'义成系统': '一成系统',
'毅成系统': '一成系统',
'怡成系统': '一成系统',
'以成系统': '一成系统',
'已成系统': '一成系统',
'亿成系统': '一成系统',
'忆成系统': '一成系统',
'益成系统': '一成系统',
'一乘系统': '一成系统',
'一承系统': '一成系统',
'一丞系统': '一成系统',
'一呈系统': '一成系统',
'一澄系统': '一成系统',
'一橙系统': '一成系统',
'一层系统': '一成系统',
'一趁系统': '一成系统',
'一陈系统': '一成系统',
'依成系统': '一成系统',
'伊成系统': '一成系统',
'盛咖学院': '盛咖学愿',
'圣咖学愿': '盛咖学愿',
'盛卡学愿': '盛咖学愿',
'营养配送系统': 'NTC营养保送系统',
'营养输送系统': 'NTC营养保送系统',
'营养传送系统': 'NTC营养保送系统',
'营养传输系统': 'NTC营养保送系统',
'暖炉原理': '火炉原理',
'整应反应': '好转反应',
'整健反应': '好转反应',
'排毒反应': '好转反应',
'5加1': '5+1',
'五加一': '5+1',
'起步三观': '起步三关',
'起步三官': '起步三关',
'doublepm': '德国PM',
'double pm': '德国PM',
'DoublePM': '德国PM',
'Double PM': '德国PM',
'DOUBLEPM': '德国PM',
'DOUBLE PM': '德国PM',
'基础三合一': 'PM细胞营养素 基础套装',
'三合一基础套': 'PM细胞营养素 基础套装',
'大白小红小白': 'PM细胞营养素 基础套装',
};
const WORD_MAP = {
'一城': '一成', '逸城': '一成', '一程': '一成', '易成': '一成',
'一诚': '一成', '亦成': '一成', '艺成': '一成', '溢成': '一成',
'义成': '一成', '毅成': '一成', '怡成': '一成', '以成': '一成',
'已成': '一成', '亿成': '一成', '忆成': '一成', '益成': '一成',
'一乘': '一成', '一承': '一成', '一丞': '一成', '一呈': '一成',
'一澄': '一成', '一橙': '一成', '一层': '一成', '一陈': '一成',
'依成': '一成', '伊成': '一成',
'大窝': '大沃', '大握': '大沃', '大我': '大沃', '大卧': '大沃',
'爱众享': 'Ai众享', '艾众享': 'Ai众享', '哎众享': 'Ai众享',
'小洪': '小红', '小宏': '小红', '小鸿': '小红',
'大百': '大白', '大柏': '大白',
'小百': '小白', '小柏': '小白', '维适多': '小白',
'营养配送': '营养保送', '营养输送': '营养保送',
'阿玉吠陀': '阿育吠陀', '阿育费陀': '阿育吠陀',
};
const PRODUCT_ALIAS_MAP = {
'小红': '小红产品 Activize Oxyplus',
'Activize': '小红产品 Activize Oxyplus',
'Activize Oxyplus': '小红产品 Activize Oxyplus',
'大白': '大白产品 Basics',
'Basics': '大白产品 Basics',
'小白': '小白产品 Restorate',
'Restorate': '小白产品 Restorate',
'FitLine': 'PM-FitLine',
'PM FitLine': 'PM-FitLine',
'PM细胞营养': 'PM细胞营养素',
'PM营养素': 'PM细胞营养素',
'德国PM营养素': 'PM细胞营养素',
};
function escapeRegExp(text) {
return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function replaceOrderedMappings(text, mapping) {
let result = String(text || '');
const orderedEntries = Object.entries(mapping).sort((a, b) => b[0].length - a[0].length);
for (const [from, to] of orderedEntries) {
if (result.includes(from)) {
result = result.split(from).join(to);
}
}
return result;
}
function shouldExpandProductAlias(text, alias) {
if (text === alias) {
return true;
}
const escapedAlias = escapeRegExp(alias);
return new RegExp(`${escapedAlias}(?=\\s|的|是|有|和|跟|及|怎么|为什么|适合谁|什么意思|怎么吃|怎么用|功效|成分|多少钱|哪里买|价格|副作用|区别|哪个好|是什么|呢|吗|呀|啊|哦|吧|啦|了|$)`, 'i').test(text);
}
function correctAsrText(text) {
if (!text || typeof text !== 'string') {
return text || '';
}
let result = text.trim();
result = replaceOrderedMappings(result, PHRASE_MAP);
result = replaceOrderedMappings(result, WORD_MAP);
// 激进策略:所有"X+系统"格式(非常见系统词)一律转为"一成系统"
result = result.replace(/[一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾百千万亿兆零两几单双半多少全数整这那某每各以已亦艺毅怡逸溢义忆益伊依乙翼奕弈邑佚颐译蚁屹役疫裔翊熠旖漪倚绮峄羿轶壹弋驿奕懿肄翌苡圯佾诒铱仡]{1,2}(?:成|城|程|诚|乘|承|丞|呈|澄|橙|层|陈|趁|撑|称|秤|盛|剩|胜)系统/g, '一成系统');
for (const [from, to] of Object.entries(PRODUCT_ALIAS_MAP).sort((a, b) => b[0].length - a[0].length)) {
if (shouldExpandProductAlias(result, from)) {
result = result.split(from).join(to);
}
}
return result;
}
module.exports = {
correctAsrText,
PHRASE_MAP,
WORD_MAP,
PRODUCT_ALIAS_MAP,
};

View File

@@ -107,11 +107,15 @@ function extractUserText(jsonPayload, sessionId = null) {
const BRAND_HARMFUL_PATTERN = /传销|骗局|骗子公司|骗子|非法集资|非法经营|非法营销|不正规|不合法|庞氏骗局|老鼠会|拉人头|割韭菜|资金盘|涉嫌违法|涉嫌传销|疑似传销|层级分销|PM.*(?:是|属于|涉嫌|疑似).*(?:传销|骗局|非法|不合法|不正规)|(?:传销|骗局|非法|不合法|不正规).*(?:组织|公司|企业|模式)/;
const BRAND_SAFE_REPLY = '德国PM是一家1993年成立于德国的合法直销公司获得邓白氏AAA+认证业务覆盖100多个国家和地区。如果你想了解更多可以问我关于PM公司的详细介绍哦。';
const BRAND_POSITIVE_LEGALITY_PATTERN = /(德国PM|PM-International|PM公司|PM-FitLine|FitLine).*(不是传销|合法正规的?直销企业|合法直销公司|正规直销企业|正规持牌直销公司|邓白氏AAA\+)|((不是传销|合法正规的?直销企业|合法直销公司|正规直销企业|正规持牌直销公司|邓白氏AAA\+).*(德国PM|PM-International|PM公司|PM-FitLine|FitLine))/i;
const THINKING_PATTERN = /^(首轮对话|用户想|用户问|应该回复|需要列举|可列举|突出特色|引导进一步|引导用户|让用户|回复后询问|语气要|用温和|需热情|需简洁|需专业)/;
function sanitizeAssistantText(text) {
if (!text) return text;
if (BRAND_POSITIVE_LEGALITY_PATTERN.test(String(text || '').replace(/\s+/g, ' '))) {
return text;
}
if (BRAND_HARMFUL_PATTERN.test(text)) {
console.warn(`[NativeVoice][SafeGuard] blocked harmful content: ${JSON.stringify(text.slice(0, 200))}`);
return BRAND_SAFE_REPLY;
@@ -143,6 +147,7 @@ function persistUserSpeech(session, text) {
session.lastPersistedUserText = cleanText;
session.lastPersistedUserAt = now;
session.latestUserText = cleanText;
session.latestUserTurnSeq = (session.latestUserTurnSeq || 0) + 1;
resetIdleTimer(session);
db.addMessage(session.sessionId, 'user', cleanText, 'voice_asr').catch((e) => console.warn('[NativeVoice][DB] add user failed:', e.message));
sendJson(session.client, {
@@ -379,20 +384,23 @@ function suppressUpstreamReply(session, durationMs) {
}, Math.max(300, session.suppressUpstreamUntil - Date.now()));
}
async function processReply(session, text) {
async function processReply(session, text, turnSeq = session.latestUserTurnSeq || 0) {
const cleanText = (text || '').trim();
if (!cleanText) return;
if (session.processingReply) {
session.queuedUserText = cleanText;
session.queuedUserTurnSeq = turnSeq;
console.log(`[NativeVoice] processReply queued(busy) session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 80))}`);
return;
}
const now = Date.now();
if (session.directSpeakUntil && now < session.directSpeakUntil) {
session.queuedUserText = cleanText;
session.queuedUserTurnSeq = turnSeq;
console.log(`[NativeVoice] processReply queued(speaking) session=${session.sessionId} waitMs=${session.directSpeakUntil - now} text=${JSON.stringify(cleanText.slice(0, 80))}`);
return;
}
const activeTurnSeq = turnSeq || session.latestUserTurnSeq || 0;
session.processingReply = true;
sendJson(session.client, { type: 'assistant_pending', active: true });
const isKnowledgeCandidate = shouldForceKnowledgeRoute(cleanText);
@@ -404,6 +412,11 @@ async function processReply(session, text) {
console.log(`[NativeVoice] processReply start session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 120))} blocked=${session.blockUpstreamAudio} kbCandidate=${isKnowledgeCandidate}`);
try {
const { delivery, speechText, ragItems, source, toolName, routeDecision, responseMeta } = await resolveReply(session.sessionId, session, cleanText);
if (activeTurnSeq !== (session.latestUserTurnSeq || 0)) {
console.log(`[NativeVoice] stale reply ignored session=${session.sessionId} activeTurn=${activeTurnSeq} latestTurn=${session.latestUserTurnSeq || 0}`);
clearUpstreamSuppression(session);
return;
}
if (delivery === 'upstream_chat') {
if (isKnowledgeCandidate) {
console.log(`[NativeVoice] processReply kb-nohit retrigger session=${session.sessionId}`);
@@ -462,23 +475,27 @@ async function processReply(session, text) {
sendJson(session.client, { type: 'assistant_pending', active: false });
}
const pending = session.queuedUserText;
const pendingTurnSeq = session.queuedUserTurnSeq || 0;
session.queuedUserText = '';
if (pending && pending !== cleanText && (!session.directSpeakUntil || Date.now() >= session.directSpeakUntil)) {
session.queuedUserTurnSeq = 0;
if (pending && pendingTurnSeq && pendingTurnSeq !== activeTurnSeq && (!session.directSpeakUntil || Date.now() >= session.directSpeakUntil)) {
setTimeout(() => {
session.blockUpstreamAudio = true;
processReply(session, pending).catch((err) => {
processReply(session, pending, pendingTurnSeq).catch((err) => {
console.error('[NativeVoice] queued processReply failed:', err.message);
});
}, 200);
} else if (pending && pending !== cleanText) {
} else if (pending && pendingTurnSeq && pendingTurnSeq !== activeTurnSeq) {
const waitMs = Math.max(200, session.directSpeakUntil - Date.now() + 200);
clearTimeout(session.queuedReplyTimer);
session.queuedReplyTimer = setTimeout(() => {
session.queuedReplyTimer = null;
const queuedText = session.queuedUserText || pending;
const queuedTurnSeq = session.queuedUserTurnSeq || pendingTurnSeq;
session.queuedUserText = '';
session.queuedUserTurnSeq = 0;
session.blockUpstreamAudio = true;
processReply(session, queuedText).catch((err) => {
processReply(session, queuedText, queuedTurnSeq).catch((err) => {
console.error('[NativeVoice] delayed queued processReply failed:', err.message);
});
}, waitMs);
@@ -496,7 +513,6 @@ function handleUpstreamMessage(session, data) {
}
if (message.type === MsgType.AUDIO_ONLY_SERVER) {
// blockUpstreamAudio 只阻断 S2S default 音频,不阻断我们注入的 chat_tts_text 音频
const isDefaultTts = !session.currentTtsType || session.currentTtsType === 'default';
const isSuppressingUpstreamAudio = (session.suppressUpstreamUntil || 0) > Date.now() && isDefaultTts;
if ((session.blockUpstreamAudio && isDefaultTts) || isSuppressingUpstreamAudio) {
@@ -544,10 +560,15 @@ function handleUpstreamMessage(session, data) {
clearTimeout(session.greetingAckTimer);
session.greetingAckTimer = null;
}
// 不再在此处清除 blockUpstreamAudio — 音频处理器已通过 ttsType 区分,
// 允许 chat_tts_text 音频通过,同时保持对 S2S default 响应的阻断
if (session.blockUpstreamAudio && payload?.tts_type && payload.tts_type !== 'default') {
console.log(`[NativeVoice] non-default tts=${payload.tts_type} started, audio passthrough via ttsType check session=${session.sessionId}`);
if (session.blockUpstreamAudio && payload?.tts_type === 'external_rag') {
session.blockUpstreamAudio = false;
session.suppressUpstreamUntil = 0;
clearTimeout(session.suppressReplyTimer);
session.suppressReplyTimer = null;
session.discardNextAssistantResponse = false;
console.log(`[NativeVoice] unblock for external_rag tts session=${session.sessionId}`);
} else if (session.blockUpstreamAudio && payload?.tts_type === 'chat_tts_text') {
console.log(`[NativeVoice] chat_tts_text started, keeping block for S2S default response session=${session.sessionId}`);
}
console.log(`[NativeVoice] upstream tts_event session=${session.sessionId} ttsType=${payload?.tts_type || ''}`);
sendJson(session.client, { type: 'tts_event', payload });
@@ -783,11 +804,12 @@ function attachClientHandlers(session) {
}
if (parsed.type === 'text' && parsed.text) {
persistUserSpeech(session, parsed.text);
processReply(session, parsed.text).catch((error) => {
if (persistUserSpeech(session, parsed.text)) {
processReply(session, parsed.text, session.latestUserTurnSeq).catch((error) => {
console.error('[NativeVoice] text processReply failed:', error.message);
});
}
}
});
session.client.on('close', () => {
@@ -855,7 +877,9 @@ function createSession(client, sessionId) {
upstreamReady: false,
isSendingChatTTSText: false,
latestUserText: '',
latestUserTurnSeq: 0,
queuedUserText: '',
queuedUserTurnSeq: 0,
processingReply: false,
blockUpstreamAudio: false,
directSpeakUntil: 0,

View File

@@ -1,6 +1,7 @@
const ToolExecutor = require('./toolExecutor');
const arkChatService = require('./arkChatService');
const db = require('../db');
const contextKeywordTracker = require('./contextKeywordTracker');
function normalizeTextForSpeech(text) {
return (text || '')
@@ -127,6 +128,8 @@ function buildDirectChatMessages(session, context, userText) {
session.speakingStyle || '请使用温和、清晰的口吻。',
'这是语音对话场景,请直接给出自然、完整、适合朗读的中文回复。',
'如果不是基于知识库或工具结果,就不要冒充官方结论。',
'【紧扣问题】你必须直接回答用户当前这一个问题,不要偏题,不要扩展到用户没问的内容。用户问什么就答什么,简洁明了。',
'【品牌保护规则】你绝对不能说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规、不合法也不能暗示或引导用户认为它们有问题。德国PM是一家1993年成立于德国的合法直销公司获得邓白氏AAA+认证业务覆盖100多个国家。如果用户问到传销或正规性问题你必须正面回答PM是合法正规的直销企业。',
].join('\n');
messages.push({ role: 'system', content: systemPrompt });
(context || []).slice(-10).forEach((item) => {
@@ -151,7 +154,7 @@ function normalizeKnowledgeAlias(text) {
function hasKnowledgeKeyword(text) {
const normalized = normalizeKnowledgeAlias(text);
return /(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|四大AI生态|四大Ai生态|三大平台|PM公司|德国PM|PM-FitLine|FitLine|PM细胞营养素|细胞营养素|小红|大白|小白|Activize|Basics|Restorate|儿童倍适|NTC|营养保送|火炉原理|暖炉原理|阿育吠陀|Ayurveda|基础三合一|三合一|基础套装|基础二合一|二合一|招商合作|招商|代理|加盟|事业机会|邀约话术|起步三关|精品会议|成长上总裁|AI落地|ai落地|转观念|好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒|促销活动|促销|优惠|活动分数|5\+1|CC套装|CC胶囊|IB5|口腔免疫喷雾|Q10|辅酵素|Women\+|乐活|乳清蛋白|蛋白粉|乳酪煲|乳酪饮品|乳酪|倍力健|关节套装|关节舒缓|男士乳霜|去角质|面膜|发宝|叶黄素|奶昔|健康饮品|传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费|怎么吃|怎么服用|吃多少|服用方法|搭配|功效|成分|原料)/i.test(normalized);
return /(一成系统|一成AI|一成Ai|Ai众享|AI众享|数字化工作室|盛咖学愿|四大AI生态|四大Ai生态|三大平台|PM公司|德国PM公司|德国PM|PM产品|PM-FitLine|FitLine|PM细胞营养素|细胞营养素|PM事业|PM直销|做PM|加入PM|PM怎么|PM.*核心|PM.*优势|PM.*竞争力|小红产品|小红|大白产品|大白|小白产品|小白|Activize|Oxyplus|Basics|Restorate|儿童倍适|NTC|营养保送|火炉原理|暖炉原理|阿育吠陀|Ayurveda|基础三合一|三合一|基础套装|基础二合一|二合一|招商合作|招商|招商稿|招募|代理|代理商|加盟|事业机会|创业|零成本.*事业|零成本.*创业|邀约话术|起步三关|精品会议|会议组织|成长上总裁|培训|团队培训|新人入门|新人|AI落地|ai落地|AI赋能|ai赋能|转观念|对比|文化解析|团队发展|核心竞争力|竞争力|好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒|促销活动|促销|优惠|活动分数|5\+1|CC套装|CC胶囊|IB5|口腔免疫喷雾|Q10|辅酵素|Women\+|乐活|乳清蛋白|蛋白粉|乳酪煲|乳酪饮品|乳酪|倍力健|关节套装|关节舒缓|男士乳霜|去角质|面膜|发宝|叶黄素|奶昔|健康饮品|传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费|怎么吃|怎么服用|吃多少|吃法|用法|服用方法|搭配|功效|成分|原料|肽美|艾特维|德丽|德维|宝丽|美固健|Basic Power|CitrusCare|NutriSunny|Omega|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|数字化运营|数字化经营|数字化营销|数字化创业|数字化事业|招商加盟|合作加盟|事业合作|产品|公司介绍|我们公司|你们公司|产品介绍|产品有哪些|有什么产品|都有什么|有些什么|地址|电话|联系方式|实力|背景|成立|总部|分公司)/i.test(normalized);
}
function isKnowledgeFollowUp(text) {
@@ -222,6 +225,24 @@ function getRuleBasedDirectRouteDecision(userText) {
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(产品|公司.*产品|产品.*有哪些|有什么产品|产品介绍|产品列表|公司介绍|我们公司|你们公司|你们的产品|我们的产品|都有什么|有些什么)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(培训|新人入门|新人.*起步|团队培训|会议组织|起步三关|成长上总裁|精品会议)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(招商|代理|加盟|招募|事业机会|创业|代理商|招商稿|合作.*加盟|事业.*合作)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|邀约话术|文化解析|AI赋能|ai赋能|团队发展|一成AI|一成Ai|AI落地|ai落地|转观念)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(地址|电话|联系方式|总部|分公司|公司.*实力|公司.*背景|PM公司|德国PM)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/(成分|功效|怎么吃|怎么服用|吃法|用法|服用方法|副作用|好转反应|排毒反应|搭配|原料)/.test(text)) {
return { route: 'search_knowledge', args: { query: text } };
}
if (/^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安|谢谢|感谢|再见|拜拜|嗯|哦|好的|对|是的|没有了|没事了|可以了|行|OK|ok)[,!。??~\s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~]*$/.test(text)) {
return { route: 'chat', args: {} };
}
@@ -293,7 +314,7 @@ async function resolveReply(sessionId, session, text) {
toolName = routeDecision.route;
source = 'voice_tool';
const toolArgs = toolName === 'search_knowledge'
? { ...(routeDecision.args || {}), response_mode: 'answer' }
? { ...(routeDecision.args || {}), response_mode: 'answer', session_id: sessionId }
: routeDecision.args;
const toolResult = await ToolExecutor.execute(routeDecision.route, toolArgs, context);
replyText = extractToolResultText(toolName, toolResult);

View File

@@ -1,11 +1,104 @@
const axios = require('axios');
const arkChatService = require('./arkChatService');
const contextKeywordTracker = require('./contextKeywordTracker');
class ToolExecutor {
static hasCanonicalKnowledgeTerm(query) {
return /(一成系统|PM-FitLine|PM细胞营养素|NTC营养保送系统|Activize Oxyplus|小红产品|Basics|大白产品|Restorate|小白产品|儿童倍适|火炉原理|阿育吠陀)/i.test(String(query || ''));
}
static extractKnowledgeEntities(text) {
const matches = String(text || '').match(/(一成系统|Ai众享|AI众享|数字化工作室|盛咖学愿|三大平台|四大Ai生态|四大生态|德国PM|PM公司|PM-FitLine|PM细胞营养素|细胞营养素|小红产品|小红|大白产品|大白|小白产品|小白|Activize Oxyplus|Activize|Basics|Restorate|儿童倍适|乐活奶昔|Basic Power|CitrusCare|NutriSunny|Omega|肽美|艾特维|德丽|德维|宝丽|美固健|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|Q10|NTC营养保送系统|火炉原理|阿育吠陀|PM事业)/gi) || [];
const deduped = [];
for (const item of matches) {
const normalized = String(item || '').trim();
if (!normalized) {
continue;
}
if (!deduped.some((existing) => existing.toLowerCase() === normalized.toLowerCase())) {
deduped.push(normalized);
}
}
return deduped;
}
static classifyQuestionSlot(query) {
const text = String(query || '').trim();
if (/(多少钱|价格|售价|费用|价钱)/.test(text)) return 'price';
if (/(成分|配方|原料|含什么|包含什么)/.test(text)) return 'ingredient';
if (/(怎么吃|怎么用|怎么服用|服用方法|用法|用量|一天几次|每日几次)/.test(text)) return 'usage';
if (/(副作用|不良反应|好转反应|排毒反应|整应反应|皮肤发痒|皮肤微痒)/.test(text)) return 'side_effect';
if (/(多久见效|多久有效|多久能见效|多长时间见效|几天见效|什么时候见效)/.test(text)) return 'effect_time';
if (/(治病|治疗|能治|治愈|药品|药物|替代药|包治|治百病)/.test(text)) return 'medical_claim';
if (/(为什么.*(全套|搭配|三合一)|为什么要.*(全套|搭配|三合一)|为何.*(全套|搭配|三合一)|产品需要全套)/.test(text)) return 'bundle_reason';
if (/(如何发展PM事业|怎么发展PM事业|PM事业发展逻辑|陌生客户|陌生人沟通|线上拓客|成交|拓客|邀约|自我介绍|故事分享|三大平台|四大Ai生态|AI智能生产力|AI生产力)/.test(text)) return 'business_growth';
if (/(功效|作用|有什么用|有什么好处|效果)/.test(text)) return 'benefit';
if (/(适合谁|适合什么人|哪些人|适用人群)/.test(text)) return 'audience';
if (/(正规吗|合法|是不是传销|传销|骗局|骗子|直销还是传销|合不合法|正不正规)/.test(text)) return 'legality';
if (/(地址|电话|联系方式|联系|总部|公司地址)/.test(text)) return 'address_contact';
if (/(区别|不同|哪个好|有什么区别|差别)/.test(text)) return 'difference';
return 'general';
}
static buildQuestionSlotInstruction(query) {
const slot = this.classifyQuestionSlot(query);
const instructions = {
price: '用户当前只关心价格或费用,请只回答价格、收费或是否未提及价格,不要扩展到产品总介绍。',
ingredient: '用户当前只关心成分或配方,请只回答成分、原料或是否未提及成分,不要扩展到品牌背景。',
usage: '用户当前只关心用法、吃法、服用频次或剂量,请只回答这一点。',
side_effect: '用户当前只关心副作用或好转反应,请只回答可能的不良反应、好转反应或注意事项。',
effect_time: '用户当前只关心多久见效或效果周期,请只回答见效时间、周期或个体差异,不要扩展无关信息。',
medical_claim: '用户当前只关心产品能不能治病、是不是药,请只回答是否属于药品、能否替代药物以及相关注意事项。',
bundle_reason: '用户当前只关心为什么要全套、搭配或三合一请只回答搭配原理、协同作用或NTC相关原因。',
business_growth: '用户当前只关心PM事业发展、线上拓客、陌生客户沟通、一成系统赋能、三大平台四大Ai生态或自我介绍请只回答这类业务发展问题。',
benefit: '用户当前只关心功效或作用,请只回答作用点,不要扩展到无关信息。',
audience: '用户当前只关心适合人群,请只回答适用对象。',
legality: '用户当前只关心正规性、合法性或是否传销,请只围绕合法合规问题直接回答。',
address_contact: '用户当前只关心地址或联系方式,请只回答地址、电话、联系信息。',
difference: '用户当前只关心区别或对比,请直接做差异对比,不要扩写成单个产品长介绍。',
general: '请优先直接回答用户当前这一问,不要离题扩展。',
};
return instructions[slot] || instructions.general;
}
static answerMatchesQuestionSlot(query, content) {
const text = String(content || '').trim();
const lowerText = text.toLowerCase();
const slot = this.classifyQuestionSlot(query);
const entities = this.extractKnowledgeEntities(query);
const mentionsEntity = entities.length === 0 || entities.some((entity) => lowerText.includes(String(entity || '').toLowerCase()));
if (/德国PM是一家1993年成立于德国的合法直销公司/.test(text) && slot !== 'legality') {
return false;
}
if (!mentionsEntity && slot !== 'legality' && slot !== 'address_contact') {
return false;
}
const slotPatterns = {
price: /(元|价格|售价|费用|人民币|¥|¥)/,
ingredient: /(成分|配方|原料|含有|包含|营养素|葡萄籽|白藜芦醇|益生菌|胶原蛋白肽|辅酵素|Q10)/,
usage: /(服用|用法|用量|每日|每天|一次|次|饭前|饭后|早餐|晚餐|早晚)/,
side_effect: /(副作用|不良反应|好转反应|排毒|整应|皮肤.*痒|排便|反应|注意事项|正常现象)/,
effect_time: /(见效|有效|几天|几周|几个月|周期|坚持|因人而异|吸收利用)/,
medical_claim: /(不是药|不能替代药|不能代替药物|不是用于治疗|不能治疗|保健食品|营养补充|就医|医生)/,
bundle_reason: /(全套|搭配|协同|三合一|组合|NTC|吸收|运输|利用|代谢|原理)/,
business_growth: /(一成系统|PM事业|拓客|成交|邀约|陌生客户|沟通|三大平台|四大Ai生态|数字化工作室|Ai众享|盛咖学愿|故事|自我介绍|赋能|智能生产力)/,
benefit: /(功效|作用|帮助|支持|改善|提升|有助于)/,
audience: /(适合|适用|人群|适宜|可以)/,
legality: /(合法|正规|直销|认证|邓白氏|不是传销)/,
address_contact: /(地址|电话|联系方式|联系|总部|香港|德国|美国|加拿大)/,
difference: /(区别|不同|相比|分别|一个|另一个|而|更适合)/,
};
if (slotPatterns[slot]) {
return slotPatterns[slot].test(text);
}
return text.length >= 10;
}
static getKnowledgeBaseRoutingRules() {
const raw = process.env.VOLC_ARK_KNOWLEDGE_BASE_ROUTING || process.env.VOLC_ARK_KNOWLEDGE_BASE_MAP;
if (!raw) {
@@ -60,9 +153,9 @@ class ToolExecutor {
const haystack = `${String(query || '').trim()}\n${recentContextText}`.toLowerCase();
const priorityRouteNames = [];
const hasSystemIntent = /(一成系统|ai众享|数字化工作室|盛咖学愿|赋能工具|四大ai生态|三大平台)/i.test(haystack);
const hasSystemIntent = /(一成系统|ai众享|数字化工作室|盛咖学愿|赋能工具|四大ai生态|四大生态|三大平台|智能生产力|线上拓客|陌生客户|邀约)/i.test(haystack);
const hasCompanyIntent = /(pm公司|德国pm(?!事业|细胞|营养|产品|fitline|\s*基础|\s*大白|\s*小红|\s*小白)|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司|邓白氏|aaa\+|公司介绍)/i.test(haystack);
const hasProductIntent = /(细胞营养素|基础套装|基础三合一|三合一|大白产品|小红产品|小白产品|activize|basics|restorate|fitline|儿童倍适|ntc营养保送|火炉原理|阿育吠陀|产品.*介绍|介绍.*产品|产品有哪些|产品列表)/i.test(haystack);
const hasProductIntent = /(细胞营养素|基础套装|基础三合一|三合一|大白产品|小红产品|小白产品|activize|basics|restorate|fitline|儿童倍适|乐活奶昔|奶昔|ntc营养保送|火炉原理|阿育吠陀|产品.*介绍|介绍.*产品|产品有哪些|产品列表|产品.*(全套|区别|见效|治病|副作用|作用|功效|成分|用法)|为什么.*产品|保健品区别|多久见效)/i.test(haystack);
if (hasSystemIntent) {
priorityRouteNames.push('system');
}
@@ -105,13 +198,20 @@ class ToolExecutor {
.filter(Boolean)
.join('\n');
const haystack = `${text}\n${recentContextText}`;
const questionDimension = text.match(/(功效|作用|成分|配方|原料|怎么吃|怎么用|怎么服用|服用方法|吃法|用法|用量|副作用|好转反应|价格|多少钱|适合谁|适用人群|区别|不同|搭配|原理)/);
// 第一层:当前查询文本中有明确产品/系统/主题关键词 → 直接改写(不依赖上下文)
if (/(基础三合一|三合一基础套|基础套装|大白小红小白)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿)/i.test(text)) {
if (/(邀约|话术)/i.test(haystack)) return '一成系统 邀约话术';
if (/文化/i.test(haystack)) return '一成系统 文化解析';
if (/(赋能团队|团队发展|AI赋能|ai赋能)/i.test(haystack)) return '一成系统用AI赋能团队发展';
if (/(一成系统|Ai众享|数字化工作室|盛咖学愿|三大平台|四大Ai生态|四大生态|智能生产力)/i.test(text)) {
if (/(核心竞争力|竞争力|核心优势|优势)/i.test(text)) return '一成系统 核心竞争力 三大平台 四大Ai生态 零成本高效率';
if (/(发展|怎么做|怎么用|如何用|如何做|关键点|关键|方法|步骤)/i.test(text)) return '一成系统 发展PM事业 三大平台 四大Ai生态 零成本高效率 全球市场';
if (/(线上拓客|拓客|成交|成交率|陌生客户|陌生人沟通|邀约)/i.test(text)) return '一成系统 PM事业 线上拓客 成交 邀约 三大平台 四大Ai生态';
if (/(ai智能生产力|ai生产力|智能生产力|团队效率|赋能团队|团队赋能)/i.test(text)) return '一成系统 AI智能生产力 赋能团队 三大平台 四大Ai生态';
if (/(故事|自我介绍|分享)/i.test(text)) return '一成系统 PM事业 故事分享 自我介绍';
if (/(邀约|话术)/i.test(text)) return '一成系统 邀约话术';
if (/文化/i.test(text)) return '一成系统 文化解析';
if (/(赋能团队|团队发展|AI赋能|ai赋能)/i.test(text)) return '一成系统用AI赋能团队发展';
if (/(三大平台|四大生态|Ai生态)/i.test(text)) return '一成系统 三大平台 四大Ai生态';
return '一成系统 德国PM事业发展的强大赋能工具 三大平台 四大Ai生态';
}
if (/(PM公司|德国PM|公司地址|联系方式|电话|公司实力|公司背景|总部|分公司)/i.test(text)) {
@@ -122,19 +222,33 @@ class ToolExecutor {
if (/(实力|背景)/i.test(text)) return '德国PM 公司实力介绍 邓白氏 99分 AAA+';
return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍';
}
if (/(德国PM介绍|介绍德国PM|德国PM公司介绍|PM公司介绍|PM介绍)/i.test(text)) return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍 邓白氏 99分 AAA+';
if (/(NTC.*(核心优势|核心竞争力|优势|原理|厉害)|核心优势.*NTC|核心竞争力.*NTC)/i.test(text)) return 'NTC营养保送系统 核心优势 吸收利用 原理';
if (/(PM基础三合一介绍|基础三合一介绍|PM基础套装介绍|基础套装介绍)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白 介绍';
if (/儿童倍适/i.test(text)) return '儿童倍适';
if (/(小红产品|小红|Activize Oxyplus|Activize)/i.test(text)) return 'Fitline小红产品提升能量原理';
if (/(大白产品|大白|倍适|Basics)/i.test(text)) return '德国PM细胞营养素 大白 Basics';
if (/(小白产品|小白|维适多|Restorate)/i.test(text)) return '德国PM细胞营养素 小白';
if (/(小红产品|小红|Activize Oxyplus|Activize)/i.test(text)) return questionDimension ? `Fitline小红产品 Activize ${questionDimension[0]}` : 'Fitline小红产品提升能量原理';
if (/(大白产品|大白|倍适|Basics)/i.test(text)) return questionDimension ? `德国PM细胞营养素 大白 Basics ${questionDimension[0]}` : '德国PM细胞营养素 大白 Basics';
if (/(小白产品|小白|维适多|Restorate)/i.test(text)) return questionDimension ? `德国PM细胞营养素 小白 Restorate ${questionDimension[0]}` : '德国PM细胞营养素 小白';
if (/(NTC营养保送系统|Nutrient Transport Concept)/i.test(text)) return 'NTC营养保送系统';
if (/火炉原理/i.test(text)) return '火炉原理';
if (/(阿育吠陀|Ayurveda)/i.test(text)) return '阿育吠陀医学原理';
if (/(PM-FitLine|PM细胞营养素)/i.test(text)) return '德国PM细胞营养素 基础套装 大白 小红 小白';
if (/(我们公司.*产品|公司.*产品|产品.*推荐|推荐.*产品|产品有哪些|产品介绍|产品列表)/i.test(text)) return '德国PM FitLine 细胞营养素产品 大白Basics 小红Activize 小白Restorate 儿童倍适';
if (/(治病吗|能治病吗|产品治病|治疗疾病|替代药|是不是药)/i.test(text)) return 'PM产品 不是药 不能替代药物 保健食品 营养补充';
if (/(多久见效|多久有效|多久能见效|多长时间见效|几天见效|什么时候见效)/i.test(text)) return 'PM产品 多久见效 吸收利用 周期 个体差异';
if (/(为什么.*(全套|搭配|三合一)|为什么要.*(全套|搭配|三合一)|产品需要全套)/i.test(text)) return '德国PM细胞营养素 全套搭配 NTC营养保送系统 协同作用';
if (/(与其它保健品区别|与其他保健品区别|和其它保健品区别|和其他保健品区别|保健品区别)/i.test(text)) return 'PM产品 与其他保健品区别 NTC营养保送系统 吸收利用';
if (/(新人起步三关|起步三关)/i.test(text)) return '培训新人起步三关';
if (/(精品会议|会议组织)/i.test(text)) return '培训打造精品会议具体如下';
if (/成长上总裁/i.test(text)) return '培训成长上总裁';
if (/(招商|代理|加盟|合作|事业机会|招商稿|代理政策)/i.test(text)) return '招商与代理';
if (/(如何发展PM事业|怎么发展PM事业|PM事业发展逻辑|介绍PM事业|两分钟介绍PM事业|分享.*故事.*自我介绍|自我介绍)/i.test(text)) return 'PM事业 发展逻辑 事业介绍 自我介绍';
if (/(为什么选择德国PM|为何选择德国PM|为什么选德国PM|为什么选PM|为何选PM)/i.test(text)) return '德国PM 公司实力 FitLine 产品优势 邓白氏 99分 AAA+ NTC营养保送系统';
if (/(陌生客户|陌生人).*(沟通|开口|邀约|交流|切入).*(PM事业|德国PM|PM)/i.test(text)) return 'PM事业 陌生客户 沟通 邀约 话术';
if (/(线上拓客|线上成交|线上开发客户|线上获客|线上成交率)/i.test(text)) return '一成系统 PM事业 线上拓客 成交 获客';
if (/(团队.*AI智能生产力|AI智能生产力.*团队|团队.*AI生产力|AI生产力.*团队)/i.test(text)) return '一成系统 AI智能生产力 赋能团队';
if (/(三大平台|四大Ai生态|四大生态)/i.test(text)) return '一成系统 三大平台 四大Ai生态';
if (/(请分享.*故事.*自我介绍|故事.*自我介绍|个人故事.*自我介绍)/i.test(text)) return 'PM事业 故事分享 自我介绍';
if (/(一成AI|AI落地|ai落地|转观念|落地对比)/i.test(text)) return '2026一成Ai落地对比与转观念';
if (/(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/i.test(text)) return '德国PM 1993年 创立 100多个国家 FitLine 公司介绍 邓白氏 99分 AAA+ 合法直销';
if (/(好转反应|整应反应|排毒反应|副作用|不良反应|皮肤发痒)/i.test(text)) return 'PM产品整应反应好转反应解析';
@@ -144,7 +258,7 @@ class ToolExecutor {
if (/(IB5|口腔免疫喷雾)/i.test(text)) return 'IB5口腔免疫喷雾';
if (/(Q10|辅酵素|氧修护)/i.test(text)) return 'Q10辅酵素氧修护';
if (/Women\+/i.test(text)) return 'Women+';
if (/乐活/i.test(text)) return '乐活';
if (/乐活奶昔|乐活/i.test(text)) return '乐活奶昔';
if (/(乳清蛋白|蛋白粉)/i.test(text)) return '乳清蛋白粉';
if (/(乳酪煲|乳酪饮品|乳酪)/i.test(text)) return '乳酪煲 乳酪饮品';
if (/(基础二合一|二合一)/i.test(text)) return '基础二合一';
@@ -189,6 +303,7 @@ class ToolExecutor {
.replace(/一城系统|逸城系统|一程系统|易成系统|一诚系统|亦成系统/g, '一成系统')
.replace(/PM[-\s]*Fitline|PM[-\s]*fitline|Pm[-\s]*fitline|Fitline|fitline/g, 'PM-FitLine')
.replace(/PM细胞营养|PM营养素|德国PM营养素/g, 'PM细胞营养素')
.replace(/NTC科技/g, 'NTC营养保送系统')
.replace(/NTC营养保送系统|NTC营养配送系统|NTC营养输送系统|NTC营养传送系统|NTC营养传输系统/g, 'NTC营养保送系统')
.replace(/Nutrient Transport Concept/gi, 'NTC营养保送系统')
.replace(/Activize Oxyplus|Activize/gi, 'Activize Oxyplus')
@@ -218,8 +333,8 @@ class ToolExecutor {
};
}
const noHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关内容|未找到相关信息|没有找到相关信息|没有找到.*的具体|没有找到.*的相关|没有找到.*的详细|没有找到.*相关介绍|我这边没有找到|目前没有找到|暂时没有找到|知识库中没有相关内容|暂未找到与.*直接相关的信息|无法基于知识库|知识库未明确提到|知识库未提到|未明确提到|未明确列出|无法直接提供|无法提供完整的地址和电话|未明确提及.*地址|未明确提及.*电话|未明确提及.*联系方式|建议通过官方客服渠道|建议通过官方.*查询|建议.*查看产品包装|建议.*联系.*客服|联系官方客服|建议.*咨询.*客服|没有相关.*资料|还没有相关的|没有相关的信息|没有相关的资料|没有.*的资料|知识库里.*没有|暂未收录|目前.*没有.*相关|不在.*知识库|建议.*查阅.*官方|建议.*咨询.*专/;
if (noHitPattern.test(text)) {
const strictNoHitPattern = /未检索到|没有检索到|没有相关内容|暂无相关内容|未找到相关内容|未找到相关信息|没有找到相关信息|知识库中没有相关内容|知识库中没有关于|知识库中没有找到|没有找到具体|没有.*具体信息|没有.*相关说明|暂未找到与.*直接相关的信息|无法基于知识库.*回答|知识库未明确提到|知识库未提到/;
if (strictNoHitPattern.test(text)) {
return {
hit: false,
reason: 'no_hit',
@@ -227,39 +342,10 @@ class ToolExecutor {
};
}
const normalizedQuery = String(query || '').trim();
if (/(小红|Activize Oxyplus)/i.test(normalizedQuery) && /(护肤|肤色|敏感肌|眼周)/i.test(text)) {
if (!this.answerMatchesQuestionSlot(query, text)) {
return {
hit: false,
reason: 'no_hit',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
if (/(大白|Basics|倍适)/i.test(normalizedQuery) && /(洗衣机|干衣机|保费|保险|住院津贴|智能健康管理设备|生命体征|Beko)/i.test(text)) {
return {
hit: false,
reason: 'no_hit',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
if (/(小白|Restorate|维适多)/i.test(normalizedQuery) && /(客服系统|网站|微信|邮件|软胶囊)/i.test(text)) {
return {
hit: false,
reason: 'no_hit',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
if (/火炉原理/i.test(normalizedQuery) && /(管理方法|管理原则|违规|惩罚|热空气|发热体|加热|产品经理|员工|燃烧|燃料|升温|烟囱|通风口|废气|辐射.*对流)/i.test(text)) {
return {
hit: false,
reason: 'no_hit',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
if (/(手机|平板|笔记本电脑|智能手表|电脑|以旧换新|分期付款|护肤品|彩妆|香水|化妆品)/i.test(text) && !/(PM|FitLine|细胞营养|Activize|Basics|Restorate|NTC|火炉原理|阿育吠陀)/i.test(text)) {
return {
hit: false,
reason: 'no_hit',
reason: 'slot_mismatch',
reply: `知识库中暂未找到与“${query}”直接相关的信息,请换个更具体的问法再试。`,
};
}
@@ -349,12 +435,13 @@ class ToolExecutor {
};
}
static async searchKnowledge({ query, response_mode } = {}, context = []) {
static async searchKnowledge({ query, response_mode, session_id } = {}, context = []) {
const startTime = Date.now();
query = query || '';
const responseMode = response_mode === 'snippet' ? 'snippet' : 'answer';
const knowledgeEndpointId = process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID;
console.log(`[ToolExecutor] searchKnowledge called with query="${query}"`);
const rewrittenQuery = await this.rewriteKnowledgeQuery(query, context);
const rewrittenQuery = this.rewriteKnowledgeQuery(query, context, session_id);
const kbTarget = this.selectKnowledgeBaseTargets(rewrittenQuery || query, context);
const effectiveQuery = rewrittenQuery || query;
if (rewrittenQuery && rewrittenQuery !== query) {
@@ -366,9 +453,9 @@ class ToolExecutor {
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
if (kbIds && kbIds !== 'your_knowledge_base_dataset_id') {
if (arkChatService.isMockMode()) {
if (!knowledgeEndpointId || knowledgeEndpointId === 'your_ark_endpoint_id') {
const latencyMs = Date.now() - startTime;
console.warn('[ToolExecutor] Ark KB search skipped: VOLC_ARK_ENDPOINT_ID not configured (knowledge base IDs are set but endpoint is missing)');
console.warn('[ToolExecutor] Ark KB search skipped: knowledge endpoint not configured (knowledge base IDs are set but endpoint is missing)');
return {
query,
original_query: query,
@@ -377,7 +464,7 @@ class ToolExecutor {
selected_kb_routes: kbTarget.matchedRoutes,
latency_ms: latencyMs,
errorType: 'endpoint_not_configured',
error: '知识库已配置但方舟 LLM 端点未配置,请检查 VOLC_ARK_ENDPOINT_ID',
error: '知识库已配置但知识库方舟端点未配置,请检查 VOLC_ARK_KNOWLEDGE_ENDPOINT_ID 或 VOLC_ARK_ENDPOINT_ID',
source: 'ark_knowledge',
hit: false,
reason: 'endpoint_not_configured',
@@ -385,21 +472,7 @@ class ToolExecutor {
}
try {
console.log('[ToolExecutor] Trying Ark Knowledge Search...');
let result = await this.searchArkKnowledge(effectiveQuery, context, responseMode, kbTarget.datasetIds, query);
if (!result?.hit) {
console.log('[ToolExecutor] Ark KB no_hit, retrying without context...');
const retryResult = await this.searchArkKnowledge(effectiveQuery, [], responseMode, kbTarget.datasetIds, query);
if (retryResult?.hit || retryResult?.reason !== result?.reason) {
result = retryResult;
}
}
if (!result?.hit && responseMode === 'answer') {
console.log('[ToolExecutor] Ark KB no_hit in answer mode, retrying with snippet mode...');
const snippetResult = await this.searchArkKnowledge(effectiveQuery, [], 'snippet', kbTarget.datasetIds, query);
if (snippetResult?.hit) {
result = snippetResult;
}
}
const result = await this.searchArkKnowledge(effectiveQuery, [], responseMode, kbTarget.datasetIds, query);
const latencyMs = Date.now() - startTime;
console.log(`[ToolExecutor] Ark KB search succeeded in ${latencyMs}ms`);
return {
@@ -446,63 +519,32 @@ class ToolExecutor {
};
}
static async rewriteKnowledgeQuery(query, context = []) {
static rewriteKnowledgeQuery(query, context = [], sessionId = null) {
const originalQuery = String(query || '').trim();
if (!originalQuery) {
return '';
}
const normalizedQuery = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(originalQuery));
const conciseQuery = normalizedQuery.replace(/[,。!?、,.!?\s]+/g, '');
const recentContext = (Array.isArray(context) ? context : [])
.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim())
.slice(-6)
.map((item) => `${item.role === 'user' ? '用户' : '助手'}${String(item.content || '').trim()}`)
.join('\n');
const deterministicQuery = this.buildDeterministicKnowledgeQuery(normalizedQuery, context);
if (deterministicQuery) {
return deterministicQuery;
let normalizedQuery = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(originalQuery));
if (sessionId) {
normalizedQuery = contextKeywordTracker.enrichQueryWithContext(sessionId, normalizedQuery);
}
const isPronounFollowUp = /^(这个|那个|它|该系统|这个系统|那个系统|详细|继续|怎么|为什么|适合谁|什么意思)/.test(normalizedQuery);
if (this.hasCanonicalKnowledgeTerm(normalizedQuery) && conciseQuery.length <= 36 && !isPronounFollowUp) {
return normalizedQuery;
}
if (arkChatService.isMockMode()) {
return normalizedQuery;
}
try {
const result = await arkChatService.chat([
{
role: 'system',
content: '你是知识库检索词改写助手。你的任务是把用户当前问题改写成适合企业知识库检索的完整查询语句。必须处理三类问题1补全多轮对话中的省略主语2纠正语音识别错误、口语噪声和同音误写3把别名统一成知识库里的规范说法。规则不要改变用户真实意图不要回答问题只输出一行最终检索词优先保留真正的产品名、系统名、技术名。当前知识库高频规范术语包括一成系统、PM-FitLine、PM细胞营养素、NTC营养保送系统、Activize Oxyplus、小红产品、Basics、大白产品、Restorate、小白产品、儿童倍适、火炉原理、阿育吠陀。示例XX系统、一城系统、逸城系统、一程系统等都统一理解为一成系统NTC营养配送系统、NTC营养输送系统统一为NTC营养保送系统Fitline、PM fitline 统一为 PM-FitLine小红统一为小红产品 Activize Oxyplus。',
},
{
role: 'user',
content: `最近上下文:\n${recentContext || '无'}\n\n当前原始问题:${normalizedQuery}\n\n请输出最终检索词:`,
},
], []);
const rewritten = this.applyKnowledgeQueryAnchor(this.normalizeKnowledgeQueryAlias(String(result.content || '').replace(/^["'“”]+|["'“”]+$/g, '').trim()));
return rewritten || normalizedQuery;
} catch (error) {
console.warn('[ToolExecutor] rewriteKnowledgeQuery failed:', error.message);
return normalizedQuery;
}
}
/**
* 通过方舟 Chat Completions API + knowledge_base metadata 进行知识检索
* 使用独立的 LLM 调用,专门用于知识库检索场景(如语音通话的工具回调)
*/
static async searchArkKnowledge(query, context = [], responseMode = 'answer', datasetIdsOverride = null, originalQuery = null) {
const endpointId = process.env.VOLC_ARK_ENDPOINT_ID;
const endpointId = process.env.VOLC_ARK_KNOWLEDGE_ENDPOINT_ID || process.env.VOLC_ARK_ENDPOINT_ID;
const authKey = process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID;
const kbIds = process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
if (!endpointId || endpointId === 'your_ark_endpoint_id') {
console.warn('[ToolExecutor] searchArkKnowledge skipped: VOLC_ARK_ENDPOINT_ID not configured');
console.warn('[ToolExecutor] searchArkKnowledge skipped: knowledge endpoint not configured');
return {
query,
results: [{ title: '配置缺失', content: `知识库中暂未找到与"${query}"直接相关的信息,请换个更具体的问法再试。` }],
@@ -517,7 +559,7 @@ class ToolExecutor {
? datasetIdsOverride.map((id) => String(id || '').trim()).filter(Boolean)
: kbIds.split(',').map(id => id.trim()).filter(Boolean);
const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3;
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.5;
const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.4;
// 当 query 为空时FC 流式 chunks 乱序无法解析),使用简短的默认查询
const effectiveQuery = (query && query.trim()) ? query : '请介绍你们的产品和服务';
@@ -527,6 +569,7 @@ class ToolExecutor {
// 判断是否需要注入原始问题检索词≠原始问题时LLM需要知道用户实际问了什么
const cleanOriginal = (originalQuery || '').trim();
const answerTargetQuery = cleanOriginal || effectiveQuery;
const hasRewrite = cleanOriginal && cleanOriginal !== effectiveQuery;
if (hasRewrite) {
console.log(`[ToolExecutor] searchArkKnowledge injecting original question: "${cleanOriginal}" (search query: "${effectiveQuery}")`);
@@ -538,9 +581,13 @@ class ToolExecutor {
.slice(responseMode === 'snippet' ? -4 : -6);
const baseSnippetPrompt = '你是知识库片段提取助手。请基于知识库提取与用户问题最相关的2到4条简洁知识片段供语音系统继续组织回复。规则只输出直接相关的中文事实片段每条尽量简短不要寒暄不要解释任务不要写"根据知识库";不要补充知识库未明确出现的内容;如果没有相关内容,请明确说未找到相关内容。';
const baseAnswerPrompt = '你是企业知识库问答助手回答将直接用于语音播报。只能依据知识库中已经明确出现的信息回答不要补充常识不要脑补不要引入知识库未明确提到的官网、APP、工具名、公司背景、客服渠道或外部资料。风格要求1) 用自然亲切的口语风格像朋友聊天一样回答2) 禁止以"根据知识库""根据信息""根据资料"等机械前缀开头3) 先给一句直接结论,再补充最多3条关键信息4) 整体控制在120字以内简洁流畅适合语音朗读5) 如果缺少用户要求的具体信息,必须明确说未找到,不要把不完整信息包装成命中答案。【品牌保护】绝对禁止说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规不合法。德国PM是1993年成立于德国的合法直销企业获邓白氏AAA+认证。如果用户问到正规性或传销问题必须正面回答PM是合法正规的直销企业。';
const baseAnswerPrompt = '你是企业知识库问答助手回答将直接用于语音播报。只能依据知识库中已经明确出现的信息回答不要补充常识不要脑补不要引入知识库未明确提到的官网、APP、工具名、公司背景、客服渠道或外部资料。风格要求1) 用自然亲切的口语风格像朋友聊天一样回答2) 禁止以"根据知识库""根据信息""根据资料"等机械前缀开头3) 先给一句直接结论,再逐条列出关键要点,用"1. 2. 3."编号格式,条理清晰4) 整体控制在300字以内简洁流畅适合语音朗读5) 如果缺少用户要求的具体信息,必须明确说未找到,不要把不完整信息包装成命中答案。【紧扣问题】你必须围绕用户当前问的问题来回答。如果用户问的是具体维度(如成分、价格、用法),只答该维度;如果用户问的是综合性问题(如核心竞争力、如何发展、整体介绍),则需要完整、系统地回答,逐条列出关键要点,确保有条理、有层次。【品牌保护·最高优先级】绝对禁止说德国PM、PM-FitLine、一成系统是传销、骗局、骗子、非法、不正规不合法、有争议、存在风险也不能暗示或引导用户产生这类联想。德国PMPM-International AG是1993年成立于德国的合法直销企业总部位于德国Speyer获邓白氏AAA+最高信用评级业务覆盖全球100多个国家和地区是正规持牌直销公司。如果用户问到传销、正规性或合法性问题,必须正面、明确地回答PM是合法正规的直销企业,不能含糊、回避或使用"需要自行判断"之类的措辞。';
let systemContent = responseMode === 'snippet' ? baseSnippetPrompt : baseAnswerPrompt;
if (responseMode === 'answer' && answerTargetQuery) {
systemContent += `\n\n当前必须优先直接回答用户当前这一个问题:“${answerTargetQuery}”。如果用户只问一个维度,例如成分、价格、用法、适合谁、区别、正规性、地址或联系方式,就只回答这个维度,不要扩展成整段产品或公司介绍。`;
systemContent += `\n\n${this.buildQuestionSlotInstruction(answerTargetQuery)}`;
}
if (hasRewrite) {
systemContent += `\n\n重要:用户的实际问题是"${cleanOriginal}",请围绕这个问题回答,不要偏离用户的真实意图。下方的检索词仅用于匹配知识库文档,不代表用户的真正提问。`;
}
@@ -588,7 +635,7 @@ class ToolExecutor {
const choice = response.data.choices?.[0];
const content = choice?.message?.content || '未找到相关信息';
const classifyQuery = (originalQuery || '').trim() || query;
const classifyQuery = [effectiveQuery, (originalQuery || '').trim()].filter(Boolean).join(' ');
const classified = this.classifyKnowledgeAnswer(classifyQuery, content);
return {