diff --git a/api/chat_routes.py b/api/chat_routes.py index b0c32d4..3e3efb4 100644 --- a/api/chat_routes.py +++ b/api/chat_routes.py @@ -2196,6 +2196,9 @@ def rag(): import re start_time = _time.time() full_answer = [] + # === 性能插桩:各阶段耗时 === + _t = {'intent': 0, 'cache': 0, 'search': 0, 'vlm': 0, 'image': 0, 'rescue': 0, 'llm': 0, 'post': 0} + _t_prev = start_time try: # 0. 意图分析(改写 + 双层判断) @@ -2227,7 +2230,6 @@ def rag(): # 构建上下文 context_text = "" if history: - # 提取最近的助手回答 for msg in reversed(history): if msg.get("role") == "assistant": context_text = msg.get("content", "") @@ -2260,7 +2262,6 @@ def rag(): 请直接回答用户问题。""" - # 流式生成回答 for content in call_llm_stream( client, prompt=user_prompt, @@ -2274,14 +2275,15 @@ def rag(): full_answer.append(content) 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" - return # 直接返回,不执行后续检索 + return except Exception as e: logger.warning(f"意图分析失败: {e},继续执行检索流程") 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 语义缓存检查(跳过检索+生成全流程) _semantic_cache_emb = None @@ -2343,6 +2345,8 @@ def rag(): # 1. 发送开始事件 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. 执行混合检索(扩大召回数量,确保图片切片有机会被召回) # 如果意图分析生成了子查询(对比类),传给搜索引擎并行检索 @@ -2619,6 +2623,8 @@ def rag(): # 2.5. 懒加载增强(Phase 4) # 暂时禁用:VLM 调用耗时过长,可能导致请求超时 # TODO: 后续可改为异步后台任务 + _t['search'] = round((_time.time() - _t_prev) * 1000, 1) + _t_prev = _time.time() try: import asyncio from knowledge.lazy_enhance import enhance_retrieved_chunks @@ -2626,9 +2632,13 @@ def rag(): asyncio.run(enhance_retrieved_chunks(contexts, message, kb_name)) except Exception as e: logger.warning(f"懒加载增强失败: {e}") + _t['vlm'] = round((_time.time() - _t_prev) * 1000, 1) + _t_prev = _time.time() # 3. 选择要展示的图片(Phase 5) selected_images = select_images(contexts, message) + _t['image'] = round((_time.time() - _t_prev) * 1000, 1) + _t_prev = _time.time() # 调试事件:图片选择详情 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" # 5. 流式生成回答 + _t['rescue'] = round((_time.time() - _t_prev) * 1000, 1) + _t_prev = _time.time() from core.engine import get_engine engine = get_engine() for token in engine.generate_answer_stream(message, enhanced_context, history): full_answer.append(token) 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:答案对齐过滤器 # 从 LLM 回答中提取图号引用,过滤图片选择结果 @@ -2873,12 +2887,16 @@ def rag(): if step.get('name') == 'rerank' and step.get('applied'): rerank_time = step.get('time_ms', 0) rerank_cached = step.get('cached', False) + _t['post'] = round((_time.time() - _t_prev) * 1000, 1) finish_event["timing"] = { "total_search_ms": timing_info.get('total_ms', 0), "rerank_ms": rerank_time, "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 写入语义缓存 try: from core.semantic_cache import get_semantic_cache diff --git a/api/sync_routes.py b/api/sync_routes.py index 55dce3c..db0f2dd 100644 --- a/api/sync_routes.py +++ b/api/sync_routes.py @@ -315,3 +315,48 @@ def stop_sync_monitor() -> Tuple[Any, int]: message="操作失败", 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 + }) diff --git a/core/engine.py b/core/engine.py index 8491074..894bd7f 100644 --- a/core/engine.py +++ b/core/engine.py @@ -82,7 +82,7 @@ try: CLUSTER_SEED_FLOOR, CLUSTER_MAX_BOOST_PER_SECTION, CLUSTER_MAX_SECTIONS, CLUSTER_SECTION_PREFIX_LEVELS, # 上下文与生成 - LLM_TEMPERATURE, LLM_MAX_TOKENS, RECALL_MULTIPLIER, + LLM_TEMPERATURE, LLM_TOP_P, LLM_MAX_TOKENS, RECALL_MULTIPLIER, # FAQ 与黑名单 FAQ_RECALL_TOP_K, FAQ_BOOST_AMOUNT, FAQ_DECAY_MONTHS, FAQ_DECAY_RATE, FAQ_DECAY_MAX, BLACKLIST_MIN_DISLIKES, BLACKLIST_CACHE_TTL, @@ -2361,6 +2361,7 @@ class RAGEngine: "", MODEL, temperature=LLM_TEMPERATURE, + top_p=LLM_TOP_P, max_tokens=LLM_MAX_TOKENS, messages=messages, error_prefix="[错误]" diff --git a/core/intent_analyzer.py b/core/intent_analyzer.py index 4043dc8..6bf23c8 100644 --- a/core/intent_analyzer.py +++ b/core/intent_analyzer.py @@ -22,7 +22,7 @@ import threading from dataclasses import dataclass, asdict 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__) @@ -355,17 +355,17 @@ class IntentAnalyzer: model = self.model or self._get_default_model() from core.llm_utils import call_llm - content = call_llm( - client, - "", - model, + _llm_kwargs = dict( temperature=INTENT_TEMPERATURE, max_tokens=INTENT_MAX_TOKENS, messages=[ {"role": "system", "content": self.SYSTEM_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 result = self._parse_json(content)