Files
rag/scripts/analyze_rag_performance.py

274 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
RAG 性能分析脚本
分析 RAG 流程各阶段耗时,帮助定位性能瓶颈
"""
import sys
import os
import json
import time
import argparse
from datetime import datetime
from typing import Dict
# 添加项目根目录到路径
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)
def call_rag_stream_api(
question: str,
kb_name: str = "public_kb",
base_url: str = "http://localhost:5001",
) -> Dict:
"""
调用 RAG 流式 API 并收集各阶段事件
Returns:
包含各阶段耗时信息的字典
"""
import requests
url = f"{base_url.rstrip('/')}/rag"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer mock-token-admin"
}
data = {
"message": question,
"collections": [kb_name] if kb_name else []
}
events = []
timing = {
'total_duration_ms': 0,
'search_time_ms': 0,
'rerank_time_ms': 0,
'llm_time_ms': 0,
'stages': []
}
try:
start_time = time.time()
response = requests.post(url, json=data, headers=headers, stream=True, timeout=300)
response.raise_for_status()
for line in response.iter_lines():
if line:
line_str = line.decode('utf-8')
if line_str.startswith('data: '):
try:
event = json.loads(line_str[6:])
event['_received_at'] = time.time()
events.append(event)
# 分析 finish 事件中的耗时信息
if event.get('type') == 'finish':
timing['total_duration_ms'] = event.get('duration_ms', 0)
if 'timing' in event:
timing['search_time_ms'] = event['timing'].get('total_search_ms', 0)
timing['rerank_time_ms'] = event['timing'].get('rerank_ms', 0)
timing['rerank_cached'] = event['timing'].get('rerank_cached', False)
except json.JSONDecodeError:
continue
end_time = time.time()
timing['actual_elapsed_ms'] = int((end_time - start_time) * 1000)
# 计算 LLM 生成时间
if timing['total_duration_ms'] > 0 and timing['search_time_ms'] > 0:
timing['llm_time_ms'] = timing['total_duration_ms'] - timing['search_time_ms']
finish_event = next((event for event in events if event.get('type') == 'finish'), None)
error_event = next((event for event in events if event.get('type') == 'error'), None)
return {
'success': finish_event is not None and error_event is None,
'error': error_event.get('message', '') if error_event else (
'' if finish_event else 'SSE 流结束但未收到 finish 事件'
),
'events': events,
'timing': timing,
'answer': finish_event.get('answer', '') if finish_event else '',
}
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': str(e),
'events': events,
'timing': timing
}
def analyze_performance(result: Dict, question: str) -> None:
"""分析并打印性能数据"""
print("\n" + "=" * 80)
print(f"问题: {question}")
print("=" * 80)
if not result['success']:
print(f"[X] 请求失败: {result.get('error', '未知错误')}")
return
timing = result['timing']
events = result['events']
# 打印各阶段事件时间线
print("\n[事件时间线]")
print("-" * 80)
first_event_time = None
for event in events:
event_type = event.get('type', 'unknown')
received_at = event.get('_received_at', 0)
if first_event_time is None:
first_event_time = received_at
relative_time = 0
else:
relative_time = (received_at - first_event_time) * 1000
if event_type == 'finish':
print(f" {relative_time:>8.0f}ms | {event_type:20s} | 总耗时: {event.get('duration_ms', 0)}ms")
elif event_type == 'sources':
sources = event.get('sources', [])
print(f" {relative_time:>8.0f}ms | {event_type:20s} | 找到 {len(sources)} 个来源")
elif event_type == 'chunks_retrieved':
chunks = event.get('data', {}).get('chunks', [])
print(f" {relative_time:>8.0f}ms | {event_type:20s} | 召回 {len(chunks)} 个切片")
elif event_type == 'chunk':
# 流式输出,只显示第一个
if not hasattr(analyze_performance, '_chunk_printed'):
print(f" {relative_time:>8.0f}ms | {event_type:20s} | 开始流式输出...")
analyze_performance._chunk_printed = True
else:
print(f" {relative_time:>8.0f}ms | {event_type:20s}")
if hasattr(analyze_performance, '_chunk_printed'):
delattr(analyze_performance, '_chunk_printed')
# 打印耗时统计
print("\n[耗时统计]")
print("-" * 80)
total = timing['total_duration_ms']
search = timing['search_time_ms']
rerank = timing['rerank_time_ms']
llm = timing['llm_time_ms']
actual = timing.get('actual_elapsed_ms', total)
if total > 0:
print(f" 总耗时 (API报告): {total:>8}ms ({total/1000:.2f}s)")
print(f" 实际耗时 (本地测量): {actual:>8}ms ({actual/1000:.2f}s)")
print()
print(f" 检索阶段: {search:>8}ms ({search/total*100:.1f}%)")
if rerank > 0:
cached_flag = " [缓存]" if timing.get('rerank_cached') else ""
print(f" 重排序阶段: {rerank:>8}ms ({rerank/total*100:.1f}%){cached_flag}")
print(f" LLM生成阶段: {llm:>8}ms ({llm/total*100:.1f}%)")
# 性能诊断
print("\n[性能诊断]")
print("-" * 80)
if total > 10000:
print(" [!] 总耗时超过 10 秒,需要优化")
if search > 3000:
print(f" [!] 检索耗时过长 ({search}ms),可能原因:")
print(" - 向量库数据量过大")
print(" - 未命中查询缓存")
print(" - BM25 索引加载慢")
if rerank > 2000 and not timing.get('rerank_cached'):
print(f" [!] 重排序耗时过长 ({rerank}ms),可能原因:")
print(" - Rerank 模型计算量大")
print(" - 候选切片数量过多")
if llm > 5000:
print(f" [!] LLM生成耗时过长 ({llm}ms),可能原因:")
print(" - 模型生成速度慢")
print(" - 输出 token 数量多")
print(" - 网络延迟")
print(" 建议: 检查 MiMo thinking 是否关闭、上下文长度及 max_tokens")
if total < 5000:
print(" [OK] 性能良好")
else:
print(" [!] 未能获取耗时数据")
def run_performance_tests(
base_url: str = "http://localhost:5001",
kb_name: str = "public_kb",
question: str = None,
):
"""执行性能测试"""
# 测试问题(不同类型)
test_questions = [
{"question": "智启科技成立于哪一年?", "type": "精确匹配"},
{"question": "公司的愿景是什么?", "type": "语义理解"},
{"question": "公司有哪些分公司,分别在哪些城市?", "type": "跨文档关联"},
{"question": "一个入职3年的员工累计病假1个月能拿到多少病假工资", "type": "复杂推理"},
{"question": "P4级工程师的年薪范围是多少", "type": "表格数据"},
]
if question:
test_questions = [{"question": question, "type": "自定义问题"}]
print("=" * 80)
print("RAG Performance Analysis")
print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 80)
results = []
for i, test in enumerate(test_questions):
print(f"\n\n[{i+1}/{len(test_questions)}] 类型: {test['type']}")
result = call_rag_stream_api(test['question'], kb_name=kb_name, base_url=base_url)
analyze_performance(result, test['question'])
results.append({
'question': test['question'],
'type': test['type'],
'success': result['success'],
'timing': result['timing']
})
# 汇总统计
print("\n\n" + "=" * 80)
print("[Performance Summary]")
print("=" * 80)
successful = [r for r in results if r['success']]
if successful:
total_times = [r['timing']['total_duration_ms'] for r in successful]
search_times = [r['timing']['search_time_ms'] for r in successful]
llm_times = [r['timing']['llm_time_ms'] for r in successful]
print(f"\n成功请求数: {len(successful)}/{len(results)}")
print(f"\n平均总耗时: {sum(total_times)/len(total_times):.0f}ms")
print(f"平均检索耗时: {sum(search_times)/len(search_times):.0f}ms")
print(f"平均LLM耗时: {sum(llm_times)/len(llm_times):.0f}ms")
print(f"\n最快总耗时: {min(total_times)}ms")
print(f"最慢总耗时: {max(total_times)}ms")
# 保存详细报告
report_dir = os.path.join(PROJECT_ROOT, ".data", "performance")
os.makedirs(report_dir, exist_ok=True)
report_file = os.path.join(
report_dir,
f"performance_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
)
with open(report_file, 'w', encoding='utf-8') as f:
json.dump({
'test_time': datetime.now().isoformat(),
'results': results
}, f, ensure_ascii=False, indent=2)
print(f"\n详细报告已保存: {report_file}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="RAG SSE 链路耗时分析")
parser.add_argument("--base-url", default="http://localhost:5001")
parser.add_argument("--kb", default="public_kb", help="目标知识库名称")
parser.add_argument("--question", help="只测试一个自定义问题")
args = parser.parse_args()
run_performance_tests(args.base_url, args.kb, args.question)