feat(server): KB prompt优化、字幕修复、S2S重连、助手配置API

- assistantProfileConfig: KB answer prompt改为分层策略(严格产品信息+灵活常识补充)
- nativeVoiceGateway: S2S upstream自动重连(最多50次)、event 351字幕debounce(800ms取最长文本)
- toolExecutor: 确定性query改写增强、KB查询传递session上下文
- contextKeywordTracker: 支持KB话题记忆优先enrichment
- contentSafeGuard: 新增品牌安全内容过滤服务
- assistantProfileService: 新增助手配置CRUD服务
- routes/assistantProfile: 新增助手配置API路由
- knowledgeKeywords: 扩展KB关键词词典
- fastAsrCorrector: ASR纠错规则更新
- tests/: KB prompt测试、保护窗口测试、Viking性能测试
- docs/: 助手配置API文档、系统提示词目录
This commit is contained in:
User
2026-03-24 17:19:36 +08:00
parent 57a03677a9
commit 9567eb7358
34 changed files with 7076 additions and 46 deletions

View File

@@ -0,0 +1,593 @@
/**
* 基于知识库实际内容的功能性测试
* 覆盖单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');