355 lines
11 KiB
JavaScript
355 lines
11 KiB
JavaScript
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const { performance } = require('perf_hooks');
|
|||
|
|
|
|||
|
|
class MockToolExecutor {
|
|||
|
|
static async searchKnowledge({ query, response_mode }, context) {
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
|
|||
|
|
|
|||
|
|
const mockResults = {
|
|||
|
|
'小红产品有什么功效': {
|
|||
|
|
hit: true,
|
|||
|
|
content: 'FitLine小红Activize Oxyplus的核心功效是提升细胞能量。它含有维生素B族、C和辅酶Q10等成分。主要作用:1.提升精力和体能。2.促进细胞氧气利用。3.增强免疫力。'
|
|||
|
|
},
|
|||
|
|
'大白产品怎么吃': {
|
|||
|
|
hit: true,
|
|||
|
|
content: '德国PM大白Basics是基础营养素,早上空腹服用,1平勺兑200-300ml温水。'
|
|||
|
|
},
|
|||
|
|
'德国PM公司介绍': {
|
|||
|
|
hit: true,
|
|||
|
|
content: '德国PM-International是1993年创立的国际营养品直销企业。核心品牌FitLine专注细胞营养,拥有独家NTC营养保送系统技术。'
|
|||
|
|
},
|
|||
|
|
'NTC营养保送系统原理': {
|
|||
|
|
hit: true,
|
|||
|
|
content: 'NTC营养保送系统是PM产品的核心技术优势。它能确保营养素在体内精准保送到细胞层面。'
|
|||
|
|
},
|
|||
|
|
'基础三合一怎么吃': {
|
|||
|
|
hit: true,
|
|||
|
|
hot_answer: true,
|
|||
|
|
content: '基础三合一这样吃:1.大白Basics:早上空腹,1平勺兑200-300ml温水。2.小红Activize:大白喝完15-30分钟后,兑100-150ml温水。3.小白Restorate:睡前空腹,1平勺兑200ml温水。'
|
|||
|
|
},
|
|||
|
|
'今天天气怎么样': {
|
|||
|
|
hit: false,
|
|||
|
|
content: '知识库中暂未找到相关信息。'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const result = mockResults[query] || { hit: false, content: '未找到相关信息' };
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
query,
|
|||
|
|
results: [{ title: '模拟知识库结果', content: result.content }],
|
|||
|
|
hit: result.hit,
|
|||
|
|
source: result.hot_answer ? 'hot_answer_cache' : 'mock_knowledge',
|
|||
|
|
hot_answer: result.hot_answer
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class VikingRetrievalPerformanceTester {
|
|||
|
|
constructor(options = {}) {
|
|||
|
|
this.results = [];
|
|||
|
|
this.outputDir = options.outputDir || path.join(__dirname, 'test_results');
|
|||
|
|
this.verbose = options.verbose !== false;
|
|||
|
|
this.warmupRuns = options.warmupRuns || 2;
|
|||
|
|
this.mockMode = options.mockMode !== false;
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(this.outputDir)) {
|
|||
|
|
fs.mkdirSync(this.outputDir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log(msg) {
|
|||
|
|
if (this.verbose) {
|
|||
|
|
console.log(`[VikingTest] ${msg}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async warmup(queries) {
|
|||
|
|
this.log('Warming up...');
|
|||
|
|
const ToolExecutor = this.mockMode ? MockToolExecutor : require('../services/toolExecutor');
|
|||
|
|
for (let i = 0; i < this.warmupRuns; i++) {
|
|||
|
|
for (const query of queries) {
|
|||
|
|
try {
|
|||
|
|
await ToolExecutor.searchKnowledge({ query, response_mode: 'answer' }, []);
|
|||
|
|
} catch (e) {
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
this.log('Warmup complete');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async testLatency(testQueries, iterations = 10) {
|
|||
|
|
this.log(`Starting latency test with ${iterations} iterations...`);
|
|||
|
|
const ToolExecutor = this.mockMode ? MockToolExecutor : require('../services/toolExecutor');
|
|||
|
|
|
|||
|
|
const results = {};
|
|||
|
|
|
|||
|
|
for (const { name, query } of testQueries) {
|
|||
|
|
this.log(`Testing query: ${name}`);
|
|||
|
|
const latencies = [];
|
|||
|
|
const hits = [];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < iterations; i++) {
|
|||
|
|
const start = performance.now();
|
|||
|
|
let result;
|
|||
|
|
try {
|
|||
|
|
result = await ToolExecutor.searchKnowledge({ query, response_mode: 'answer' }, []);
|
|||
|
|
const latency = performance.now() - start;
|
|||
|
|
latencies.push(latency);
|
|||
|
|
hits.push(!!result.hit);
|
|||
|
|
this.log(` Iteration ${i + 1}: ${latency.toFixed(2)}ms, hit=${result.hit}`);
|
|||
|
|
} catch (e) {
|
|||
|
|
this.log(` Iteration ${i + 1} error: ${e.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
results[name] = {
|
|||
|
|
query,
|
|||
|
|
latencies,
|
|||
|
|
hits,
|
|||
|
|
avgLatency: latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0,
|
|||
|
|
minLatency: latencies.length ? Math.min(...latencies) : 0,
|
|||
|
|
maxLatency: latencies.length ? Math.max(...latencies) : 0,
|
|||
|
|
p50Latency: this.percentile(latencies, 50),
|
|||
|
|
p95Latency: this.percentile(latencies, 95),
|
|||
|
|
p99Latency: this.percentile(latencies, 99),
|
|||
|
|
hitRate: hits.length ? hits.filter(h => h).length / hits.length : 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.results.push({
|
|||
|
|
type: 'latency',
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
iterations,
|
|||
|
|
mockMode: this.mockMode,
|
|||
|
|
results
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async testCacheEfficiency(queries, cacheHitsIterations = 5) {
|
|||
|
|
this.log('Testing cache efficiency...');
|
|||
|
|
const ToolExecutor = this.mockMode ? MockToolExecutor : require('../services/toolExecutor');
|
|||
|
|
|
|||
|
|
const results = [];
|
|||
|
|
|
|||
|
|
for (const { name, query } of queries) {
|
|||
|
|
this.log(`Testing cache for query: ${name}`);
|
|||
|
|
|
|||
|
|
const firstStart = performance.now();
|
|||
|
|
const firstResult = await ToolExecutor.searchKnowledge({ query, response_mode: 'answer' }, []);
|
|||
|
|
const firstLatency = performance.now() - firstStart;
|
|||
|
|
|
|||
|
|
const cacheLatencies = [];
|
|||
|
|
for (let i = 0; i < cacheHitsIterations; i++) {
|
|||
|
|
const start = performance.now();
|
|||
|
|
const result = await ToolExecutor.searchKnowledge({ query, response_mode: 'answer' }, []);
|
|||
|
|
const latency = performance.now() - start;
|
|||
|
|
cacheLatencies.push(latency);
|
|||
|
|
this.log(` Cache hit ${i + 1}: ${latency.toFixed(2)}ms`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const avgCacheLatency = cacheLatencies.reduce((a, b) => a + b, 0) / cacheLatencies.length;
|
|||
|
|
|
|||
|
|
results.push({
|
|||
|
|
name,
|
|||
|
|
query,
|
|||
|
|
firstHitLatency: firstLatency,
|
|||
|
|
cacheHitLatencies: cacheLatencies,
|
|||
|
|
avgCacheLatency,
|
|||
|
|
speedup: firstLatency / avgCacheLatency,
|
|||
|
|
firstHit: firstResult
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.results.push({
|
|||
|
|
type: 'cache',
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
cacheHitsIterations,
|
|||
|
|
mockMode: this.mockMode,
|
|||
|
|
results
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async testConcurrency(queries, concurrencyLevels = [1, 5, 10, 20]) {
|
|||
|
|
this.log('Testing concurrency...');
|
|||
|
|
const ToolExecutor = this.mockMode ? MockToolExecutor : require('../services/toolExecutor');
|
|||
|
|
|
|||
|
|
const results = {};
|
|||
|
|
|
|||
|
|
for (const concurrency of concurrencyLevels) {
|
|||
|
|
this.log(`Testing concurrency level: ${concurrency}`);
|
|||
|
|
|
|||
|
|
const startTime = performance.now();
|
|||
|
|
const promises = [];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < concurrency; i++) {
|
|||
|
|
const queryObj = queries[i % queries.length];
|
|||
|
|
promises.push(
|
|||
|
|
ToolExecutor.searchKnowledge({ query: queryObj.query, response_mode: 'answer' }, [])
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const allResults = await Promise.all(promises);
|
|||
|
|
const totalTime = performance.now() - startTime;
|
|||
|
|
|
|||
|
|
const successCount = allResults.filter(r => r && !r.error).length;
|
|||
|
|
|
|||
|
|
results[concurrency] = {
|
|||
|
|
concurrency,
|
|||
|
|
totalTime,
|
|||
|
|
throughput: concurrency / (totalTime / 1000),
|
|||
|
|
successRate: successCount / concurrency,
|
|||
|
|
results: allResults
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.log(` Throughput: ${results[concurrency].throughput.toFixed(2)} req/s`);
|
|||
|
|
this.log(` Success rate: ${(results[concurrency].successRate * 100).toFixed(1)}%`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.results.push({
|
|||
|
|
type: 'concurrency',
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
concurrencyLevels,
|
|||
|
|
mockMode: this.mockMode,
|
|||
|
|
results
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
percentile(arr, p) {
|
|||
|
|
if (arr.length === 0) return 0;
|
|||
|
|
const sorted = [...arr].sort((a, b) => a - b);
|
|||
|
|
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|||
|
|
return sorted[Math.max(0, index)];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
generateReport() {
|
|||
|
|
const report = {
|
|||
|
|
generatedAt: new Date().toISOString(),
|
|||
|
|
mockMode: this.mockMode,
|
|||
|
|
summary: {},
|
|||
|
|
tests: this.results
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const test of this.results) {
|
|||
|
|
if (test.type === 'latency') {
|
|||
|
|
report.summary.latency = Object.fromEntries(
|
|||
|
|
Object.entries(test.results).map(([name, data]) => [
|
|||
|
|
name,
|
|||
|
|
{
|
|||
|
|
avg: data.avgLatency.toFixed(2),
|
|||
|
|
p95: data.p95Latency.toFixed(2),
|
|||
|
|
hitRate: (data.hitRate * 100).toFixed(1) + '%'
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
);
|
|||
|
|
} else if (test.type === 'cache') {
|
|||
|
|
report.summary.cache = test.results.map(r => ({
|
|||
|
|
name: r.name,
|
|||
|
|
speedup: r.speedup.toFixed(2) + 'x'
|
|||
|
|
}));
|
|||
|
|
} else if (test.type === 'concurrency') {
|
|||
|
|
report.summary.concurrency = Object.fromEntries(
|
|||
|
|
Object.entries(test.results).map(([level, data]) => [
|
|||
|
|
level,
|
|||
|
|
{
|
|||
|
|
throughput: data.throughput.toFixed(2) + ' req/s',
|
|||
|
|
successRate: (data.successRate * 100).toFixed(1) + '%'
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return report;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saveReport(filename = null) {
|
|||
|
|
const report = this.generateReport();
|
|||
|
|
const filepath = path.join(
|
|||
|
|
this.outputDir,
|
|||
|
|
filename || `viking_performance_${Date.now()}.json`
|
|||
|
|
);
|
|||
|
|
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
|
|||
|
|
this.log(`Report saved to ${filepath}`);
|
|||
|
|
return filepath;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
printSummary() {
|
|||
|
|
console.log('\n' + '='.repeat(80));
|
|||
|
|
console.log('VIKING RETRIEVAL PERFORMANCE TEST SUMMARY');
|
|||
|
|
if (this.mockMode) {
|
|||
|
|
console.log('(Mock Mode - For Framework Validation)');
|
|||
|
|
}
|
|||
|
|
console.log('='.repeat(80));
|
|||
|
|
|
|||
|
|
const report = this.generateReport();
|
|||
|
|
|
|||
|
|
if (report.summary.latency) {
|
|||
|
|
console.log('\n--- Latency Test ---');
|
|||
|
|
for (const [name, data] of Object.entries(report.summary.latency)) {
|
|||
|
|
console.log(` ${name}:`);
|
|||
|
|
console.log(` Avg: ${data.avg}ms, P95: ${data.p95}ms, Hit Rate: ${data.hitRate}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (report.summary.cache) {
|
|||
|
|
console.log('\n--- Cache Efficiency ---');
|
|||
|
|
for (const r of report.summary.cache) {
|
|||
|
|
console.log(` ${r.name}: Speedup ${r.speedup}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (report.summary.concurrency) {
|
|||
|
|
console.log('\n--- Concurrency Test ---');
|
|||
|
|
for (const [level, data] of Object.entries(report.summary.concurrency)) {
|
|||
|
|
console.log(` ${level} concurrent:`);
|
|||
|
|
console.log(` Throughput: ${data.throughput}, Success: ${data.successRate}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('\n' + '='.repeat(80));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async runFullSuite() {
|
|||
|
|
this.log('Starting full Viking retrieval performance test suite...');
|
|||
|
|
|
|||
|
|
const testQueries = [
|
|||
|
|
{ name: 'Product Query - Xiaohong', query: '小红产品有什么功效' },
|
|||
|
|
{ name: 'Product Query - Dabai', query: '大白产品怎么吃' },
|
|||
|
|
{ name: 'Company Info', query: '德国PM公司介绍' },
|
|||
|
|
{ name: 'NTC Technology', query: 'NTC营养保送系统原理' },
|
|||
|
|
{ name: 'Hot Answer', query: '基础三合一怎么吃' },
|
|||
|
|
{ name: 'No Hit Query', query: '今天天气怎么样' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
await this.warmup(testQueries.map(q => q.query));
|
|||
|
|
await this.testLatency(testQueries, 10);
|
|||
|
|
await this.testCacheEfficiency(testQueries.slice(0, 3), 5);
|
|||
|
|
await this.testConcurrency(testQueries.slice(0, 3), [1, 3, 5]);
|
|||
|
|
|
|||
|
|
this.printSummary();
|
|||
|
|
this.saveReport();
|
|||
|
|
|
|||
|
|
return this.results;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = VikingRetrievalPerformanceTester;
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
(async () => {
|
|||
|
|
const args = process.argv.slice(2);
|
|||
|
|
const mockMode = args[0] !== 'real';
|
|||
|
|
|
|||
|
|
const tester = new VikingRetrievalPerformanceTester({ mockMode });
|
|||
|
|
await tester.runFullSuite();
|
|||
|
|
})();
|
|||
|
|
}
|