增加语义缓存匹配测试

This commit is contained in:
Tian jianyong 2025-12-24 14:21:55 +08:00
parent 2c0ea3467b
commit 31c183c906
4 changed files with 206 additions and 41 deletions

View File

@ -3,6 +3,7 @@ asyncmy==0.2.10
DateTime==5.5
fastapi[all]==0.115.8
httpx==0.27.2
jieba==0.42.1
loguru==0.7.3
openpyxl==3.1.5
pandas==2.2.3

View File

@ -0,0 +1,43 @@
"""
测试语义缓存的相似度计算
"""
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.semantic_cache_service import SemanticCacheService
def test_similarity():
"""测试相似度计算"""
service = SemanticCacheService()
# 测试问题对
test_cases = [
("公司有什么产品?", "公司有什么产品?"),
("公司有什么产品?", "公司有哪些产品?"),
("公司有什么产品?", "你们公司有哪些产品?"),
("公司有什么产品?", "今天天气怎么样?"),
]
print("🔍 相似度计算测试")
print("=" * 60)
for q1, q2 in test_cases:
similarity = service._calculate_text_similarity(q1, q2)
keywords1 = service._extract_keywords(q1)
keywords2 = service._extract_keywords(q2)
threshold = service.SIMILARITY_THRESHOLD
status = "✅ 命中" if similarity >= threshold else "❌ 未命中"
print(f"\n问题1: {q1}")
print(f"问题2: {q2}")
print(f"关键词1: {keywords1}")
print(f"关键词2: {keywords2}")
print(f"相似度: {similarity:.3f} (阈值: {threshold})")
print(f"状态: {status}")
if __name__ == "__main__":
test_similarity()

View File

