2026-03-13 13:06:46 +08:00
const ToolExecutor = require ( './toolExecutor' ) ;
const db = require ( '../db' ) ;
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
const redisClient = require ( './redisClient' ) ;
2026-03-20 10:56:29 +08:00
const { hasKnowledgeRouteKeyword } = require ( './knowledgeKeywords' ) ;
2026-03-13 13:06:46 +08:00
function normalizeTextForSpeech ( text ) {
return ( text || '' )
. replace ( /^#{1,6}\s*/gm , '' )
. replace ( /\*\*([^*]*)\*\*/g , '$1' )
. replace ( /__([^_]*)__/g , '$1' )
. replace ( /\*([^*]+)\*/g , '$1' )
. replace ( /_([^_]+)_/g , '$1' )
. replace ( /~~([^~]*)~~/g , '$1' )
. replace ( /`{1,3}[^`]*`{1,3}/g , '' )
. replace ( /^[-*]{3,}\s*$/gm , '' )
. replace ( /^>\s*/gm , '' )
. replace ( /!\[[^\]]*\]\([^)]*\)/g , '' )
. replace ( /\[([^\]]*)\]\([^)]*\)/g , '$1' )
. replace ( /^[\s]*[-*+]\s+/gm , ' ' )
. replace ( /^[\s]*\d+[.)]\s+/gm , ' ' )
. replace ( /---\s*来源[: :]/g , '来源:' )
. replace ( /\r/g , ' ' )
. replace ( /\n{2,}/g , '。' )
. replace ( /\n/g , ' ' )
. replace ( /。{2,}/g , '。' )
. replace ( /([! ? ; , ])\1+/g , '$1' )
. replace ( /([。!?;,])\s*([。!?;,])/g , '$2' )
. replace ( /\s+/g , ' ' )
. trim ( ) ;
}
function splitTextForSpeech ( text , maxLen = 180 ) {
const content = normalizeTextForSpeech ( text ) ;
if ( ! content ) return [ ] ;
if ( content . length <= maxLen ) return [ content ] ;
const chunks = [ ] ;
let remaining = content ;
while ( remaining . length > maxLen ) {
const currentMaxLen = chunks . length === 0 ? Math . min ( 90 , maxLen ) : maxLen ;
let splitIndex = Math . max (
remaining . lastIndexOf ( '。' , currentMaxLen ) ,
remaining . lastIndexOf ( '! ' , currentMaxLen ) ,
remaining . lastIndexOf ( '? ' , currentMaxLen ) ,
remaining . lastIndexOf ( '; ' , currentMaxLen ) ,
remaining . lastIndexOf ( ', ' , currentMaxLen ) ,
remaining . lastIndexOf ( ',' , currentMaxLen )
) ;
if ( splitIndex < Math . floor ( currentMaxLen / 2 ) ) {
splitIndex = currentMaxLen ;
} else {
splitIndex += 1 ;
}
chunks . push ( remaining . slice ( 0 , splitIndex ) . trim ( ) ) ;
remaining = remaining . slice ( splitIndex ) . trim ( ) ;
}
if ( remaining ) chunks . push ( remaining ) ;
return chunks . filter ( Boolean ) ;
}
function estimateSpeechDurationMs ( text ) {
const plainText = normalizeTextForSpeech ( text ) . replace ( /\s+/g , '' ) ;
const length = plainText . length ;
return Math . max ( 4000 , Math . min ( 60000 , length * 180 ) ) ;
}
2026-03-17 11:00:09 +08:00
function normalizeKnowledgeAlias ( text ) {
return String ( text || '' )
2026-03-23 13:58:41 +08:00
. replace ( /一成[, ,、。!?\s]+系统/g , '一成系统' )
2026-03-17 11:00:09 +08:00
. replace ( /X{2}系统/gi , '一成系统' )
2026-03-23 13:58:41 +08:00
. replace ( /[\u4e00-\u9fff]{1,3}(?:成|城|程|诚|乘|声|生)[, ,、\s]*系统/g , '一成系统' )
. replace ( /(?:一城|逸城|一程|易成|一诚|亦成|艺成|溢成|义成|毅成|怡成|以成|已成|亿成|忆成|益成|益生|易诚|义诚|忆诚|以诚|一声|亿生|易乘)系统/g , '一成系统' )
2026-03-17 11:00:09 +08:00
. replace ( /大窝|大握|大我|大卧/g , '大沃' )
. replace ( /盛咖学院|圣咖学愿|盛咖学院|圣咖学院|盛卡学愿/g , '盛咖学愿' )
. replace ( /AI众享|Ai众享|爱众享|艾众享|哎众享/gi , 'Ai众享' )
. replace ( /暖炉原理/g , '火炉原理' ) ;
}
2026-03-13 13:06:46 +08:00
function hasKnowledgeKeyword ( text ) {
2026-03-20 10:56:29 +08:00
const normalized = normalizeKnowledgeAlias ( text ) . replace ( /\s+/g , '' ) ;
return hasKnowledgeRouteKeyword ( normalized ) ;
2026-03-13 13:06:46 +08:00
}
function isKnowledgeFollowUp ( text ) {
2026-03-23 13:58:41 +08:00
const normalized = String ( text || '' ) . trim ( ) . replace ( /[, ,。!??~~ \s]+$/g , '' ) . replace ( /^(那你|那再|那(?!个|种|款|些)|你再|再来|再|麻烦你|帮我)[, ,、\s]*/g , '' ) ;
2026-03-17 11:00:09 +08:00
if ( ! normalized ) return false ;
2026-03-23 13:58:41 +08:00
if ( /^(详细|详细说说|详细查一下|展开说说|继续说|继续讲|介绍一下|给我介绍一下|详细介绍一下|继续介绍一下|怎么用|怎么操作|怎么配置|适合谁|有什么区别|费用多少|价格多少|怎么申请|怎么开通|是什么|什么意思|地址在哪|公司地址在哪|电话多少|公司电话多少|联系方式|公司联系方式|具体政策|具体内容|怎么吃|功效是什么|有什么功效|成分是什么|有什么成分|多少钱|哪里买|怎么买|配方|原理是什么|有什么好处|怎么服用|适合什么人|规格是什么|什么规格|包装是什么|什么包装|剂型是什么|什么剂型|形态是什么|什么形态|一天几次|每天几次|每日几次|一天吃几次|每天吃几次|一天服用几次|每日服用几次)$/ . test ( normalized ) ) {
2026-03-17 11:00:09 +08:00
return true ;
}
2026-03-23 13:58:41 +08:00
// ========== 质疑/纠正/怀疑/复查类话术( 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 ) ;
2026-03-13 13:06:46 +08:00
}
function shouldForceKnowledgeRoute ( userText , context = [ ] ) {
const text = ( userText || '' ) . trim ( ) ;
if ( ! text ) return false ;
if ( hasKnowledgeKeyword ( text ) ) return true ;
if ( ! isKnowledgeFollowUp ( text ) ) return false ;
const recentContextText = ( Array . isArray ( context ) ? context : [ ] )
. slice ( - 6 )
. map ( ( item ) => String ( item ? . content || '' ) . trim ( ) )
. join ( '\n' ) ;
return hasKnowledgeKeyword ( recentContextText ) ;
}
function withHandoffSummary ( session , context ) {
const summary = String ( session ? . handoffSummary || '' ) . trim ( ) ;
if ( ! summary || session ? . handoffSummaryUsed ) {
return context ;
}
return [
{ role : 'assistant' , content : ` 会话交接摘要: ${ summary } ` } ,
... ( Array . isArray ( context ) ? context : [ ] ) ,
] ;
}
function getRuleBasedDirectRouteDecision ( userText ) {
const text = ( userText || '' ) . trim ( ) ;
if ( ! text ) return { route : 'chat' , args : { } } ;
if ( /(几点|几号|日期|星期|周几|现在时间|当前时间)/ . test ( text ) ) return { route : 'get_current_time' , args : { } } ;
if ( /(天气|气温|下雨|晴天|阴天|温度)/ . test ( text ) ) {
return { route : 'query_weather' , args : { city : text . replace ( /.*?(北京|上海|广州|深圳|杭州|成都|重庆|武汉|西安|南京|苏州|天津|长沙|郑州|青岛|宁波|无锡)/ , '$1' ) || '北京' } } ;
}
if ( /(订单|物流|快递|单号)/ . test ( text ) ) return { route : 'query_order' , args : { order _id : text } } ;
if ( /^[\d\s+\-*/().=%]+$/ . test ( text ) || /(等于多少|帮我算|计算一下|算一下)/ . test ( text ) ) {
return { route : 'calculate' , args : { expression : text . replace ( /(帮我算|计算一下|算一下|等于多少)/g , '' ) . trim ( ) || text } } ;
}
2026-03-17 11:00:09 +08:00
if ( /(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/ . test ( text ) ) {
return { route : 'search_knowledge' , args : { query : text } } ;
}
2026-03-18 17:43:13 +08:00
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 } } ;
}
2026-03-20 10:56:29 +08:00
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 ( /(孕妇|哺乳期|怀孕|孕期|儿童|小孩|老人|老年人|手术后|化疗|放疗|术后|月子)/ . test ( text ) ) {
return { route : 'search_knowledge' , args : { query : text } } ;
}
if ( /(多少钱|价格|贵不贵|性价比|划算|值得买|保质期|储存|哪里买|怎么买|多久见效|适合谁|适用人群)/ . test ( text ) ) {
return { route : 'search_knowledge' , args : { query : text } } ;
}
if ( /(营养素|营养品|保健品|保健食品|营养补充|细胞营养|NTC|暖炉原理|火炉原理|阿育吠陀)/ . test ( text ) ) {
return { route : 'search_knowledge' , args : { query : text } } ;
}
if ( /(科普|误区|认证|检测|检测报告|安全认证|GMP|Halal|临床试验)/ . 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 ) ) {
2026-03-18 17:43:13 +08:00
return { route : 'search_knowledge' , args : { query : text } } ;
}
2026-03-13 13:06:46 +08:00
if ( /^(喂|你好|您好|嗨|哈喽|hello|hi|在吗|在不在|早上好|中午好|下午好|晚上好|早安|晚安|谢谢|感谢|再见|拜拜|嗯|哦|好的|对|是的|没有了|没事了|可以了|行|OK|ok)[, ,!。??~~ \s]*[啊呀吧呢哦嗯嘛哈的了]*[!。??~~ ]*$/ . test ( text ) ) {
return { route : 'chat' , args : { } } ;
}
if ( /^(喂[, ,\s]*)?(你好|您好)[, ,!。??\s]*(在吗|请问)?[!。??]*$/ . test ( text ) ) {
return { route : 'chat' , args : { } } ;
}
2026-03-16 14:43:51 +08:00
return { route : 'chat' , args : { } } ;
2026-03-13 13:06:46 +08:00
}
function extractToolResultText ( toolName , toolResult ) {
if ( ! toolResult ) return '' ;
if ( toolName === 'search_knowledge' ) {
if ( toolResult . errorType === 'timeout' ) {
return '知识库查询超时了,请稍后重试,或换一种更具体的问法再试。' ;
}
if ( toolResult . errorType === 'not_configured' ) {
return '知识库当前未配置完成,请先检查知识库配置。' ;
}
2026-03-16 14:43:51 +08:00
if ( toolResult . errorType === 'endpoint_not_configured' ) {
return '知识库已配置但方舟LLM端点未就绪, 暂时无法检索, 请稍后再试。' ;
}
2026-03-13 13:06:46 +08:00
if ( toolResult . results && Array . isArray ( toolResult . results ) ) {
return toolResult . results . map ( ( item ) => item . content || JSON . stringify ( item ) ) . join ( '\n' ) ;
}
if ( typeof toolResult === 'string' ) return toolResult ;
if ( toolResult . error ) return toolResult . error ;
}
if ( toolName === 'query_weather' && ! toolResult . error ) return ` ${ toolResult . city } 今天 ${ toolResult . weather } ,气温 ${ toolResult . temp } ,湿度 ${ toolResult . humidity } , ${ toolResult . wind } 。 ${ toolResult . tips || '' } ` . trim ( ) ;
if ( toolName === 'query_order' && ! toolResult . error ) return ` 订单 ${ toolResult . order _id } 当前状态是 ${ toolResult . status } ,预计送达时间 ${ toolResult . estimated _delivery } ,快递单号 ${ toolResult . tracking _number } 。 ` ;
if ( toolName === 'get_current_time' && ! toolResult . error ) return ` 现在是 ${ toolResult . datetime } , ${ toolResult . weekday } 。 ` ;
if ( toolName === 'calculate' && ! toolResult . error ) return ` ${ toolResult . expression } 的计算结果是 ${ toolResult . formatted } 。 ` ;
if ( toolResult . error ) return toolResult . error ;
return typeof toolResult === 'string' ? toolResult : JSON . stringify ( toolResult ) ;
}
async function resolveReply ( sessionId , session , text ) {
2026-03-23 13:58:41 +08:00
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 } ) )
: [ ] ;
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
console . log ( ` [resolveReply] fast-path hit in ${ Date . now ( ) - _resolveStart } ms session= ${ sessionId } source= ${ fastResult . hot _answer ? 'hot_answer' : ( fastResult . cache _hit ? 'cache' : 'direct' ) } mode= ${ fastResult . retrieval _mode || 'answer' } ` ) ;
2026-03-23 13:58:41 +08:00
if ( ragItems . length > 0 ) {
session . handoffSummaryUsed = true ;
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
// raw 模式: ragItems 已包含上下文 + 多个 KB 片段,直接透传
const isRawMode = fastResult . retrieval _mode === 'raw' ;
const finalRagItems = isRawMode
? ragItems
: [ { title : '知识库结果' , content : normalizeTextForSpeech ( replyText ) . replace ( /^(根据知识库信息[, ,: :\s]*|根据.*?[, ,]\s*)/i , '' ) || replyText } ] ;
2026-03-23 13:58:41 +08:00
return {
delivery : 'external_rag' ,
speechText : '' ,
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
ragItems : finalRagItems ,
2026-03-23 13:58:41 +08:00
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 ,
} ,
} ;
}
}
}
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
// 上下文加载:优先 Redis( ~5ms) , 降级 MySQL( ~100ms)
2026-03-23 13:58:41 +08:00
const _dbStart = Date . now ( ) ;
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
let recentMessages = null ;
if ( process . env . ENABLE _REDIS _CONTEXT !== 'false' ) {
const redisHistory = await redisClient . getRecentHistory ( sessionId , 5 ) . catch ( ( ) => null ) ;
if ( redisHistory && redisHistory . length > 0 ) {
recentMessages = redisHistory ;
const _dbMs = Date . now ( ) - _dbStart ;
if ( _dbMs > 5 ) console . log ( ` [resolveReply] Redis getRecentHistory took ${ _dbMs } ms session= ${ sessionId } items= ${ redisHistory . length } ` ) ;
}
}
if ( ! recentMessages ) {
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 } ` ) ;
}
2026-03-16 14:43:51 +08:00
const scopedMessages = session ? . handoffSummaryUsed
? recentMessages . filter ( ( item ) => ! /^chat_/i . test ( String ( item ? . source || '' ) ) )
: recentMessages ;
const baseContext = scopedMessages
. filter ( ( item ) => item && ( item . role === 'user' || item . role === 'assistant' ) )
. map ( ( item ) => ( { role : item . role , content : item . content } ) ) ;
2026-03-13 13:06:46 +08:00
const context = withHandoffSummary ( session , baseContext ) ;
2026-03-23 13:58:41 +08:00
let routeDecision = getRuleBasedDirectRouteDecision ( originalText ) ;
if ( routeDecision . route === 'chat' && shouldForceKnowledgeRoute ( originalText , context ) ) {
routeDecision = { route : 'search_knowledge' , args : { query : originalText } } ;
2026-03-13 13:06:46 +08:00
}
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
// KB保护窗口: 60秒内有KB命中, 当前非纯闲聊, 强制走KB搜索
// 防止追问(如"它需要漱口吗") 绕过KB走S2S自由编造
const KB _PROTECTION _WINDOW _MS = 60000 ;
if ( routeDecision . route === 'chat' && session ? . _lastKbHitAt && ( Date . now ( ) - session . _lastKbHitAt < KB _PROTECTION _WINDOW _MS ) ) {
const isPureChitchat = /^(喂|你好|嗨|hi|hello|谢谢|谢谢你|谢谢啦|多谢|感谢|再见|拜拜|拜|好的|嗯|哦|行|没事了|不用了|可以了|好的谢谢|没问题|知道了|明白了|了解了|好嘞|好吧|行吧|ok|okay)[, ,。!?~\s]*$/i . test ( originalText ) ;
if ( ! isPureChitchat ) {
routeDecision = { route : 'search_knowledge' , args : { query : originalText } } ;
console . log ( ` [resolveReply] KB protection window active, forcing KB route session= ${ sessionId } lastKbHit= ${ Math . round ( ( Date . now ( ) - session . _lastKbHitAt ) / 1000 ) } s ago ` ) ;
}
}
2026-03-13 13:06:46 +08:00
let replyText = '' ;
let source = 'voice_bot' ;
let toolName = null ;
let responseMeta = {
route : routeDecision . route ,
original _text : originalText ,
} ;
if ( routeDecision . route === 'chat' ) {
session . handoffSummaryUsed = true ;
return {
delivery : 'upstream_chat' ,
speechText : '' ,
ragItems : [ ] ,
source ,
toolName ,
routeDecision ,
responseMeta ,
} ;
} else {
toolName = routeDecision . route ;
source = 'voice_tool' ;
const toolArgs = toolName === 'search_knowledge'
2026-03-23 13:58:41 +08:00
? { ... ( routeDecision . args || { } ) , response _mode : 'answer' , session _id : sessionId , _session : session }
2026-03-13 13:06:46 +08:00
: routeDecision . args ;
2026-03-23 13:58:41 +08:00
const metaToolArgs = toolArgs && typeof toolArgs === 'object'
? Object . fromEntries ( Object . entries ( toolArgs ) . filter ( ( [ key ] ) => key !== '_session' ) )
: toolArgs ;
2026-03-13 13:06:46 +08:00
const toolResult = await ToolExecutor . execute ( routeDecision . route , toolArgs , context ) ;
replyText = extractToolResultText ( toolName , toolResult ) ;
responseMeta = {
... responseMeta ,
tool _name : toolName ,
2026-03-23 13:58:41 +08:00
tool _args : metaToolArgs || { } ,
2026-03-13 13:06:46 +08:00
source : toolResult ? . source || null ,
original _query : toolResult ? . original _query || routeDecision . args ? . query || originalText ,
rewritten _query : toolResult ? . rewritten _query || null ,
2026-03-16 14:43:51 +08:00
selected _dataset _ids : toolResult ? . selected _dataset _ids || null ,
selected _kb _routes : toolResult ? . selected _kb _routes || null ,
2026-03-13 13:06:46 +08:00
hit : typeof toolResult ? . hit === 'boolean' ? toolResult . hit : null ,
reason : toolResult ? . reason || null ,
error _type : toolResult ? . errorType || null ,
latency _ms : toolResult ? . latency _ms || null ,
} ;
const ragItems = toolName === 'search_knowledge'
? ( toolResult ? . hit && Array . isArray ( toolResult ? . results )
? toolResult . results
. filter ( ( item ) => item && item . content )
. map ( ( item ) => ( {
title : item . title || '知识库结果' ,
content : item . content ,
} ) )
: [ ] )
: ( ! toolResult ? . error && replyText
? [ { title : ` ${ toolName } 结果 ` , content : replyText } ]
: [ ] ) ;
if ( ragItems . length > 0 ) {
session . handoffSummaryUsed = true ;
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
const isRawMode = toolResult ? . retrieval _mode === 'raw' ;
let finalRagItems = ragItems ;
if ( toolName === 'search_knowledge' && ! isRawMode ) {
// 旧模式: LLM 加工过的文本,清理后合并为单条
const speechText = normalizeTextForSpeech ( replyText ) ;
if ( speechText ) {
const cleanedText = speechText . replace ( /^(根据知识库信息[, ,: :\s]*|根据.*?[, ,]\s*)/i , '' ) ;
finalRagItems = [ { title : '知识库结果' , content : cleanedText || speechText } ] ;
}
2026-03-17 11:00:09 +08:00
}
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
// raw 模式: ragItems 已包含上下文 + 多个 KB 片段,直接透传给 S2S
2026-03-13 13:06:46 +08:00
return {
delivery : 'external_rag' ,
speechText : '' ,
feat(kb): VikingDB纯检索+重排+Redis上下文+全库搜索+别名扩展+KB保护窗口+RAG语气引导
- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异
- 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写
- toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery
- nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优
- realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式
- app.js: 健康检查新增 redis/reranker/kbRetrievalMode
- 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
2026-03-26 14:30:32 +08:00
ragItems : finalRagItems ,
2026-03-13 13:06:46 +08:00
source ,
toolName ,
routeDecision ,
responseMeta ,
} ;
}
2026-03-16 14:43:51 +08:00
if ( toolName === 'search_knowledge' && ! toolResult ? . hit ) {
session . handoffSummaryUsed = true ;
2026-03-17 11:00:09 +08:00
// 敏感问题(传销/正规性) 知识库未命中时, 不交给S2S自由发挥, 直接返回安全回复
if ( /(传销|骗局|骗子|正规吗|合法吗|正不正规|合不合法|是不是传销|直销还是传销|层级分销|非法集资|拉人头|下线|发展下线|报单|人头费)/ . test ( originalText ) ) {
const safeReply = '德国PM是一家1993年成立于德国的合法直销公司, 获得邓白氏AAA+认证, 业务覆盖100多个国家和地区。它不是传销, 是正规的直销企业哦。如果你想了解更多, 可以问我关于PM公司或产品的详细介绍。' ;
return {
delivery : 'external_rag' ,
speechText : '' ,
ragItems : [ { title : '品牌保护' , content : safeReply } ] ,
source : 'voice_tool' ,
toolName : 'search_knowledge' ,
routeDecision ,
responseMeta : { ... responseMeta , hit : true , reason : 'brand_protection' } ,
} ;
}
2026-03-23 13:58:41 +08:00
// 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' } ,
} ;
}
2026-03-16 14:43:51 +08:00
return {
delivery : 'upstream_chat' ,
speechText : '' ,
ragItems : [ ] ,
source : 'voice_bot' ,
toolName : null ,
routeDecision ,
responseMeta ,
} ;
}
2026-03-13 13:06:46 +08:00
}
const speechText = normalizeTextForSpeech ( replyText ) ;
session . handoffSummaryUsed = true ;
if ( ! speechText ) {
return { delivery : 'local_tts' , speechText : '' , ragItems : [ ] , source , toolName , routeDecision , responseMeta } ;
}
return { delivery : 'local_tts' , speechText , ragItems : [ ] , source , toolName , routeDecision , responseMeta } ;
}
module . exports = {
getRuleBasedDirectRouteDecision ,
2026-03-17 11:00:09 +08:00
normalizeKnowledgeAlias ,
2026-03-13 13:06:46 +08:00
normalizeTextForSpeech ,
splitTextForSpeech ,
estimateSpeechDurationMs ,
shouldForceKnowledgeRoute ,
resolveReply ,
} ;