Files
rag/scripts/test_rag_questions.py
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

355 lines
18 KiB
Python
Raw Permalink 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 系统测试脚本
自动执行 40 个测试问题并记录结果
"""
import sys
import os
import json
import time
from datetime import datetime
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 测试问题列表
TEST_QUESTIONS = [
# 一、精确匹配测试10题
{"id": 1, "category": "精确匹配", "question": "智启科技成立于哪一年?", "expected": "2015年3月", "source": "公司简介.txt / 员工手册.txt"},
{"id": 2, "category": "精确匹配", "question": "公司的客服热线是多少?", "expected": "400-888-8888", "source": "公司简介.txt"},
{"id": 3, "category": "精确匹配", "question": "公司总部位于哪里?", "expected": "北京市海淀区中关村大街1号科技大厦15层", "source": "公司简介.txt"},
{"id": 4, "category": "精确匹配", "question": "ZDAP平台标准版支持多少并发用户", "expected": "50人", "source": "产品手册.txt"},
{"id": 5, "category": "精确匹配", "question": "企业版单表最大支持多少行数据?", "expected": "1亿行", "source": "产品手册.txt"},
{"id": 6, "category": "精确匹配", "question": "年假满10年不满20年可以休多少天", "expected": "10天", "source": "员工手册.txt / 常见问题.txt"},
{"id": 7, "category": "精确匹配", "question": "产假可以休多少天?", "expected": "158天", "source": "员工手册.txt / 常见问题.txt"},
{"id": 8, "category": "精确匹配", "question": "技术研发中心的负责人是谁?", "expected": "张明远(技术总监)", "source": "公司简介.txt / 组织架构说明.txt"},
{"id": 9, "category": "精确匹配", "question": "市场营销部有多少人?", "expected": "120人", "source": "公司简介.txt / 组织架构说明.txt"},
{"id": 10, "category": "精确匹配", "question": "上海分公司的负责人是谁?", "expected": "华东区总经理 张华", "source": "组织架构说明.txt"},
# 二、语义理解测试6题
{"id": 11, "category": "语义理解", "question": "公司的愿景是什么?", "expected": "成为全球领先的智能数据服务提供商", "source": "公司简介.txt / 员工手册.txt"},
{"id": 12, "category": "语义理解", "question": "ZDAP的智能预警功能有哪些通知方式", "expected": "邮件、短信、企业微信、钉钉", "source": "产品手册.txt"},
{"id": 13, "category": "语义理解", "question": "什么是直连数据集?", "expected": "直接查询源数据库,实时性强", "source": "产品手册.txt"},
{"id": 14, "category": "语义理解", "question": "入职当天需要做什么?", "expected": "签订劳动合同、领取工牌、开通账号、参观、培训等", "source": "常见问题.txt"},
{"id": 15, "category": "语义理解", "question": "请假4天需要谁审批", "expected": "部门负责人 + 人力资源部审批", "source": "常见问题.txt"},
{"id": 16, "category": "语义理解", "question": "如何申请外部培训?", "expected": "OA系统提交申请→填写信息→部门负责人审批→人力资源部审批→超5000元签服务协议", "source": "常见问题.txt"},
# 三、跨文档关联测试4题
{"id": 17, "category": "跨文档关联", "question": "公司有哪些分公司,分别在哪些城市?", "expected": "上海、深圳、成都、武汉四家分公司", "source": "公司简介.txt + 组织架构说明.txt"},
{"id": 18, "category": "跨文档关联", "question": "技术研发中心下设哪些团队,各自的职责是什么?", "expected": "AI算法组、数据工程组、平台开发组、前端开发组、测试组", "source": "公司简介.txt + 组织架构说明.txt"},
{"id": 19, "category": "跨文档关联", "question": "公司的薪酬由哪些部分组成?绩效奖金的范围是多少?", "expected": "基本工资+绩效奖金+年终奖金+津贴补贴绩效奖金0-30%基本工资", "source": "员工手册.txt + 常见问题.txt"},
{"id": 20, "category": "跨文档关联", "question": "病假工资怎么算?不同工龄有什么区别?", "expected": "按工龄60%-100%发放", "source": "常见问题.txt"},
# 四、复杂推理测试3题
{"id": 21, "category": "复杂推理", "question": "一个入职3年的员工累计病假1个月能拿到多少病假工资", "expected": "工龄2-4年按基本工资70%发放", "source": "常见问题.txt"},
{"id": 22, "category": "复杂推理", "question": "如果我绩效考核连续两个月是D级会怎样", "expected": "进入绩效改进期(PIP)1-3个月改进期仍不达标可调岗或解除合同", "source": "常见问题.txt"},
{"id": 23, "category": "复杂推理", "question": "ZDAP企业版的简单查询响应时间要求是多少", "expected": "<1秒", "source": "产品手册.txt"},
# 五、表格数据测试3题
{"id": 24, "category": "表格数据", "question": "P4级工程师的年薪范围是多少", "expected": "25-35万元", "source": "组织架构说明.txt"},
{"id": 25, "category": "表格数据", "question": "M3级经理对应的职称是什么", "expected": "高级经理", "source": "组织架构说明.txt"},
{"id": 26, "category": "表格数据", "question": "数据工程组有多少人?负责人是谁?", "expected": "80人负责人王工", "source": "组织架构说明.txt"},
# 六、否定性测试3题
{"id": 27, "category": "否定性测试", "question": "公司有员工宿舍吗?", "expected": "没有员工宿舍但为外地新员工提供15天免费过渡住宿", "source": "常见问题.txt"},
{"id": 28, "category": "否定性测试", "question": "公司的股票代码是什么?", "expected": "文档中未提及", "source": ""},
{"id": 29, "category": "否定性测试", "question": "公司有食堂吗?", "expected": "没有食堂,但提供早餐和午餐", "source": "常见问题.txt"},
# 七、关键词检索测试3题
{"id": 30, "category": "关键词检索", "question": "五险一金缴纳比例是多少?", "expected": "养老保险个人8%公司16%医疗保险个人2%公司10%", "source": "常见问题.txt"},
{"id": 31, "category": "关键词检索", "question": "Kong Gateway在系统架构中的作用是什么", "expected": "API网关层JWT认证、限流熔断", "source": "产品手册.txt"},
{"id": 32, "category": "关键词检索", "question": "ClickHouse用于什么用途", "expected": "分析引擎", "source": "产品手册.txt"},
# 八、长文档/分块测试2题
{"id": 33, "category": "长文档测试", "question": "三峡工程2024年发电量是多少", "expected": "需从三峡公报PDF中检索", "source": "三峡公报_*.pdf"},
{"id": 34, "category": "长文档测试", "question": "三峡水库的水位调节范围是多少?", "expected": "需从三峡公报PDF中检索", "source": "三峡公报_*.pdf"},
# 九、学术论文测试2题
{"id": 35, "category": "学术论文", "question": "这篇论文的主要贡献是什么?", "expected": "需从论文PDF中提取", "source": "2604.09205v1.pdf"},
{"id": 36, "category": "学术论文", "question": "论文使用了什么方法或模型?", "expected": "需从论文PDF中提取", "source": "2604.09205v1.pdf"},
# 十、边缘案例测试4题
{"id": 37, "category": "边缘案例", "question": "如果我要报销5000元以内的费用需要谁审批", "expected": "主管审批,备案即可", "source": "组织架构说明.txt"},
{"id": 38, "category": "边缘案例", "question": "公司的核心工作时间是什么时候?", "expected": "10:00-16:00必须到岗", "source": "常见问题.txt"},
{"id": 39, "category": "边缘案例", "question": "ZDAP支持哪些图表类型", "expected": "柱状图、折线图、饼图、散点图、热力图、地图、雷达图、漏斗图", "source": "产品手册.txt"},
{"id": 40, "category": "边缘案例", "question": "技术支持热线的服务时间是怎样的?", "expected": "工作日9:00-21:00周末及节假日10:00-18:00", "source": "产品手册.txt"},
]
def call_rag_api(question: str, kb_name: str = "public", top_k: int = 5) -> dict:
"""
调用 RAG API 进行问答
Args:
question: 问题内容
kb_name: 知识库名称
top_k: 返回结果数量
Returns:
包含答案和检索上下文的字典
"""
import requests
url = "http://localhost:5001/rag"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer mock-token-admin"
}
data = {
"message": question # 使用 message 字段
}
try:
response = requests.post(url, json=data, headers=headers, timeout=120)
response.raise_for_status()
result = response.json()
return {
"answer": result.get("answer"),
"sources": result.get("sources", []),
"contexts": [] # /rag 接口不返回 contexts但有 sources
}
except requests.exceptions.RequestException as e:
return {"error": str(e), "answer": None, "sources": [], "contexts": []}
def evaluate_answer(actual: str, expected: str) -> dict:
"""
评估答案准确性
Args:
actual: 实际答案
expected: 预期答案
Returns:
评估结果字典
"""
if not actual:
return {"score": 0, "grade": "不合格", "reason": "未获取到答案", "matched_keywords": []}
actual_lower = actual.lower()
expected_lower = expected.lower()
# 使用 jieba 分词(中文友好)
try:
import jieba
expected_keywords = set(jieba.cut(expected_lower))
actual_keywords = set(jieba.cut(actual_lower))
except ImportError:
# jieba 未安装时的降级方案
expected_keywords = set(expected_lower.replace("", " ").replace("", " ").replace("", " ").split())
actual_keywords = set(actual_lower.replace("", " ").replace("", " ").replace("", " ").split())
# 过滤停用词和短词
stop_words = {"", "", "", "", "", "", "", "", "", "", "", "", "", "可以", "需要", "应该"}
expected_keywords = {w for w in expected_keywords if len(w) >= 2 and w not in stop_words}
actual_keywords = {w for w in actual_keywords if len(w) >= 2 and w not in stop_words}
matched_keywords = expected_keywords & actual_keywords
if len(expected_keywords) == 0:
keyword_score = 50
else:
keyword_score = int(len(matched_keywords) / len(expected_keywords) * 100)
# 完全匹配加分
if expected_lower in actual_lower:
keyword_score = min(100, keyword_score + 20)
# 评分等级
if keyword_score >= 80:
grade = "优秀"
elif keyword_score >= 60:
grade = "良好"
elif keyword_score >= 40:
grade = "合格"
else:
grade = "不合格"
return {
"score": keyword_score,
"grade": grade,
"matched_keywords": list(matched_keywords),
"reason": f"关键词匹配度 {keyword_score}%"
}
def run_tests():
"""执行所有测试"""
print("=" * 80)
print("RAG 系统测试开始")
print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"测试问题数量: {len(TEST_QUESTIONS)}")
print("=" * 80)
print()
results = []
for i, test in enumerate(TEST_QUESTIONS):
print(f"[{i+1}/{len(TEST_QUESTIONS)}] 测试类别: {test['category']}")
print(f"问题: {test['question']}")
# 调用 API
start_time = time.time()
response = call_rag_api(test['question'])
elapsed_time = time.time() - start_time
# 提取答案和上下文
answer = response.get("answer", "")
sources = response.get("sources", [])
# 评估答案
evaluation = evaluate_answer(answer, test['expected'])
result = {
"id": test['id'],
"category": test['category'],
"question": test['question'],
"expected": test['expected'],
"expected_source": test['source'],
"actual_answer": answer,
"actual_sources": sources,
"response_time": round(elapsed_time, 2),
"evaluation": evaluation
}
results.append(result)
# 打印结果
print(f"预期答案: {test['expected']}")
# 过滤 emoji 和特殊字符,避免编码问题
import re
def sanitize_text(text):
if not text:
return "无答案"
# 移除 emoji 和特殊 Unicode 字符
text = re.sub(r'[\U00010000-\U0010ffff]', '', text)
return text[:200] + "..." if len(text) > 200 else text
answer_display = sanitize_text(answer)
print(f"实际答案: {answer_display}")
print(f"检索来源: {sources[:3]}..." if len(sources) > 3 else f"检索来源: {sources}")
print(f"评分: {evaluation['score']} ({evaluation['grade']}) - {evaluation['reason']}")
print(f"响应时间: {elapsed_time:.2f}s")
print("-" * 80)
return results
def generate_report(results: list) -> str:
"""生成测试报告"""
# 统计各类别得分
category_stats = {}
for r in results:
cat = r['category']
if cat not in category_stats:
category_stats[cat] = {"total": 0, "scores": [], "times": []}
category_stats[cat]['total'] += 1
category_stats[cat]['scores'].append(r['evaluation']['score'])
category_stats[cat]['times'].append(r['response_time'])
# 计算总体统计
all_scores = [r['evaluation']['score'] for r in results]
all_times = [r['response_time'] for r in results]
# 生成报告
report = []
report.append("# RAG 系统测试报告")
report.append("")
report.append(f"**测试时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report.append(f"**测试问题数量**: {len(results)}")
report.append("")
# 总体统计
report.append("## 一、总体统计")
report.append("")
report.append(f"- **平均得分**: {sum(all_scores)/len(all_scores):.1f}")
report.append(f"- **最高得分**: {max(all_scores)}")
report.append(f"- **最低得分**: {min(all_scores)}")
report.append(f"- **平均响应时间**: {sum(all_times)/len(all_times):.2f}")
report.append(f"- **优秀率 (≥80分)**: {len([s for s in all_scores if s >= 80])/len(all_scores)*100:.1f}%")
report.append(f"- **合格率 (≥40分)**: {len([s for s in all_scores if s >= 40])/len(all_scores)*100:.1f}%")
report.append("")
# 分类统计
report.append("## 二、分类统计")
report.append("")
report.append("| 测试类别 | 题数 | 平均分 | 最高分 | 最低分 | 平均响应时间 |")
report.append("|----------|------|--------|--------|--------|--------------|")
for cat, stats in category_stats.items():
avg_score = sum(stats['scores']) / len(stats['scores'])
avg_time = sum(stats['times']) / len(stats['times'])
report.append(f"| {cat} | {stats['total']} | {avg_score:.1f} | {max(stats['scores'])} | {min(stats['scores'])} | {avg_time:.2f}s |")
report.append("")
# 详细结果
report.append("## 三、详细测试结果")
report.append("")
for r in results:
report.append(f"### Q{r['id']}: {r['question']}")
report.append("")
report.append(f"- **测试类别**: {r['category']}")
report.append(f"- **预期答案**: {r['expected']}")
report.append(f"- **预期来源**: {r['expected_source']}")
report.append(f"- **实际答案**: {r['actual_answer']}")
# 处理 sources 格式(可能是 dict 列表或 str 列表)
sources = r['actual_sources']
if sources and isinstance(sources[0], dict):
sources_str = ', '.join([s.get('source', str(s)) for s in sources[:5]])
else:
sources_str = ', '.join([str(s) for s in sources[:5]]) if sources else ''
report.append(f"- **检索来源**: {sources_str}")
report.append(f"- **评分**: {r['evaluation']['score']} ({r['evaluation']['grade']})")
report.append(f"- **响应时间**: {r['response_time']}s")
report.append("")
# 问题与建议
report.append("## 四、问题分析")
report.append("")
# 低分问题
low_score_questions = [r for r in results if r['evaluation']['score'] < 40]
if low_score_questions:
report.append("### 低分问题 (< 40分)")
report.append("")
for r in low_score_questions:
report.append(f"- Q{r['id']}: {r['question']} (得分: {r['evaluation']['score']})")
report.append("")
return "\n".join(report)
def main():
"""主函数"""
# Windows 控制台编码处理
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
# 执行测试
results = run_tests()
# 生成报告
report = generate_report(results)
# 保存报告
report_file = "rag_test_report.md"
with open(report_file, "w", encoding="utf-8") as f:
f.write(report)
print("\n" + "=" * 80)
print("测试完成!")
print(f"报告已保存到: {report_file}")
print("=" * 80)
# 保存详细结果 JSON
json_file = "rag_test_results.json"
with open(json_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"详细结果已保存到: {json_file}")
if __name__ == "__main__":
main()