@ -49,6 +49,13 @@ class PerformanceTimer:
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("计时器未启动")
@ -57,17 +64,23 @@ class PerformanceTimer:
return self
def get_elapsed_ms(self) -> float:
return self.elapsed_ms
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.elapsed_ms / 1000
return self.get_elapsed_ms() / 1000
def format(self) -> str:
"""格式化输出时间"""
if self.elapsed_ms < 1000:
return f"{self.elapsed_ms:.2f}ms"
elapsed = self.get_elapsed_ms()
if elapsed < 1000:
return f"{elapsed:.2f}ms"
else:
return f"{self.get_elapsed_seconds():.2f}s ({self.elapsed_ms:.0f}ms)"
return f"{self.get_elapsed_seconds():.2f}s ({elapsed:.0f}ms)"
async def login_get_token() -> str:
@ -121,10 +134,10 @@ async def stream_chat_async(
chat_id: str,
session_id: str,
request_number: int = 1
) -> tuple[str, PerformanceTimer]:
) -> dict:
"""
异步版aiohttp适合已有事件循环的应用不阻塞主线程
返回完整回答和耗时
返回完整回答和相关时间统计
"""
print(f"\n{'='*60}")
print(f"📤 请求 #{request_number}")
@ -133,8 +146,14 @@ async def stream_chat_async(
print(f"🔗 Session ID: {session_id}")
print(f"{'='*60}")
# 创建计时器
timer = PerformanceTimer().start()
token = await login_get_token()
# 重启计时器,只计算流式请求的时间(不包含登录时间)
timer.restart()
payload = {
"chat_id": chat_id,
"question": question,
@ -149,9 +168,9 @@ async def stream_chat_async(
answer_parts = []
event_type = "message"
# 性能计时
timer = PerformanceTimer().start()
# 性能计时 - timer 已在登录前创建并 restart
first_token_time = None
connection_time = None
async with aiohttp.ClientSession() as session:
async with session.post(
@ -160,16 +179,23 @@ async def stream_chat_async(
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}")
@ -177,16 +203,36 @@ async def stream_chat_async(
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 and "answer" in payload_obj:
first_token_time = timer.get_elapsed_ms()
# 记录首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")
@ -199,47 +245,71 @@ async def stream_chat_async(
answer_parts.append(piece)
# 流式输出:每个片段都单独打印
print(piece, end="", flush=True)
time.sleep(0.05) # 每个片段间隔50ms让流式效果更明显
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 first_token_time:
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")
print(f"📝 回答长度: {len(''.join(answer_parts))} 字符")
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}")
full_answer = "".join(answer_parts)
return full_answer, timer
# 返回完整统计信息
return {
"answer": full_answer,
"timer": timer,
"first_token_ms": first_token_time,
"connection_ms": connection_time
}
async def run_performance_test(
question: str,
questions: list[str],
chat_id: str,
session_id: str,
num_requests: int = 3,
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(f" - 问题: {question}")
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:
answer, timer = await stream_chat_async(
result = await stream_chat_async(
question=question,
chat_id=chat_id,
session_id=session_id,
@ -247,9 +317,13 @@ async def run_performance_test(
)
results.append({
"request_number": i,
"elapsed_ms": timer.get_elapsed_ms(),
"elapsed_seconds": timer.get_elapsed_seconds(),
"answer_length": len(answer)
"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 # 后续可以根据响应判断
})
# 如果不是最后一次请求,等待一段时间
@ -261,8 +335,11 @@ async def run_performance_test(
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)
})
@ -283,12 +360,12 @@ async def run_performance_test(
min_time = min(elapsed_times)
max_time = max(elapsed_times)
print(f"\n{'请求#':<8} {'耗时':<15} {'速度评估'}")
print("-" * 50)
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} {'失败':<15}")
print(f"#{r['request_number']:<6} {r['question'][:28]:<30} {'失败':<12} {'-':<10}")
else:
# 速度评估
elapsed = r["elapsed_ms"]
@ -301,9 +378,11 @@ async def run_performance_test(
else:
speed = "🐢 较慢"
print(f"#{r['request_number']:<6} {r['elapsed_ms']:.0f}ms".ljust(15) + 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("-" * 50)
print("-" * 80)
print(f"\n📊 统计信息:")
print(f" 平均耗时: {avg_time:.0f}ms ({avg_time/1000:.2f}s)")
print(f" 最快耗时: {min_time:.0f}ms")
@ -322,6 +401,19 @@ async def run_performance_test(
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")
@ -393,12 +485,20 @@ if __name__ == "__main__":
)
else:
# 性能测试模式
# 定义三个测试问题
# 问题1和2完全相同用于测试精确缓存
# 问题3与问题1意思相同但表述略不同用于测试语义缓存
questions = [
"公司有什么产品?", # 问题1
"公司有什么产品?", # 问题2完全相同测试精确缓存
"公司有哪些产品?", # 问题3表述略不同测试语义缓存
]
asyncio.run(
run_performance_test(
question=question,
questions=questions,
chat_id=args.chat_id,
session_id=args.session_id,
num_requests=args.num_requests,
delay_between_requests=args.delay
)
)

View File

@ -25,6 +25,14 @@ import logging
from typing import Optional, Tuple, Dict, Any, List
from dataclasses import dataclass, asdict
# 尝试导入jieba分词库
try:
import jieba
JIEBA_AVAILABLE = True
except ImportError:
JIEBA_AVAILABLE = False
logger.warning("jieba未安装语义匹配将使用简单的关键词提取")
# 配置日志
logger = logging.getLogger(__name__)
@ -107,18 +115,29 @@ class SemanticCacheService:
提取问题关键词用于语义匹配
策略
1. 保留完整问题作为主要匹配依据
2. 提取名词动词等核心词汇
3. 过滤停用词
1. 使用jieba分词如果可用提取中文词语
2. 保留完整问题作为主要匹配依据
3. 提取名词动词等核心词汇
4. 过滤停用词
"""
# 简单停用词列表
stopwords = {'', '', '', '', '', '', '', '', '', '', '', '', '', '请问', '能不能', '可以', '怎么', '如何', '什么', '多少', '几个'}
stopwords = {'', '', '', '', '', '', '', '', '', '', '', '', '', '请问', '能不能', '可以', '怎么', '如何', '什么', '多少', '几个', '哪些', '那个', '这个'}
# 分词(简单按字符或词语切分)
words = re.findall(r'[\w\u4e00-\u9fff]+', question.lower())
# 过滤停用词和过短的词
keywords = [w for w in words if w not in stopwords and len(w) > 1]
if JIEBA_AVAILABLE:
# 使用jieba分词
words = list(jieba.cut(question))
# 过滤停用词、过短的词和标点
keywords = [w.strip() for w in words
if w.strip() and w.strip() not in stopwords
and len(w.strip()) > 1
and not re.match(r'^[\s\d\W]+$', w)]
else:
# 分词(简单按字符或词语切分)
words = re.findall(r'[\w\u4e00-\u9fff]+', question.lower())
# 过滤停用词和过短的词
keywords = [w for w in words if w not in stopwords and len(w) > 1]
return keywords
@ -203,6 +222,7 @@ class SemanticCacheService:
# 遍历缓存,计算相似度
best_match = None
best_match_key = None # 记录最佳匹配的key
best_similarity = 0.0
for key in cache_keys:
@ -221,6 +241,7 @@ class SemanticCacheService:
if similarity > best_similarity:
best_similarity = similarity
best_match = entry
best_match_key = key # 保存正确的key
except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"[SemanticCache] 解析缓存失败: {key}, {e}")
@ -231,7 +252,7 @@ class SemanticCacheService:
logger.info(f"[SemanticCache] 语义命中 (相似度={best_similarity:.2f}): {question[:30]}...")
# 更新命中次数
best_match.hit_count += 1
await redis.set(key, json.dumps(best_match.to_dict()), ex=self.CACHE_TTL_HOURS * 3600)
await redis.set(best_match_key, json.dumps(best_match.to_dict()), ex=self.CACHE_TTL_HOURS * 3600)
return best_match.answer, best_similarity
return None