kangda-robot-backend/ruoyi-fastapi-backend/test/test_sse.py
2025-12-24 18:41:42 +08:00

506 lines
18 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.

import argparse # 解析命令行参数
import asyncio # 异步事件循环
import json # 处理 JSON 文本
import os # 读取环境变量
import sys # 系统参数
import time # 时间统计
import uuid # 生成随机 chat/session id
import aiohttp # 异步 HTTP 客户端
import requests # 同步 HTTP 客户端
# 下面的默认参数可以通过环境变量覆盖,避免把机密写死在代码里。
API_URL = os.environ.get(
"RAGFLOW_URL",
"http://10.0.0.202:9099/system/ragflow/converse_with_chat_assistant",
)
AUTH_TOKEN = os.environ.get(
"RAGFLOW_TOKEN",
"Bearer ",
)
DEFAULT_CHAT_ID = os.environ.get(
"RAGFLOW_CHAT_ID", "a455abcadbcb11f0884b0242ac130006"
)
DEFAULT_SESSION_ID = os.environ.get(
"RAGFLOW_SESSION_ID", "3d3ec1e2b9344591a1486fc95c1e01ed"
)
DEFAULT_QUESTION = os.environ.get(
"RAGFLOW_QUESTION", "你好,请用简洁的语言介绍你自己"
)
LOGIN_URL = os.environ.get(
"RAGFLOW_LOGIN_URL",
"http://10.0.0.202:9099/login",
)
# 登录凭据
LOGIN_USERNAME = os.environ.get("RAGFLOW_USERNAME", "admin")
LOGIN_PASSWORD = os.environ.get("RAGFLOW_PASSWORD", "admin123")
class PerformanceTimer:
"""性能计时器类"""
def __init__(self):
self.start_time = None
self.end_time = None
self.elapsed_ms = 0
def start(self):
self.start_time = time.perf_counter()
return self
def restart(self):
"""重新开始计时"""
self.start_time = time.perf_counter()
self.end_time = None
self.elapsed_ms = 0
return self
def stop(self):
if self.start_time is None:
raise ValueError("计时器未启动")
self.end_time = time.perf_counter()
self.elapsed_ms = (self.end_time - self.start_time) * 1000
return self
def get_elapsed_ms(self) -> float:
if self.start_time is not None and self.end_time is not None:
return self.elapsed_ms
elif self.start_time is not None:
# 如果没有stop返回当前经过的时间
return (time.perf_counter() - self.start_time) * 1000
return 0
def get_elapsed_seconds(self) -> float:
return self.get_elapsed_ms() / 1000
def format(self) -> str:
"""格式化输出时间"""
elapsed = self.get_elapsed_ms()
if elapsed < 1000:
return f"{elapsed:.2f}ms"
else:
return f"{self.get_elapsed_seconds():.2f}s ({elapsed:.0f}ms)"
async def login_get_token() -> str:
"""
登录获取token
"""
print("🔐 正在登录获取token...")
payload = {
"username": LOGIN_USERNAME,
"password": LOGIN_PASSWORD
}
timer = PerformanceTimer().start()
async with aiohttp.ClientSession() as session:
async with session.post(
LOGIN_URL, data=payload, timeout=60
) as resp:
timer.stop()
if resp.status != 200:
print(f"❌ 登录失败HTTP状态码: {resp.status}")
error_text = await resp.text()
print(f"错误响应: {error_text}")
raise Exception(f"登录失败: {resp.status}")
response_data = await resp.json()
# 检查响应格式
if response_data.get("code") != 200:
print(f"❌ 登录失败,响应码: {response_data.get('code')}")
print(f"响应信息: {response_data.get('msg', '未知错误')}")
raise Exception(f"登录失败: {response_data.get('msg')}")
token = response_data.get("token")
if not token:
print("❌ 登录成功但未获取到token")
print(f"完整响应: {response_data}")
raise Exception("登录成功但未获取到token")
print(f"✅ 登录成功,耗时: {timer.format()}")
print(f"📝 Token预览: {token[:50]}...")
return token
async def stream_chat_async(
question: str,
chat_id: str,
session_id: str,
request_number: int = 1
) -> dict:
"""
异步版aiohttp适合已有事件循环的应用不阻塞主线程。
返回完整回答和相关时间统计
"""
print(f"\n{'='*60}")
print(f"📤 请求 #{request_number}")
print(f"❓ 问题: {question}")
print(f"🆔 Chat ID: {chat_id}")
print(f"🔗 Session ID: {session_id}")
print(f"{'='*60}")
# 创建计时器
timer = PerformanceTimer().start()
token = await login_get_token()
# 重启计时器,只计算流式请求的时间(不包含登录时间)
timer.restart()
payload = {
"chatId": chat_id,
"question": question,
"stream": True,
"sessionId": session_id,
}
headers = {
"Authorization": f"Bearer {token}",
"Accept": "text/event-stream",
}
answer_parts = []
event_type = "message"
# 性能计时 - timer 已在登录前创建并 restart
first_token_time = None
connection_time = None
async with aiohttp.ClientSession() as session:
async with session.post(
API_URL, json=payload, headers=headers, timeout=120
) as resp:
print(
f"# status={resp.status} content-type={resp.headers.get('content-type')}")
resp.raise_for_status()
# 记录连接建立时间
connection_time = timer.get_elapsed_ms()
print(f"[debug] 连接建立时间: {connection_time:.0f}ms")
event_count = 0
while True:
raw_line = await resp.content.readline()
if not raw_line:
print(f"[debug] 收到 {event_count} 个事件,流结束")
break
line = raw_line.decode(errors="ignore").strip() # 逐行读取 SSE 数据
if not line or line.startswith(":"):
continue
if line.startswith("event:"):
event_type = line.split(":", 1)[1].strip() or "message"
print(f"[debug] 事件类型: {event_type}")
continue
if not line.startswith("data:"):
print(f"[skip] {line}")
continue
data_str = line[len("data:"):].strip()
if not data_str:
continue
event_count += 1
try:
payload_obj = json.loads(data_str)
except json.JSONDecodeError:
print(f"[{event_type}] {data_str}")
event_type = "message"
continue
# 打印所有收到的响应,便于调试
print(f"[debug] 收到事件 #{event_count}: {list(payload_obj.keys())}")
# 记录首Token时间遇到任何包含内容的字段时
if first_token_time is None:
# 检测缓存命中情况
if "from_cache" in payload_obj and payload_obj.get("from_cache") is True:
# 缓存命中首Token就是连接建立时间
first_token_time = connection_time
cache_total_time = payload_obj.get("total_time")
print(f"[debug] 🔥 缓存命中! 连接时间: {connection_time:.0f}ms, 缓存总耗时: {cache_total_time}ms")
elif "answer" in payload_obj:
first_token_time = timer.get_elapsed_ms()
print(f"[debug] 首Token时间(answer): {first_token_time:.0f}ms")
elif "data" in payload_obj and payload_obj.get("data") is not True:
# 缓存命中时可能直接返回完整答案
first_token_time = timer.get_elapsed_ms()
print(f"[debug] 首Token时间(data): {first_token_time:.0f}ms")
elif payload_obj.get("status") == "completed" or event_type == "end":
# 如果一开始就收到completed可能是即时响应
first_token_time = connection_time # 使用连接时间作为近似
print(f"[debug] 首Token时间(即时响应): {first_token_time:.0f}ms")
if event_type == "end" or payload_obj.get("status") == "completed":
print("\n[stream] completed")
break
if payload_obj.get("data") is True:
event_type = "message"
continue
if "answer" in payload_obj:
piece = payload_obj.get("answer", "")
answer_parts.append(piece)
# 流式输出:每个片段都单独打印
print(piece, end="", flush=True)
await asyncio.sleep(0.05) # 使用异步sleep而不是time.sleep
else:
print(f"[{event_type}] {payload_obj}")
event_type = "message"
timer.stop()
full_answer = "".join(answer_parts)
print(f"\n\n{'='*60}")
print(f"✅ 请求 #{request_number} 完成")
print(f"⏱️ 总耗时: {timer.format()}")
if connection_time is not None:
print(f"🔗 连接建立: {connection_time:.0f}ms")
if first_token_time is not None:
print(f"🚀 首Token延迟: {first_token_time:.0f}ms")
if connection_time is not None:
ttft = first_token_time - connection_time # Time To First Token
print(f"⏱️ 首Token耗时(不含连接): {ttft:.0f}ms")
else:
print(f"🚀 首Token延迟: 未记录")
print(f"📝 回答长度: {len(full_answer)} 字符")
print(f"{'='*60}")
# 返回完整统计信息
return {
"answer": full_answer,
"timer": timer,
"first_token_ms": first_token_time,
"connection_ms": connection_time
}
async def run_performance_test(
questions: list[str],
chat_id: str,
session_id: str,
delay_between_requests: float = 2.0
):
"""
运行性能测试,多次请求以测试缓存效果
Args:
questions: 问题列表,每个请求使用对应的问题
chat_id: 聊天会话ID
session_id: 会话ID
delay_between_requests: 请求之间的间隔时间(秒)
"""
num_requests = len(questions)
print("\n" + "🚀"*30)
print("🎯 性能测试开始")
print(f"📊 测试配置:")
print(f" - 请求次数: {num_requests}")
print(f" - 请求间隔: {delay_between_requests}s")
print("📝 测试问题:")
for i, q in enumerate(questions):
print(f" {i+1}. {q}")
print("🚀"*30)
results = []
for i in range(1, num_requests + 1):
question = questions[i-1]
try:
result = await stream_chat_async(
question=question,
chat_id=chat_id,
session_id=session_id,
request_number=i
)
results.append({
"request_number": i,
"question": question,
"elapsed_ms": result["timer"].get_elapsed_ms(),
"elapsed_seconds": result["timer"].get_elapsed_seconds(),
"first_token_ms": result["first_token_ms"],
"connection_ms": result["connection_ms"],
"answer_length": len(result["answer"]),
"from_cache": False # 后续可以根据响应判断
})
# 如果不是最后一次请求,等待一段时间
if i < num_requests:
print(f"\n⏳ 等待 {delay_between_requests}s 后进行下一次请求...")
await asyncio.sleep(delay_between_requests)
except Exception as e:
print(f"\n❌ 请求 #{i} 失败: {str(e)}")
results.append({
"request_number": i,
"question": question,
"elapsed_ms": -1,
"elapsed_seconds": -1,
"first_token_ms": None,
"connection_ms": None,
"answer_length": 0,
"error": str(e)
})
# 打印性能测试汇总
print("\n" + "📊"*30)
print("📈 性能测试结果汇总")
print("📊"*30)
valid_results = [r for r in results if r["elapsed_ms"] > 0]
if not valid_results:
print("❌ 所有请求都失败了")
return
elapsed_times = [r["elapsed_ms"] for r in valid_results]
avg_time = sum(elapsed_times) / len(elapsed_times)
min_time = min(elapsed_times)
max_time = max(elapsed_times)
print(f"\n{'请求#':<8} {'问题':<30} {'总耗时':<12} {'首Token':<10} {'速度评估'}")
print("-" * 80)
for r in results:
if r["elapsed_ms"] < 0:
print(f"#{r['request_number']:<6} {r['question'][:28]:<30} {'失败':<12} {'-':<10}")
else:
# 速度评估
elapsed = r["elapsed_ms"]
if elapsed < 500:
speed = "🚀 极快 (缓存?)"
elif elapsed < 1500:
speed = "⚡ 较快"
elif elapsed < 3000:
speed = "⏱️ 正常"
else:
speed = "🐢 较慢"
first_token = f"{r['first_token_ms']:.0f}ms" if r["first_token_ms"] else "-"
question_short = r['question'][:28] + ".." if len(r['question']) > 28 else r['question']
print(f"#{r['request_number']:<6} {question_short:<30} {r['elapsed_ms']:.0f}ms{'':<5} {first_token:<10} {speed}")
print("-" * 80)
print(f"\n📊 统计信息:")
print(f" 平均耗时: {avg_time:.0f}ms ({avg_time/1000:.2f}s)")
print(f" 最快耗时: {min_time:.0f}ms")
print(f" 最慢耗时: {max_time:.0f}ms")
# 缓存效果分析
if len(valid_results) >= 2:
first_time = valid_results[0]["elapsed_ms"]
last_time = valid_results[-1]["elapsed_ms"]
print(f"\n💡 缓存效果分析:")
if last_time < first_time * 0.7:
print(f" ✅ 缓存可能生效! 后端请求({first_time:.0f}ms)比首次请求快 {((first_time - last_time) / first_time * 100):.1f}%")
elif last_time > first_time * 1.3:
print(f" ⚠️ 缓存可能未生效,后续请求比首次请求慢")
else:
print(f" 🤔 响应时间相近,缓存效果不确定")
# 首Token时间分析
first_tokens = [r["first_token_ms"] for r in valid_results if r["first_token_ms"] is not None]
if len(first_tokens) >= 2:
first_ttft = first_tokens[0]
last_ttft = first_tokens[-1]
print(f"\n🚀 首Token时间分析:")
if last_ttft < first_ttft * 0.5:
print(f" ✅ 后续请求首Token更快可能命中缓存")
elif last_ttft > first_ttft * 1.5:
print(f" ⚠️ 后续请求首Token更慢可能是新问题")
else:
print(f" 🤔 首Token时间相近")
print("📊"*30 + "\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Call ragflow chat endpoint with SSE streaming and performance testing."
)
parser.add_argument(
"question",
nargs="?",
help="要提的问题,未提供则用环境变量 RAGFLOW_QUESTION",
)
parser.add_argument(
"-q",
"--question",
dest="question_opt",
help="与位置参数等价,优先级更高。",
)
parser.add_argument(
"--chat-id",
default=DEFAULT_CHAT_ID,
help="chatId默认取 RAGFLOW_CHAT_ID。",
)
parser.add_argument(
"--session-id",
default=DEFAULT_SESSION_ID or uuid.uuid4().hex,
help="sessionId默认取 RAGFLOW_SESSION_ID否则随机生成。",
)
parser.add_argument(
"--url",
default=API_URL,
help="接口地址,默认取 RAGFLOW_URL。",
)
parser.add_argument(
"-n",
"--num-requests",
type=int,
default=3,
help="性能测试的请求次数默认3次。",
)
parser.add_argument(
"-d",
"--delay",
type=float,
default=2.0,
help="请求之间的间隔时间(秒)默认2秒。",
)
parser.add_argument(
"--single",
action="store_true",
help="单次请求模式(不进行性能测试)。",
)
args = parser.parse_args()
# Allow either positional or -q/--question to override env default.
question = args.question_opt or args.question or DEFAULT_QUESTION
# Override globals if provided via CLI.
API_URL = args.url # type: ignore
if args.single:
# 单次请求模式
asyncio.run(
stream_chat_async(
question=question,
chat_id=args.chat_id,
session_id=args.session_id,
)
)
else:
# 性能测试模式
if question and question != DEFAULT_QUESTION:
questions = [question, question, question]
print(f"📝 使用命令行参数的问题进行性能测试: {question}")
else:
questions = [
"公司有什么产品?", # 问题1
"公司有什么产品?", # 问题2完全相同测试精确缓存
"公司有哪些产品?", # 问题3表述略不同测试语义缓存
]
asyncio.run(
run_performance_test(
questions=questions,
chat_id=args.chat_id,
session_id=args.session_id,
delay_between_requests=args.delay
)
)