/** * 基于知识库实际内容的功能性测试 * 覆盖:单KB查询、多KB查询(话题切换)、追问+质疑混合、确定性改写、热答案匹配 * * 运行方式: node --test tests/test_kb_scenarios.js */ const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); const { shouldForceKnowledgeRoute } = require('../services/realtimeDialogRouting'); const { hasKnowledgeRouteKeyword } = require('../services/knowledgeKeywords'); const ToolExecutor = require('../services/toolExecutor'); // ================================================================ // 辅助函数 // ================================================================ function assertKbRoute(text, ctx, msg) { assert.equal(shouldForceKnowledgeRoute(text, ctx), true, msg || `"${text}" should route to KB`); } function assertNotKbRoute(text, ctx, msg) { assert.equal(shouldForceKnowledgeRoute(text, ctx), false, msg || `"${text}" should NOT route to KB`); } function buildCtx(pairs) { return pairs.map(([role, content]) => ({ role, content })); } // ================================================================ // 1. 单知识库查询 —— 每个产品/话题独立首轮查询 // ================================================================ describe('单KB查询 —— 产品类', () => { const productQueries = [ ['基础三合一怎么吃', '基础三合一'], ['大白产品有什么功效', '大白Basics'], ['小红Activize的作用是什么', '小红Activize'], ['小白Restorate怎么服用', '小白Restorate'], ['儿童倍适适合几岁的孩子', '儿童倍适'], ['CC套装怎么用', 'CC套装'], ['Q10辅酵素有什么功效', 'Q10辅酵素'], ['IB5口腔喷雾怎么用', 'IB5口腔喷雾'], ['D-Drink小绿排毒饮怎么用', 'D-Drink'], ['Hair+发宝怎么用', 'Hair+发宝'], ['运动饮料Fitness-Drink是什么', 'Fitness-Drink'], ['TopShape纤萃减肥产品', 'TopShape'], ['Generation 50+乐活产品', 'Generation 50+'], ['Apple Antioxy细胞抗氧素功效', 'Apple Antioxy'], ['ProShape氨基酸BCAA是什么', 'ProShape'], ['Herbal Tea草本茶功效', 'Herbal Tea'], ['Med Dental+草本护理牙膏', 'Med Dental+'], ['Men Face男士护肤乳霜', 'Men Face'], ['叶黄素产品怎么吃', '叶黄素'], ['关节套装关节舒缓怎么用', '关节套装'], ['乳清蛋白粉适合谁', '乳清蛋白'], ['乐活奶昔怎么喝', '乐活奶昔'], ]; for (const [query, label] of productQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); describe('单KB查询 —— 系统/平台类', () => { const systemQueries = [ ['一成系统是什么', '一成系统'], ['三大平台介绍一下', '三大平台'], ['四大AI生态是什么', '四大AI生态'], ['行动圈怎么用', '行动圈'], ['盟主社区是什么', '盟主社区'], ['AI众享是什么', 'AI众享'], ['数字化工作室怎么用', '数字化工作室'], ['盛咖学愿培训平台', '盛咖学愿'], ]; for (const [query, label] of systemQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); describe('单KB查询 —— 科学原理类', () => { const scienceQueries = [ ['NTC营养保送系统是什么原理', 'NTC'], ['火炉原理是什么意思', '火炉原理'], ['阿育吠陀是什么', '阿育吠陀'], ]; for (const [query, label] of scienceQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); describe('单KB查询 —— 公司/认证类', () => { const companyQueries = [ ['德国PM公司介绍', '德国PM'], ['PM公司地址和电话', '地址电话'], ['邓白氏认证是什么', '邓白氏'], ['DSN全球100强', 'DSN'], ['ELAB科隆名单认证', 'ELAB'], ['PM是不是传销', '合法性'], ['Rolf Sorg是谁', '创始人'], ['宣明会慈善合作', '宣明会'], ['培安烟台工厂', '培安'], ]; for (const [query, label] of companyQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); describe('单KB查询 —— FAQ常见问题类', () => { const faqQueries = [ ['PM产品多久见效', '见效时间'], ['好转反应是什么', '好转反应'], ['为什么要全套搭配使用', '全套搭配'], ['和其他保健品有什么区别', '保健品区别'], ['孕妇能吃PM产品吗', '特殊人群'], ['产品多少钱', '价格'], ['怎么加入PM', '加入方式'], ['PM产品能治病吗', '治病声明'], ]; for (const [query, label] of faqQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); describe('单KB查询 —— 事业发展类', () => { const businessQueries = [ ['如何发展PM事业', '事业发展'], ['线上拓客怎么做', '线上拓客'], ['招商代理政策', '招商'], ['新人起步三关是什么', '新人培训'], ['为什么选择德国PM', '选择理由'], ['陌生客户怎么沟通PM事业', '陌生沟通'], ]; for (const [query, label] of businessQueries) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); // ================================================================ // 2. 多KB查询 —— 话题切换场景 // ================================================================ describe('多KB查询 —— 话题切换(同一会话中切换不同产品/话题)', () => { it('场景1: 大白→小红→小白 三个产品连续查询', () => { assertKbRoute('大白产品功效是什么'); const ctx1 = buildCtx([['user', '大白产品功效是什么'], ['assistant', '德国PM大白Basics是基础营养素...']]); assertKbRoute('那小红呢', ctx1); const ctx2 = buildCtx([ ['user', '大白产品功效是什么'], ['assistant', '德国PM大白Basics...'], ['user', '那小红呢'], ['assistant', 'FitLine小红Activize...'], ]); assertKbRoute('小白怎么吃', ctx2); }); it('场景2: 产品→公司→再回到产品', () => { assertKbRoute('基础三合一介绍一下'); assertKbRoute('德国PM公司是什么时候成立的'); const ctx = buildCtx([ ['user', '德国PM公司介绍'], ['assistant', '德国PM-International是1993年创立的...'], ]); assertKbRoute('那他们的产品有哪些', ctx); }); it('场景3: 产品→一成系统→培训', () => { assertKbRoute('CC套装怎么用'); assertKbRoute('一成系统是什么'); assertKbRoute('新人起步三关怎么做'); }); it('场景4: 科学原理→产品→FAQ', () => { assertKbRoute('NTC营养保送系统原理'); assertKbRoute('大白Basics功效'); assertKbRoute('多久能见效'); }); it('场景5: 合法性→公司→产品→事业', () => { assertKbRoute('PM是不是传销'); assertKbRoute('邓白氏AAA+是什么'); const ctx5 = buildCtx([['user', '邓白氏AAA+'], ['assistant', '邓白氏是全球最权威的商业信用评估机构...']]); assertKbRoute('那产品有哪些', ctx5); assertKbRoute('怎么加入PM事业'); }); it('场景6: 快速切换5个不同产品', () => { const products = [ 'D-Drink排毒饮怎么用', 'Q10辅酵素功效', 'IB5口腔喷雾是什么', '叶黄素怎么吃', '关节套装适合谁', ]; for (const q of products) { assertKbRoute(q); } }); }); // ================================================================ // 3. 追问场景(有上下文) // ================================================================ describe('追问场景 —— 基于上下文的知识库追问', () => { it('聊基础三合一后追问"怎么吃"', () => { const ctx = buildCtx([['user', '基础三合一介绍'], ['assistant', '基础三合一包含大白小红小白...']]); assertKbRoute('怎么吃', ctx); }); it('聊小红后追问"功效是什么"', () => { const ctx = buildCtx([['user', '小红产品'], ['assistant', 'FitLine小红Activize Oxyplus...']]); assertKbRoute('功效是什么', ctx); }); it('聊一成系统后追问"怎么用"', () => { const ctx = buildCtx([['user', '一成系统介绍'], ['assistant', '一成系统是德国PM事业...']]); assertKbRoute('怎么用', ctx); }); it('聊CC套装后追问"适合谁"', () => { const ctx = buildCtx([['user', 'CC套装功效'], ['assistant', 'CC套装含有葡萄籽提取物...']]); assertKbRoute('适合谁', ctx); }); it('聊火炉原理后追问"什么意思"', () => { const ctx = buildCtx([['user', '火炉原理'], ['assistant', '火炉原理是PM产品的核心理念...']]); assertKbRoute('什么意思', ctx); }); it('聊D-Drink后追问"多少钱"', () => { const ctx = buildCtx([['user', 'D-Drink小绿怎么用'], ['assistant', 'D-Drink小绿是14天排毒饮料...']]); assertKbRoute('多少钱', ctx); }); it('聊邓白氏后追问"什么意思"', () => { const ctx = buildCtx([['user', '邓白氏AAA+认证'], ['assistant', '邓白氏是全球最权威的...']]); assertKbRoute('什么意思', ctx); }); it('聊NTC后追问"有什么好处"', () => { const ctx = buildCtx([['user', 'NTC营养保送系统'], ['assistant', 'NTC营养保送系统是PM的核心技术...']]); assertKbRoute('有什么好处', ctx); }); it('聊Hair+后追问"成分是什么"', () => { const ctx = buildCtx([['user', 'Hair+发宝怎么用'], ['assistant', 'Hair+包含口服发宝和外用发健...']]); assertKbRoute('成分是什么', ctx); }); it('用代词追问"这个产品怎么吃"', () => { const ctx = buildCtx([['user', '小白Restorate功效'], ['assistant', '德国PM小白Restorate的核心功效是夜间修复...']]); assertKbRoute('这个产品怎么吃', ctx); }); }); // ================================================================ // 4. KB查询 + 质疑混合场景 // ================================================================ describe('KB查询+质疑混合 —— 用户查询后对回答产生质疑', () => { it('场景1: 问基础三合一→AI说冲剂→用户纠正"不对,是胶囊"', () => { assertKbRoute('基础三合一怎么吃'); const ctx = buildCtx([['user', '基础三合一怎么吃'], ['assistant', '基础三合一这样吃...']]); assertKbRoute('不对,是胶囊不是冲剂', ctx); }); it('场景2: 问小红功效→用户质疑"你搞错了吧"', () => { assertKbRoute('小红产品功效'); const ctx = buildCtx([['user', '小红产品功效'], ['assistant', '小红Activize...']]); assertKbRoute('你搞错了吧', ctx); }); it('场景3: 问NTC原理→用户说"我听说不是这样的"', () => { assertKbRoute('NTC营养保送系统原理'); const ctx = buildCtx([['user', 'NTC原理'], ['assistant', 'NTC营养保送系统...']]); assertKbRoute('我听说不是这样的', ctx); }); it('场景4: 问传销→用户要求"再查一下"', () => { assertKbRoute('PM是不是传销'); const ctx = buildCtx([['user', 'PM是不是传销'], ['assistant', '德国PM不是传销...']]); assertKbRoute('你再查查,我看网上说法不一样', ctx); }); it('场景5: 问价格→用户质疑"太贵了,你确定吗"', () => { assertKbRoute('产品多少钱'); const ctx = buildCtx([['user', '产品多少钱'], ['assistant', '产品价格因国家和地区有所不同...']]); assertKbRoute('你确定吗,怎么那么贵', ctx); }); it('场景6: 问CC套装→用户说"明明是乳霜不是胶囊"', () => { assertKbRoute('CC套装是什么'); const ctx = buildCtx([['user', 'CC套装'], ['assistant', 'CC套装包含CC-Cell胶囊和乳霜...']]); assertKbRoute('明明是乳霜不是胶囊', ctx); }); it('场景7: 问好转反应→用户说"骗人的吧"', () => { assertKbRoute('好转反应是怎么回事'); const ctx = buildCtx([['user', '好转反应'], ['assistant', '这是正常的好转反应...']]); assertKbRoute('骗人的吧,有科学依据吗', ctx); }); it('场景8: 问一成系统→用户说"跟我了解的不一样"', () => { assertKbRoute('一成系统介绍'); const ctx = buildCtx([['user', '一成系统'], ['assistant', '一成系统是德国PM事业发展的智能赋能工具...']]); assertKbRoute('跟我了解的不一样啊', ctx); }); it('场景9: 问D-Drink→用户说"这个是泡着喝的吧"', () => { assertKbRoute('D-Drink怎么用'); const ctx = buildCtx([['user', 'D-Drink怎么用'], ['assistant', 'D-Drink小绿是14天排毒饮料...']]); assertKbRoute('这个是泡着喝的吧', ctx); }); it('场景10: 问邓白氏→用户说"谁说的?有什么根据"', () => { assertKbRoute('邓白氏评级是什么'); const ctx = buildCtx([['user', '邓白氏'], ['assistant', '邓白氏是全球最权威的商业信用评估机构...']]); assertKbRoute('谁说的?有什么根据', ctx); }); }); // ================================================================ // 5. 多轮复杂对话场景(查询→追问→质疑→再查→切换话题) // ================================================================ describe('多轮复杂对话 —— 模拟真实用户完整会话', () => { it('5轮对话: 产品查询→追问→质疑→纠正→切换话题', () => { // 轮1: 直接问产品 assertKbRoute('基础三合一是什么'); // 轮2: 追问怎么吃 const ctx2 = buildCtx([['user', '基础三合一是什么'], ['assistant', '基础三合一包含大白小红小白...']]); assertKbRoute('怎么吃', ctx2); // 轮3: 质疑回答 const ctx3 = buildCtx([ ['user', '基础三合一是什么'], ['assistant', '基础三合一包含大白小红小白...'], ['user', '怎么吃'], ['assistant', '大白早上空腹1平勺...'], ]); assertKbRoute('你说的温度不对吧', ctx3); // 轮4: 用户纠正 assertKbRoute('应该是40度以下的水', ctx3); // 轮5: 切换到完全不同的话题 assertKbRoute('PM是不是传销'); }); it('6轮对话: 系统→子功能→质疑→公司→产品→FAQ', () => { assertKbRoute('一成系统介绍'); const ctx2 = buildCtx([['user', '一成系统'], ['assistant', '一成系统是德国PM事业发展的智能赋能工具...']]); assertKbRoute('行动圈是什么', ctx2); const ctx3 = buildCtx([['user', '一成系统'], ['assistant', '一成系统是德国PM事业发展的智能赋能工具...'], ['user', '行动圈是什么'], ['assistant', '行动圈是数字化工作室里的团队管理功能...']]); assertKbRoute('好像不是这样吧', ctx3); assertKbRoute('德国PM公司背景'); assertKbRoute('大白产品功效'); assertKbRoute('孕妇能吃吗'); }); it('4轮对话: 连续质疑同一个话题', () => { assertKbRoute('火炉原理是什么'); const ctx1 = buildCtx([['user', '火炉原理'], ['assistant', '火炉原理是PM产品的核心理念比喻...']]); assertKbRoute('不对,我记得不是这么说的', ctx1); assertKbRoute('你再查查,应该是另一种说法', ctx1); assertKbRoute('算了,到底是什么意思', ctx1); }); }); // ================================================================ // 6. 确定性改写验证 // ================================================================ describe('确定性改写 —— buildDeterministicKnowledgeQuery', () => { describe('产品直接改写', () => { const cases = [ ['基础三合一怎么吃', '基础套装'], ['大白产品', 'Basics'], ['小红Activize功效', 'Activize'], ['小白Restorate成分', 'Restorate'], ['儿童倍适怎么吃', '儿童倍适'], ['CC套装功效', 'CC'], ['Q10辅酵素作用', 'Q10'], ['IB5口腔喷雾', 'IB5'], ['D-Drink排毒', 'D-Drink'], ]; for (const [query, expectContain] of cases) { it(`"${query}" → 改写应含"${expectContain}"`, () => { const result = ToolExecutor.buildDeterministicKnowledgeQuery(query, []); assert.ok(result, `Should have deterministic rewrite for "${query}"`); assert.ok(result.includes(expectContain), `Rewrite should contain "${expectContain}", got "${result}"`); }); } }); describe('上下文追问改写', () => { it('上下文有"大白"时追问"怎么吃" → 改写含Basics', () => { const ctx = [{ role: 'assistant', content: '大白Basics是基础营养素...' }]; const result = ToolExecutor.buildDeterministicKnowledgeQuery('怎么吃', ctx); assert.ok(result, 'Should rewrite'); assert.ok(result.includes('Basics'), `Should contain Basics, got "${result}"`); }); it('上下文有"一成系统"时追问"怎么用" → 改写含一成系统', () => { const ctx = [{ role: 'assistant', content: '一成系统是德国PM事业发展...' }]; const result = ToolExecutor.buildDeterministicKnowledgeQuery('怎么用', ctx); assert.ok(result, 'Should rewrite'); assert.ok(result.includes('一成系统'), `Should contain 一成系统, got "${result}"`); }); it('上下文有"火炉原理"时追问"什么意思" → 改写含火炉原理', () => { const ctx = [{ role: 'assistant', content: '火炉原理是PM产品的核心理念...' }]; const result = ToolExecutor.buildDeterministicKnowledgeQuery('什么意思', ctx); assert.ok(result === '火炉原理', `Should rewrite to 火炉原理, got "${result}"`); }); }); describe('无匹配时返回空', () => { it('"今天天气好" → 无确定性改写', () => { const result = ToolExecutor.buildDeterministicKnowledgeQuery('今天天气好', []); assert.equal(result, '', 'Chitchat should not have deterministic rewrite'); }); }); }); // ================================================================ // 7. 热答案匹配测试 // ================================================================ describe('热答案匹配 —— matchHotAnswer 验证', () => { // matchHotAnswer是模块内部函数,通过ToolExecutor.execute间接调用 // 这里直接测试确定性改写+KB路由来验证热答案可达性 const hotTopics = [ ['基础三合一怎么吃', '基础三合一吃法'], ['PM是不是传销', '合法性'], ['NTC核心优势是什么', 'NTC核心优势'], ['多久见效', '见效时间'], ['为什么要全套搭配', '全套搭配原因'], ['好转反应是什么', '好转反应'], ['德国PM公司介绍', '公司介绍'], ['小红功效', '小红功效'], ['大白功效', '大白功效'], ['小白功效', '小白功效'], ['和其他保健品有什么区别', '保健品区别'], ['CC套装怎么用', 'CC套装'], ['Q10功效', 'Q10功效'], ['IB5怎么用', 'IB5'], ['邓白氏AAA+是什么', '邓白氏'], ['一成系统是什么', '一成系统'], ['火炉原理是什么', '火炉原理'], ['D-Drink排毒饮怎么用', 'D-Drink'], ['怎么加入PM', '加入方式'], ['孕妇能吃PM产品吗', '特殊人群'], ['多少钱', '价格'], ]; for (const [query, label] of hotTopics) { it(`${label}: "${query}" → 应路由到KB(热答案可达)`, () => { assertKbRoute(query); }); } }); // ================================================================ // 8. 问题维度交叉测试(同一产品不同问法) // ================================================================ describe('问题维度交叉 —— 同一产品的不同问法', () => { describe('基础三合一 × 多维度', () => { const dimensions = [ '基础三合一怎么吃', '基础三合一功效', '基础三合一成分', '基础三合一多少钱', '基础三合一适合谁', '为什么要全套搭配三合一', ]; for (const q of dimensions) { it(`"${q}" → 应走KB`, () => { assertKbRoute(q); }); } }); describe('小红 × 多维度', () => { const dimensions = [ '小红怎么吃', '小红功效是什么', '小红成分有哪些', '小红副作用', '小红多少钱', '小红适合什么人', ]; for (const q of dimensions) { it(`"${q}" → 应走KB`, () => { assertKbRoute(q); }); } }); describe('一成系统 × 多维度', () => { const dimensions = [ '一成系统是什么', '一成系统核心竞争力', '一成系统怎么用', '一成系统三大平台', '一成系统AI智能生产力', '一成系统线上拓客', '一成系统邀约话术', '一成系统文化', ]; for (const q of dimensions) { it(`"${q}" → 应走KB`, () => { assertKbRoute(q); }); } }); }); // ================================================================ // 9. 口语/ASR变体测试(语音识别的常见错误变体) // ================================================================ describe('口语/ASR变体 —— 语音识别常见变体', () => { const asrVariants = [ ['移程系统', '一成系统ASR变体'], ['PM细胞营养素', 'PM产品'], ['暖炉原理', '火炉原理ASR变体'], ['产品有哪些', '产品列表'], ['你们公司产品', '口语化'], ['这个东西怎么吃', '口语化追问'], ['咱们公司介绍一下', '口语化公司'], ]; for (const [query, label] of asrVariants) { it(`${label}: "${query}" → 应走KB路由`, () => { assertKbRoute(query); }); } }); // ================================================================ // 10. 边界测试 // ================================================================ describe('边界测试', () => { it('空字符串 → 不走KB', () => { assertNotKbRoute(''); }); it('纯标点 → 不走KB', () => { assertNotKbRoute('???'); }); it('单字"好" → 不走KB', () => { assertNotKbRoute('好'); }); it('纯数字 → 不走KB', () => { assertNotKbRoute('123456'); }); it('超长无意义文本 → 不走KB', () => { assertNotKbRoute('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊'); }); it('纯英文闲聊 → 不走KB', () => { assertNotKbRoute('hello how are you'); }); it('含KB关键词但实际是闲聊的边界', () => { // "不对"是质疑词,会被检测为KB follow-up // 需要context才会真正路由到KB // 无context时: hasKnowledgeRouteKeyword('不对') → true (因为加了质疑词) // 这是预期行为:宁可多查一次KB,也不要漏掉用户质疑 const result = shouldForceKnowledgeRoute('不对'); assert.equal(typeof result, 'boolean', 'Should return boolean'); }); }); console.log('\n=== KB场景测试加载完成 ===\n');