fix(voice-kb): sync assistant profile and stabilize reply flow
This commit is contained in:
@@ -64,9 +64,10 @@ function estimateSpeechDurationMs(text) {
|
||||
|
||||
function normalizeKnowledgeAlias(text) {
|
||||
return String(text || '')
|
||||
.replace(/一成[,,、。!?\s]+系统/g, '一成系统')
|
||||
.replace(/X{2}系统/gi, '一成系统')
|
||||
.replace(/一城系统|逸城系统|一程系统|易成系统|一诚系统|亦成系统|艺成系统|溢成系统|义成系统|毅成系统|怡成系统|以成系统|已成系统|亿成系统|忆成系统|益成系统|益生系统|易诚系统|义诚系统|忆诚系统|以诚系统|一声系统|亿生系统|易乘系统/g, '一成系统')
|
||||
.replace(/(?<![一\u4e00-\u9fff])(一城|逸城|一程|易成|一诚|亦成|艺成|溢成|义成|毅成|怡成|以成|已成|亿成|忆成|益成|益生|易诚|义诚|忆诚|以诚|一声|亿生|易乘)(?=系统)/g, '一成')
|
||||
.replace(/[\u4e00-\u9fff]{1,3}(?:成|城|程|诚|乘|声|生)[,,、\s]*系统/g, '一成系统')
|
||||
.replace(/(?:一城|逸城|一程|易成|一诚|亦成|艺成|溢成|义成|毅成|怡成|以成|已成|亿成|忆成|益成|益生|易诚|义诚|忆诚|以诚|一声|亿生|易乘)系统/g, '一成系统')
|
||||
.replace(/大窝|大握|大我|大卧/g, '大沃')
|
||||
.replace(/盛咖学院|圣咖学愿|盛咖学院|圣咖学院|盛卡学愿/g, '盛咖学愿')
|
||||
.replace(/AI众享|Ai众享|爱众享|艾众享|哎众享/gi, 'Ai众享')
|
||||
@@ -79,12 +80,62 @@ function hasKnowledgeKeyword(text) {
|
||||
}
|
||||
|
||||
function isKnowledgeFollowUp(text) {
|
||||
const normalized = String(text || '').trim().replace(/[,,。!??~~\s]+$/g, '').replace(/^(那你|那再|那|你再|再来|再|麻烦你|帮我)[,,、\s]*/g, '');
|
||||
const normalized = String(text || '').trim().replace(/[,,。!??~~\s]+$/g, '').replace(/^(那你|那再|那(?!个|种|款|些)|你再|再来|再|麻烦你|帮我)[,,、\s]*/g, '');
|
||||
if (!normalized) return false;
|
||||
if (/^(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人)$/.test(normalized)) {
|
||||
if (/^(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人|规格是什么|什么规格|包装是什么|什么包装|剂型是什么|什么剂型|形态是什么|什么形态|一天几次|每天几次|每日几次|一天吃几次|每天吃几次|一天服用几次|每日服用几次)$/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return /^(这个|那个|它|该系统|这个系统|那个系统|这个功能|那个功能|这个产品|那个产品|这个公司|那家公司|这个政策|那个政策|这个培训|那个培训)(的)?(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人)$/.test(normalized);
|
||||
// ========== 质疑/纠正/怀疑/复查类话术(9大类全覆盖) ==========
|
||||
// 当用户对AI回答产生异议时,视为知识库追问,结合上下文重新查询KB
|
||||
|
||||
// 1. 直接否定:"不是的"、"不是不是"、"才不是"、"没有啊"、"哪有"
|
||||
if (/(不是的|不是啊|不是不是|才不是|没有啊|没有吧|哪有|哪里有|不是这么回事|不是这么说|不是这个意思)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 2. 指出错误:"搞错了"、"搞混了"、"说反了"、"记岔了"、"张冠李戴"
|
||||
if (/(搞错|说错|弄错|记错|讲错|答错|错了|搞混|搞反|记岔|说反|弄反|说混|记反|张冠李戴|答非所问)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 3. 说AI不对:"你说的不对"、"不对不对"、"说的不准"、"回答有误"
|
||||
if (/(不对|不准确|不正确|有误|有问题|说的不对|讲的不对|说得不准|说得不对|回答的不对|回答得不对|回答有误|说的有问题|不太对|不太准|不太对劲)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 4. 与自己认知矛盾:"跟我了解的不一样"、"我记得不是这样"、"前后矛盾"
|
||||
if (/(不一样|不一致|我听说|我记得|我知道不是|我了解的|跟我说的|跟之前|前后矛盾|自相矛盾|前后不一|你刚才不是说|你前面说|你之前说|别人说的|别人告诉我)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 5. 怀疑/不信:"我不信"、"骗人的吧"、"吹牛"、"太夸张"、"离谱"、"扯淡"
|
||||
if (/(不信|难以置信|不太相信|骗人|忽悠|吹牛|吹的|太夸张|夸张了|不靠谱|扯淡|瞎扯|离谱|太离谱|有依据吗|有证据吗|有根据吗|可信吗|可靠吗|鬼才信|才怪)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 6. 要求复查/重新确认:"你再查查"、"再确认一下"、"重新回答"、"核实一下"
|
||||
if (/(再查|再看看|再确认|再核实|重新查|重新回答|重新说|核实一下|查清楚|搞清楚|再问一下|再问问|帮我确认|帮我核实|帮我再|你查一下|你看一下|你确认|确认一下|看看吧|查一下)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 7. 委婉质疑:"好像不是这样吧"、"我觉得不太对"、"恐怕不是"、"感觉不对"
|
||||
if (/(好像不是|好像不太对|好像不对|我觉得不|我觉得有问题|恐怕不是|似乎不对|感觉不对|感觉不太对|我怎么记得|我印象中|印象中不是)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 8. 质问来源/权威:"谁说的"、"你从哪知道的"、"有什么根据"
|
||||
if (/(谁说的|谁告诉你|你从哪|你怎么知道|有什么根据|有什么依据|哪里说的|什么地方说|你确定|确定吗|真的吗|当真|真的假的)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 9. 不可能/反问/嘲讽:"怎么可能"、"不可能"、"不会吧"、"开玩笑"、"别逗了"
|
||||
if (/(怎么可能|不可能|不会吧|不是吧|开玩笑|别逗了|少来|得了吧|算了吧|胡说|瞎说|乱说|别瞎说|别胡说|别乱说)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 10. 产品形态/剂型纠正:"粉末来的"、"是胶囊"、"冲着喝的"、"直接吞的"
|
||||
if (/(粉末|粉剂|粉状|冲剂|冲泡|片剂|药片|胶囊|软胶囊|颗粒|液体|口服液|喷雾剂|乳霜|乳液|凝胶|膏体|膏状|冲着喝|泡着喝|直接吞|是喝的|是吃的|是固体|是液体)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
// 11. 强调/纠正句式:"到底是"、"应该是"、"明明是"、"怎么变成...了"
|
||||
if (/(到底是|究竟是|应该是|明明是|本来是|实际上是|事实上|其实是|怎么变成|什么时候变|不应该是)/.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const subjectPattern = '这个|那个|它|它的|他|他的|该|这款|那款|该系统|这个系统|那个系统|这个功能|那个功能|这个产品|那个产品|这个公司|那家公司|这个政策|那个政策|这个培训|那个培训';
|
||||
const actionPattern = '详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人|规格是什么|什么规格|包装是什么|什么包装|剂型是什么|什么剂型|形态是什么|什么形态|一天几次|每天几次|每日几次|一天吃几次|每天吃几次|一天服用几次|每日服用几次';
|
||||
const subjectActionRegex = new RegExp('^(' + subjectPattern + ')(的)?(' + actionPattern + ')$');
|
||||
return subjectActionRegex.test(normalized);
|
||||
}
|
||||
|
||||
function shouldForceKnowledgeRoute(userText, context = []) {
|
||||
@@ -208,7 +259,46 @@ function extractToolResultText(toolName, toolResult) {
|
||||
}
|
||||
|
||||
async function resolveReply(sessionId, session, text) {
|
||||
const recentMessages = await db.getRecentMessages(sessionId, 20).catch(() => []);
|
||||
const _resolveStart = Date.now();
|
||||
const originalText = text.trim();
|
||||
|
||||
// 快速路径:知识库候选先尝试无context的热答案/缓存命中,跳过DB查询(省50-200ms)
|
||||
if (shouldForceKnowledgeRoute(originalText)) {
|
||||
const fastResult = await ToolExecutor.execute('search_knowledge', { query: originalText, response_mode: 'answer', session_id: sessionId, _session: session }, []);
|
||||
if (fastResult && fastResult.hit) {
|
||||
const replyText = extractToolResultText('search_knowledge', fastResult);
|
||||
const ragItems = fastResult.hit && Array.isArray(fastResult.results)
|
||||
? fastResult.results.filter(i => i && i.content).map(i => ({ title: i.title || '知识库结果', content: i.content }))
|
||||
: [];
|
||||
console.log(`[resolveReply] fast-path hit in ${Date.now() - _resolveStart}ms session=${sessionId} source=${fastResult.hot_answer ? 'hot_answer' : (fastResult.cache_hit ? 'cache' : 'direct')}`);
|
||||
if (ragItems.length > 0) {
|
||||
const cleanedText = normalizeTextForSpeech(replyText).replace(/^(根据知识库信息[,,::\s]*|根据.*?[,,]\s*)/i, '');
|
||||
session.handoffSummaryUsed = true;
|
||||
return {
|
||||
delivery: 'external_rag',
|
||||
speechText: '',
|
||||
ragItems: [{ title: '知识库结果', content: cleanedText || replyText }],
|
||||
source: 'voice_tool',
|
||||
toolName: 'search_knowledge',
|
||||
routeDecision: { route: 'search_knowledge', args: { query: originalText } },
|
||||
responseMeta: {
|
||||
route: 'search_knowledge',
|
||||
original_text: originalText,
|
||||
tool_name: 'search_knowledge',
|
||||
source: fastResult.source,
|
||||
hit: fastResult.hit,
|
||||
reason: fastResult.reason,
|
||||
latency_ms: fastResult.latency_ms,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _dbStart = Date.now();
|
||||
const recentMessages = await db.getRecentMessages(sessionId, 10).catch(() => []);
|
||||
const _dbMs = Date.now() - _dbStart;
|
||||
if (_dbMs > 50) console.log(`[resolveReply] DB getRecentMessages took ${_dbMs}ms session=${sessionId}`);
|
||||
const scopedMessages = session?.handoffSummaryUsed
|
||||
? recentMessages.filter((item) => !/^chat_/i.test(String(item?.source || '')))
|
||||
: recentMessages;
|
||||
@@ -216,10 +306,9 @@ async function resolveReply(sessionId, session, text) {
|
||||
.filter((item) => item && (item.role === 'user' || item.role === 'assistant'))
|
||||
.map((item) => ({ role: item.role, content: item.content }));
|
||||
const context = withHandoffSummary(session, baseContext);
|
||||
const originalText = text.trim();
|
||||
let routeDecision = getRuleBasedDirectRouteDecision(text.trim());
|
||||
if (routeDecision.route === 'chat' && shouldForceKnowledgeRoute(text.trim(), context)) {
|
||||
routeDecision = { route: 'search_knowledge', args: { query: text.trim() } };
|
||||
let routeDecision = getRuleBasedDirectRouteDecision(originalText);
|
||||
if (routeDecision.route === 'chat' && shouldForceKnowledgeRoute(originalText, context)) {
|
||||
routeDecision = { route: 'search_knowledge', args: { query: originalText } };
|
||||
}
|
||||
let replyText = '';
|
||||
let source = 'voice_bot';
|
||||
@@ -243,14 +332,17 @@ async function resolveReply(sessionId, session, text) {
|
||||
toolName = routeDecision.route;
|
||||
source = 'voice_tool';
|
||||
const toolArgs = toolName === 'search_knowledge'
|
||||
? { ...(routeDecision.args || {}), response_mode: 'answer', session_id: sessionId }
|
||||
? { ...(routeDecision.args || {}), response_mode: 'answer', session_id: sessionId, _session: session }
|
||||
: routeDecision.args;
|
||||
const metaToolArgs = toolArgs && typeof toolArgs === 'object'
|
||||
? Object.fromEntries(Object.entries(toolArgs).filter(([key]) => key !== '_session'))
|
||||
: toolArgs;
|
||||
const toolResult = await ToolExecutor.execute(routeDecision.route, toolArgs, context);
|
||||
replyText = extractToolResultText(toolName, toolResult);
|
||||
responseMeta = {
|
||||
...responseMeta,
|
||||
tool_name: toolName,
|
||||
tool_args: toolArgs || {},
|
||||
tool_args: metaToolArgs || {},
|
||||
source: toolResult?.source || null,
|
||||
original_query: toolResult?.original_query || routeDecision.args?.query || originalText,
|
||||
rewritten_query: toolResult?.rewritten_query || null,
|
||||
@@ -316,6 +408,21 @@ async function resolveReply(sessionId, session, text) {
|
||||
responseMeta: { ...responseMeta, hit: true, reason: 'brand_protection' },
|
||||
};
|
||||
}
|
||||
// KB保护窗口内的问题no-hit时,走诚实兜底而非让S2S自由编造产品信息
|
||||
// 防止用户质疑/纠正时S2S瞎说(如"粉末来的呀你搞错了" → S2S编造"关节修复粉")
|
||||
if (session._lastKbHitAt && (Date.now() - session._lastKbHitAt < 60000)) {
|
||||
const honestReply = '这个问题我暂时不太确定具体细节,建议你咨询一下你的推荐人,或者换个更具体的问法再问我。';
|
||||
console.log(`[resolveReply] KB no-hit in protection window, honest fallback session=${sessionId}`);
|
||||
return {
|
||||
delivery: 'external_rag',
|
||||
speechText: '',
|
||||
ragItems: [{ title: '知识库未命中', content: honestReply }],
|
||||
source: 'voice_tool',
|
||||
toolName: 'search_knowledge',
|
||||
routeDecision,
|
||||
responseMeta: { ...responseMeta, hit: false, reason: 'honest_fallback' },
|
||||
};
|
||||
}
|
||||
return {
|
||||
delivery: 'upstream_chat',
|
||||
speechText: '',
|
||||
|
||||
Reference in New Issue
Block a user