feat: 性能插桩、缓存清除端点、LLM top_p 与 JSON 强制输出
- chat_routes.py: 新增 8 阶段性能计时(stages_ms)+ SSE 输出 - sync_routes.py: 新增 POST /cache/clear 缓存清除端点 - engine.py: LLM 调用增加 top_p 参数 - intent_analyzer.py: response_format 加配置开关(INTENT_RESPONSE_FORMAT)
This commit is contained in:
@@ -2196,6 +2196,9 @@ def rag():
|
|||||||
import re
|
import re
|
||||||
start_time = _time.time()
|
start_time = _time.time()
|
||||||
full_answer = []
|
full_answer = []
|
||||||
|
# === 性能插桩:各阶段耗时 ===
|
||||||
|
_t = {'intent': 0, 'cache': 0, 'search': 0, 'vlm': 0, 'image': 0, 'rescue': 0, 'llm': 0, 'post': 0}
|
||||||
|
_t_prev = start_time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 0. 意图分析(改写 + 双层判断)
|
# 0. 意图分析(改写 + 双层判断)
|
||||||
@@ -2227,7 +2230,6 @@ def rag():
|
|||||||
# 构建上下文
|
# 构建上下文
|
||||||
context_text = ""
|
context_text = ""
|
||||||
if history:
|
if history:
|
||||||
# 提取最近的助手回答
|
|
||||||
for msg in reversed(history):
|
for msg in reversed(history):
|
||||||
if msg.get("role") == "assistant":
|
if msg.get("role") == "assistant":
|
||||||
context_text = msg.get("content", "")
|
context_text = msg.get("content", "")
|
||||||
@@ -2260,7 +2262,6 @@ def rag():
|
|||||||
|
|
||||||
请直接回答用户问题。"""
|
请直接回答用户问题。"""
|
||||||
|
|
||||||
# 流式生成回答
|
|
||||||
for content in call_llm_stream(
|
for content in call_llm_stream(
|
||||||
client,
|
client,
|
||||||
prompt=user_prompt,
|
prompt=user_prompt,
|
||||||
@@ -2274,14 +2275,15 @@ def rag():
|
|||||||
full_answer.append(content)
|
full_answer.append(content)
|
||||||
yield f"data: {json.dumps({'type': 'chunk', 'content': content}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'type': 'chunk', 'content': content}, ensure_ascii=False)}\n\n"
|
||||||
|
|
||||||
# 发送完成事件
|
|
||||||
yield f"data: {json.dumps({'type': 'finish', 'answer': ''.join(full_answer), 'sources': []}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'type': 'finish', 'answer': ''.join(full_answer), 'sources': []}, ensure_ascii=False)}\n\n"
|
||||||
return # 直接返回,不执行后续检索
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"意图分析失败: {e},继续执行检索流程")
|
logger.warning(f"意图分析失败: {e},继续执行检索流程")
|
||||||
|
|
||||||
retrieval_query = intent.rewritten_query if (intent and intent.rewritten_query) else message
|
retrieval_query = intent.rewritten_query if (intent and intent.rewritten_query) else message
|
||||||
|
_t['intent'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
|
|
||||||
# 1.5 语义缓存检查(跳过检索+生成全流程)
|
# 1.5 语义缓存检查(跳过检索+生成全流程)
|
||||||
_semantic_cache_emb = None
|
_semantic_cache_emb = None
|
||||||
@@ -2343,6 +2345,8 @@ def rag():
|
|||||||
|
|
||||||
# 1. 发送开始事件
|
# 1. 发送开始事件
|
||||||
yield f"data: {json.dumps({'type': 'start', 'message': '正在检索知识库...'}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'type': 'start', 'message': '正在检索知识库...'}, ensure_ascii=False)}\n\n"
|
||||||
|
_t['cache'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
|
|
||||||
# 2. 执行混合检索(扩大召回数量,确保图片切片有机会被召回)
|
# 2. 执行混合检索(扩大召回数量,确保图片切片有机会被召回)
|
||||||
# 如果意图分析生成了子查询(对比类),传给搜索引擎并行检索
|
# 如果意图分析生成了子查询(对比类),传给搜索引擎并行检索
|
||||||
@@ -2619,6 +2623,8 @@ def rag():
|
|||||||
# 2.5. 懒加载增强(Phase 4)
|
# 2.5. 懒加载增强(Phase 4)
|
||||||
# 暂时禁用:VLM 调用耗时过长,可能导致请求超时
|
# 暂时禁用:VLM 调用耗时过长,可能导致请求超时
|
||||||
# TODO: 后续可改为异步后台任务
|
# TODO: 后续可改为异步后台任务
|
||||||
|
_t['search'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
try:
|
try:
|
||||||
import asyncio
|
import asyncio
|
||||||
from knowledge.lazy_enhance import enhance_retrieved_chunks
|
from knowledge.lazy_enhance import enhance_retrieved_chunks
|
||||||
@@ -2626,9 +2632,13 @@ def rag():
|
|||||||
asyncio.run(enhance_retrieved_chunks(contexts, message, kb_name))
|
asyncio.run(enhance_retrieved_chunks(contexts, message, kb_name))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"懒加载增强失败: {e}")
|
logger.warning(f"懒加载增强失败: {e}")
|
||||||
|
_t['vlm'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
|
|
||||||
# 3. 选择要展示的图片(Phase 5)
|
# 3. 选择要展示的图片(Phase 5)
|
||||||
selected_images = select_images(contexts, message)
|
selected_images = select_images(contexts, message)
|
||||||
|
_t['image'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
|
|
||||||
# 调试事件:图片选择详情
|
# 调试事件:图片选择详情
|
||||||
if IS_DEV:
|
if IS_DEV:
|
||||||
@@ -2754,12 +2764,16 @@ def rag():
|
|||||||
yield f"data: {json.dumps({'type': 'context_built', 'data': {'chunk_count': len(text_used), 'context_length': len(enhanced_context), 'budget_max_chars': CONTEXT_MAX_CHARS, 'min_score_filter': RERANK_CONTEXT_MIN_SCORE, 'confidence_top3': _confidence_score, 'score_stats': {'max': max(_scores) if _scores else 0, 'min': min(_scores) if _scores else 0, 'avg': round(sum(_scores)/len(_scores), 4) if _scores else 0}, 'context_preview': enhanced_context[:500], 'chunks_used': [{'source': ctx.get('meta',{}).get('source',''), 'page': ctx.get('meta',{}).get('page',0), 'score': ctx.get('score',0), 'preview': (ctx.get('doc','') or '')[:100]} for ctx in text_used]}}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'type': 'context_built', 'data': {'chunk_count': len(text_used), 'context_length': len(enhanced_context), 'budget_max_chars': CONTEXT_MAX_CHARS, 'min_score_filter': RERANK_CONTEXT_MIN_SCORE, 'confidence_top3': _confidence_score, 'score_stats': {'max': max(_scores) if _scores else 0, 'min': min(_scores) if _scores else 0, 'avg': round(sum(_scores)/len(_scores), 4) if _scores else 0}, 'context_preview': enhanced_context[:500], 'chunks_used': [{'source': ctx.get('meta',{}).get('source',''), 'page': ctx.get('meta',{}).get('page',0), 'score': ctx.get('score',0), 'preview': (ctx.get('doc','') or '')[:100]} for ctx in text_used]}}, ensure_ascii=False)}\n\n"
|
||||||
|
|
||||||
# 5. 流式生成回答
|
# 5. 流式生成回答
|
||||||
|
_t['rescue'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
from core.engine import get_engine
|
from core.engine import get_engine
|
||||||
engine = get_engine()
|
engine = get_engine()
|
||||||
|
|
||||||
for token in engine.generate_answer_stream(message, enhanced_context, history):
|
for token in engine.generate_answer_stream(message, enhanced_context, history):
|
||||||
full_answer.append(token)
|
full_answer.append(token)
|
||||||
yield f"data: {json.dumps({'type': 'chunk', 'content': token}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'type': 'chunk', 'content': token}, ensure_ascii=False)}\n\n"
|
||||||
|
_t['llm'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
|
_t_prev = _time.time()
|
||||||
|
|
||||||
# 6. P0:答案对齐过滤器
|
# 6. P0:答案对齐过滤器
|
||||||
# 从 LLM 回答中提取图号引用,过滤图片选择结果
|
# 从 LLM 回答中提取图号引用,过滤图片选择结果
|
||||||
@@ -2873,12 +2887,16 @@ def rag():
|
|||||||
if step.get('name') == 'rerank' and step.get('applied'):
|
if step.get('name') == 'rerank' and step.get('applied'):
|
||||||
rerank_time = step.get('time_ms', 0)
|
rerank_time = step.get('time_ms', 0)
|
||||||
rerank_cached = step.get('cached', False)
|
rerank_cached = step.get('cached', False)
|
||||||
|
_t['post'] = round((_time.time() - _t_prev) * 1000, 1)
|
||||||
finish_event["timing"] = {
|
finish_event["timing"] = {
|
||||||
"total_search_ms": timing_info.get('total_ms', 0),
|
"total_search_ms": timing_info.get('total_ms', 0),
|
||||||
"rerank_ms": rerank_time,
|
"rerank_ms": rerank_time,
|
||||||
"rerank_cached": rerank_cached,
|
"rerank_cached": rerank_cached,
|
||||||
"total_ms": duration_ms
|
"total_ms": duration_ms,
|
||||||
|
"stages_ms": _t
|
||||||
}
|
}
|
||||||
|
# 记录到日志
|
||||||
|
logger.info(f"[性能] 总{duration_ms}ms | 意图{_t['intent']:.0f} 缓存{_t['cache']:.0f} 检索{_t['search']:.0f} VLM{_t['vlm']:.0f} 图片{_t['image']:.0f} 救援{_t['rescue']:.0f} LLM{_t['llm']:.0f} 后处理{_t['post']:.0f}")
|
||||||
# 10.5 写入语义缓存
|
# 10.5 写入语义缓存
|
||||||
try:
|
try:
|
||||||
from core.semantic_cache import get_semantic_cache
|
from core.semantic_cache import get_semantic_cache
|
||||||
|
|||||||
@@ -315,3 +315,48 @@ def stop_sync_monitor() -> Tuple[Any, int]:
|
|||||||
message="操作失败",
|
message="操作失败",
|
||||||
http_status=500
|
http_status=500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 缓存管理 API ====================
|
||||||
|
|
||||||
|
@sync_bp.route('/cache/clear', methods=['POST'])
|
||||||
|
@require_gateway_auth
|
||||||
|
def clear_all_caches() -> Tuple[Any, int]:
|
||||||
|
"""
|
||||||
|
清除所有缓存层(Query / Embedding / Rerank / Semantic / Intent 精确缓存)
|
||||||
|
|
||||||
|
用于测试和调试,不影响向量库数据。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{"success": true, "message": "缓存已清除", "cleared": [...]}
|
||||||
|
"""
|
||||||
|
cleared = []
|
||||||
|
|
||||||
|
# 1. 精确匹配缓存(Query / Embedding / Rerank)
|
||||||
|
try:
|
||||||
|
from core.cache import get_cache_manager
|
||||||
|
cache = get_cache_manager()
|
||||||
|
if cache:
|
||||||
|
cache.clear_all()
|
||||||
|
cleared.append("query_cache")
|
||||||
|
cleared.append("embedding_cache")
|
||||||
|
cleared.append("rerank_cache")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"清除精确缓存失败: {e}")
|
||||||
|
|
||||||
|
# 2. 语义缓存(FAISS)
|
||||||
|
try:
|
||||||
|
from core.semantic_cache import get_semantic_cache
|
||||||
|
sc = get_semantic_cache()
|
||||||
|
if sc:
|
||||||
|
sc.clear()
|
||||||
|
cleared.append("semantic_cache")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"清除语义缓存失败: {e}")
|
||||||
|
|
||||||
|
logger.info(f"缓存清除完成: {cleared}")
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": f"已清除 {len(cleared)} 层缓存",
|
||||||
|
"cleared": cleared
|
||||||
|
})
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ try:
|
|||||||
CLUSTER_SEED_FLOOR, CLUSTER_MAX_BOOST_PER_SECTION, CLUSTER_MAX_SECTIONS,
|
CLUSTER_SEED_FLOOR, CLUSTER_MAX_BOOST_PER_SECTION, CLUSTER_MAX_SECTIONS,
|
||||||
CLUSTER_SECTION_PREFIX_LEVELS,
|
CLUSTER_SECTION_PREFIX_LEVELS,
|
||||||
# 上下文与生成
|
# 上下文与生成
|
||||||
LLM_TEMPERATURE, LLM_MAX_TOKENS, RECALL_MULTIPLIER,
|
LLM_TEMPERATURE, LLM_TOP_P, LLM_MAX_TOKENS, RECALL_MULTIPLIER,
|
||||||
# FAQ 与黑名单
|
# FAQ 与黑名单
|
||||||
FAQ_RECALL_TOP_K, FAQ_BOOST_AMOUNT, FAQ_DECAY_MONTHS, FAQ_DECAY_RATE, FAQ_DECAY_MAX,
|
FAQ_RECALL_TOP_K, FAQ_BOOST_AMOUNT, FAQ_DECAY_MONTHS, FAQ_DECAY_RATE, FAQ_DECAY_MAX,
|
||||||
BLACKLIST_MIN_DISLIKES, BLACKLIST_CACHE_TTL,
|
BLACKLIST_MIN_DISLIKES, BLACKLIST_CACHE_TTL,
|
||||||
@@ -2361,6 +2361,7 @@ class RAGEngine:
|
|||||||
"",
|
"",
|
||||||
MODEL,
|
MODEL,
|
||||||
temperature=LLM_TEMPERATURE,
|
temperature=LLM_TEMPERATURE,
|
||||||
|
top_p=LLM_TOP_P,
|
||||||
max_tokens=LLM_MAX_TOKENS,
|
max_tokens=LLM_MAX_TOKENS,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
error_prefix="[错误]"
|
error_prefix="[错误]"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import threading
|
|||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import List, Dict, Optional, Tuple
|
from typing import List, Dict, Optional, Tuple
|
||||||
|
|
||||||
from config import INTENT_TEMPERATURE, INTENT_MAX_TOKENS, INTENT_HISTORY_WINDOW, INTENT_MODEL
|
from config import INTENT_TEMPERATURE, INTENT_MAX_TOKENS, INTENT_HISTORY_WINDOW, INTENT_MODEL, INTENT_RESPONSE_FORMAT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -355,17 +355,17 @@ class IntentAnalyzer:
|
|||||||
model = self.model or self._get_default_model()
|
model = self.model or self._get_default_model()
|
||||||
|
|
||||||
from core.llm_utils import call_llm
|
from core.llm_utils import call_llm
|
||||||
content = call_llm(
|
_llm_kwargs = dict(
|
||||||
client,
|
|
||||||
"",
|
|
||||||
model,
|
|
||||||
temperature=INTENT_TEMPERATURE,
|
temperature=INTENT_TEMPERATURE,
|
||||||
max_tokens=INTENT_MAX_TOKENS,
|
max_tokens=INTENT_MAX_TOKENS,
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": self.SYSTEM_PROMPT},
|
{"role": "system", "content": self.SYSTEM_PROMPT},
|
||||||
{"role": "user", "content": user_prompt}
|
{"role": "user", "content": user_prompt}
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
if INTENT_RESPONSE_FORMAT:
|
||||||
|
_llm_kwargs["response_format"] = {"type": "json_object"}
|
||||||
|
content = call_llm(client, "", model, **_llm_kwargs)
|
||||||
|
|
||||||
# 解析 JSON
|
# 解析 JSON
|
||||||
result = self._parse_json(content)
|
result = self._parse_json(content)
|
||||||
|
|||||||
Reference in New Issue
Block a user