fix: 语义缓存误命中修复 — 二次 Jaccard 验证 + query-only embedding

问题: 同会话中语义相近但不同的问题(如"总体要求"vs"工作流程和规范要求")
会错误命中缓存,返回上一个问题的答案。

修复:
- intent_analyzer: 缓存 embedding 改用 query-only(不含历史),避免上下文污染
- intent_analyzer + chat_routes: 缓存读取增加字符级 Jaccard 二次验证(阈值0.5)
- 缓存写入存储 _raw_query 供二次验证比对
- 兼容旧缓存条目(无 _raw_query 时放行)
This commit is contained in:
lacerate551
2026-06-21 15:37:37 +08:00
parent 258be54df7
commit 21470ed28c
2 changed files with 89 additions and 35 deletions

View File

@@ -2298,32 +2298,45 @@ def rag():
cached = _sc.get(_semantic_cache_emb)
# 防御性校验:必须是 RAG 回答缓存(非 intent_analyzer 缓存),且回答非空
if cached is not None and cached.get("cache_type") == "rag_answer" and cached.get("answer"):
logger.info(f"[语义缓存] 命中: {message[:50]}...")
cached_answer = cached.get("answer", "")
# 流式返回缓存的答案
yield f"data: {json.dumps({'type': 'start', 'message': '正在检索知识库...'}, ensure_ascii=False)}\n\n"
# 分块发送缓存答案
chunk_size = 20
for i in range(0, len(cached_answer), chunk_size):
chunk = cached_answer[i:i+chunk_size]
full_answer.append(chunk)
yield f"data: {json.dumps({'type': 'chunk', 'content': chunk}, ensure_ascii=False)}\n\n"
finish_event = {
"type": "finish",
"answer": cached_answer,
"mode": "rag",
"session_id": session_id,
"sources": cached.get("sources", []),
"citations": cached.get("citations", []),
"images": cached.get("images", []),
"tables": cached.get("tables", []),
"sections": [],
"duration_ms": int((_time.time() - start_time) * 1000),
"confidence_score": 1.0,
"semantic_cache_hit": True
}
yield f"data: {json.dumps(finish_event, ensure_ascii=False)}\n\n"
return
# 二次验证:检查原始查询文本相似度,防止语义相近但不同问题误命中
_cached_query = cached.get("_raw_query", "")
if _cached_query:
_qa, _qb = set(retrieval_query), set(_cached_query)
_jaccard = len(_qa & _qb) / len(_qa | _qb) if (_qa and _qb) else 0
if _jaccard < 0.5:
logger.info(
f"[语义缓存] 二次验证拒绝 (Jaccard={_jaccard:.2f}): "
f"query='{retrieval_query[:30]}' vs cached='{_cached_query[:30]}'"
)
cached = None # 拒绝命中,继续正常流程
# 通过验证(或旧缓存无 _raw_query 字段兼容放行)
if cached is not None and cached.get("cache_type") == "rag_answer" and cached.get("answer"):
logger.info(f"[语义缓存] 命中: {message[:50]}...")
cached_answer = cached.get("answer", "")
# 流式返回缓存的答案
yield f"data: {json.dumps({'type': 'start', 'message': '正在检索知识库...'}, ensure_ascii=False)}\n\n"
# 分块发送缓存答案
chunk_size = 20
for i in range(0, len(cached_answer), chunk_size):
chunk = cached_answer[i:i+chunk_size]
full_answer.append(chunk)
yield f"data: {json.dumps({'type': 'chunk', 'content': chunk}, ensure_ascii=False)}\n\n"
finish_event = {
"type": "finish",
"answer": cached_answer,
"mode": "rag",
"session_id": session_id,
"sources": cached.get("sources", []),
"citations": cached.get("citations", []),
"images": cached.get("images", []),
"tables": cached.get("tables", []),
"sections": [],
"duration_ms": int((_time.time() - start_time) * 1000),
"confidence_score": 1.0,
"semantic_cache_hit": True
}
yield f"data: {json.dumps(finish_event, ensure_ascii=False)}\n\n"
return
except Exception as e:
logger.warning(f"[语义缓存] 检查失败: {e}")
_semantic_cache_emb = None
@@ -2886,6 +2899,7 @@ def rag():
"citations": citation_result.get("citations", []),
"images": rich_media.get("images", []),
"tables": rich_media.get("tables", []),
"_raw_query": retrieval_query,
})
logger.debug(f"[语义缓存] 写入成功: {message[:50]}...")
except Exception as e: