Files
BossAi/test_performance.py
T
selfreleaseandDevin 5c21e8fcd0 feat: BossAi 老板AI助手初始版本
包含功能:
- 法务参谋/经营决策/生活助理/情绪中心四大AI对话模块
- 专家咨询系统(用户选定→专家确认→咨询→回复→评价)
- 角色登录(用户/专家/管理员)+ ProtectedRoute 权限控制
- 管理后台(用户管理/专家管理/AI使用统计/专家活跃度)
- 专家工作台(待确认服务/待回复咨询/已回复咨询/服务中用户)
- 服务关系取消(用户和专家均可取消,需自己确认)
- 全页面移动端响应式适配
- 知识库RAG检索
- 对话历史(普通/保密模式)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-29 23:57:26 +08:00

240 lines
7.9 KiB
Python

"""性能测试:并发会话、长文档解析、OCR 耗时、API 响应时间基线。
用法: python3 test_performance.py
输出: perf_report.json + 控制台统计
"""
import json
import time
import os
import sys
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fastapi.testclient import TestClient
import api as api_module
from api import app
client = TestClient(app)
def perf_single_session_ask():
"""单会话 ask 响应时间(不含 Kimi,只测分诊+RAG)。"""
# 清理限流桶
api_module._rate_buckets.clear()
sid = client.post('/sessions', json={'mode': 'normal'}).json()['session_id']
times = []
questions = [
'员工工伤后能否开除',
'合同违约金怎么约定',
'客户欠款三个月不还',
'收到税务通知要求补缴',
'最近压力大想聊聊',
]
for q in questions:
start = time.time()
r = client.post(f'/sessions/{sid}/ask', json={'question': q, 'scene': 'legal'})
elapsed = time.time() - start
times.append(elapsed)
assert r.status_code == 200, f"ask 失败: {r.status_code}"
return {
"test": "single_session_ask",
"iterations": len(times),
"avg_ms": round(sum(times) / len(times) * 1000, 1),
"min_ms": round(min(times) * 1000, 1),
"max_ms": round(max(times) * 1000, 1),
}
def perf_concurrent_sessions():
"""并发会话创建 + 提问(10 并发)。"""
api_module._rate_buckets.clear()
def worker(i):
start = time.time()
sid = client.post('/sessions', json={'mode': 'normal'}).json()['session_id']
r = client.post(f'/sessions/{sid}/ask', json={
'question': f'测试问题 {i}', 'scene': 'legal'})
elapsed = time.time() - start
return elapsed, r.status_code
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(worker, i) for i in range(10)]
results = [f.result() for f in as_completed(futures)]
times = [r[0] for r in results]
status_codes = [r[1] for r in results]
return {
"test": "concurrent_sessions_10",
"concurrency": 10,
"avg_ms": round(sum(times) / len(times) * 1000, 1),
"min_ms": round(min(times) * 1000, 1),
"max_ms": round(max(times) * 1000, 1),
"all_200": all(c == 200 for c in status_codes),
"status_codes": status_codes,
}
def perf_long_document_parse():
"""长文档解析:100KB / 1MB TXT 提取时间。"""
api_module._rate_buckets.clear()
sid = client.post('/sessions', json={'mode': 'normal'}).json()['session_id']
results = []
for size_kb in [10, 100, 500]:
content = '合同条款测试内容。' * (size_kb * 1024 // 10)
data = content.encode('utf-8')
start = time.time()
r = client.post(f'/sessions/{sid}/file',
files={'file': (f'test_{size_kb}kb.txt', data)})
elapsed = time.time() - start
assert r.status_code == 200, f"文件上传失败: {r.status_code}"
results.append({
"size_kb": size_kb,
"parse_ms": round(elapsed * 1000, 1),
"text_length": r.json().get('text_length', 0),
})
return {"test": "long_document_parse", "results": results}
def perf_ocr_image():
"""OCR 图片识别耗时。"""
api_module._rate_buckets.clear()
try:
from PIL import Image, ImageDraw, ImageFont
import io
import pytesseract
except ImportError:
return {"test": "ocr_image", "skipped": "PIL/pytesseract not installed"}
sid = client.post('/sessions', json={'mode': 'normal'}).json()['session_id']
# 生成不同复杂度的图片
results = []
for num_lines in [1, 5, 20]:
img = Image.new('RGB', (800, 60 * num_lines + 40), 'white')
d = ImageDraw.Draw(img)
try:
font = ImageFont.truetype('/System/Library/Fonts/STHeiti Medium.ttc', 36)
except Exception:
font = ImageFont.load_default()
for i in range(num_lines):
d.text((20, 20 + i * 60), f'合同第{i+1}条违约金条款', fill='black', font=font)
buf = io.BytesIO()
img.save(buf, format='PNG')
data = buf.getvalue()
start = time.time()
r = client.post(f'/sessions/{sid}/file',
files={'file': (f'test_{num_lines}lines.png', data)})
elapsed = time.time() - start
if r.status_code == 200:
results.append({
"lines": num_lines,
"ocr_ms": round(elapsed * 1000, 1),
"text_length": r.json().get('text_length', 0),
})
else:
results.append({"lines": num_lines, "error": r.status_code})
return {"test": "ocr_image", "results": results}
def perf_rag_search():
"""RAG 检索耗时:关键词 + 向量。"""
api_module._rate_buckets.clear()
queries = ['工伤解除', '合同违约金', '税务稽查', '欠款催收', '劳动仲裁']
keyword_times = []
vector_times = []
for q in queries:
start = time.time()
r = client.get(f'/knowledge/search?q={q}')
keyword_times.append(time.time() - start)
assert r.status_code == 200
start = time.time()
r = client.get(f'/knowledge/vector-search?q={q}')
vector_times.append(time.time() - start)
assert r.status_code == 200
return {
"test": "rag_search",
"keyword_avg_ms": round(sum(keyword_times) / len(keyword_times) * 1000, 1),
"vector_avg_ms": round(sum(vector_times) / len(vector_times) * 1000, 1),
"queries": len(queries),
}
def perf_session_destroy():
"""会话销毁耗时。"""
times = []
for _ in range(20):
api_module._rate_buckets.clear()
sid = client.post('/sessions', json={'mode': 'ephemeral'}).json()['session_id']
start = time.time()
client.post(f'/sessions/{sid}/destroy')
times.append(time.time() - start)
return {
"test": "session_destroy",
"iterations": len(times),
"avg_ms": round(sum(times) / len(times) * 1000, 1),
"min_ms": round(min(times) * 1000, 1),
"max_ms": round(max(times) * 1000, 1),
}
def run_all():
"""运行全部性能测试。"""
print("=" * 60)
print("BossAi 性能测试")
print("=" * 60)
tests = [
("单会话 ask 响应时间", perf_single_session_ask),
("并发 10 会话", perf_concurrent_sessions),
("长文档解析", perf_long_document_parse),
("OCR 图片识别", perf_ocr_image),
("RAG 检索", perf_rag_search),
("会话销毁", perf_session_destroy),
]
all_results = []
for name, test_func in tests:
print(f"\n--- {name} ---")
try:
result = test_func()
all_results.append(result)
# 简洁输出
for k, v in result.items():
if k != "test" and not isinstance(v, list):
print(f" {k}: {v}")
elif isinstance(v, list) and k == "results":
for item in v:
print(f" {item}")
except Exception as e:
print(f" ERROR: {e}")
all_results.append({"test": name, "error": str(e)})
# 写入报告
with open("perf_report.json", "w", encoding="utf-8") as f:
json.dump(all_results, f, ensure_ascii=False, indent=2)
print(f"\n{'=' * 60}")
print(f"性能报告已写入 perf_report.json")
print(f"{'=' * 60}")
# 性能基线总结
print("\n性能基线总结:")
for r in all_results:
if "error" in r:
print(f" {r['test']}: ERROR")
elif "avg_ms" in r:
print(f" {r['test']}: avg {r['avg_ms']}ms")
elif "keyword_avg_ms" in r:
print(f" {r['test']}: keyword {r['keyword_avg_ms']}ms, vector {r['vector_avg_ms']}ms")
if __name__ == "__main__":
run_all()