Kaynağa Gözat

Merge branch 'dev_sgsc_wxm' of CRBC-MaaS-Platform-Project/LQAgentPlatform into dev

WangXuMing 3 hafta önce
ebeveyn
işleme
b074b53d70

+ 200 - 0
config/config.ini.template

@@ -0,0 +1,200 @@
+
+
+[model]
+MODEL_TYPE=qwen3_5_35b_a3b
+
+# Embedding模型类型选择: lq_qwen3_8b_emd, siliconflow_embed
+EMBEDDING_MODEL_TYPE=lq_qwen3_8b_emd
+
+# Rerank模型类型选择: bge_rerank_model, lq_rerank_model, silicoflow_rerank_model
+RERANK_MODEL_TYPE=lq_rerank_model
+
+# 完整性审查模型类型 (用于 llm_content_classifier_v2)
+COMPLETENESS_REVIEW_MODEL_TYPE=qwen3_5_122b_a10b
+
+
+[deepseek]
+DEEPSEEK_SERVER_URL=https://api.deepseek.com
+DEEPSEEK_MODEL_ID=deepseek-chat
+DEEPSEEK_API_KEY=sk-9fe722389bac47e9ab30cf45b32eb736
+
+[doubao]
+DOUBAO_SERVER_URL=https://ark.cn-beijing.volces.com/api/v3/
+DOUBAO_MODEL_ID=doubao-seed-1-6-flash-250715
+DOUBAO_API_KEY=c98686df-506f-432c-98de-32e571a8e916
+
+
+[qwen]
+QWEN_SERVER_URL=http://192.168.91.253:8003/v1/
+QWEN_MODEL_ID=qwen3-30b
+QWEN_API_KEY=sk-123456
+
+# Qwen3-30B 独立配置(与qwen配置相同,方便后续独立管理)
+[qwen3_30b]
+QWEN3_30B_SERVER_URL=http://192.168.91.253:8003/v1/
+QWEN3_30B_MODEL_ID=qwen3-30b
+QWEN3_30B_API_KEY=sk-123456
+
+
+[ai_review]
+# 调试模式配置
+MAX_REVIEW_UNITS=5
+REVIEW_MODE=all
+# REVIEW_MODE=all/random/first
+
+
+[app]
+APP_CODE=lq-agent
+APP_SECRET=sx-73d32556-605e-11f0-9dd8-acde48001122
+
+
+[launch]
+HOST = 0.0.0.0
+LAUNCH_PORT = 8002
+
+[redis]
+REDIS_URL=redis://:Wxcz666@@lqRedis_dev:6379
+REDIS_HOST=lqRedis_dev
+REDIS_PORT=6379
+REDIS_DB=0
+REDIS_PASSWORD=Wxcz666@
+REDIS_MAX_CONNECTIONS=50
+
+[log]
+LOG_FILE_PATH=logs
+LOG_FILE_MAX_MB=10
+LOG_BACKUP_COUNT=5
+CONSOLE_OUTPUT=True
+
+[user_lists]
+USERS=['user-001']
+
+
+[siliconflow]
+SLCF_MODEL_SERVER_URL=https://api.siliconflow.cn/v1
+SLCF_API_KEY=sk-rdabeukkgfwyelstbqlcupsrwfkmduqvadztvxeyumvllstt
+SLCF_CHAT_MODEL_ID=test-model
+SLCF_EMBED_MODEL_ID=netease-youdao/bce-embedding-base_v1
+SLCF_REANKER_MODEL_ID=BAAI/bge-reranker-v2-m3
+SLCF_VL_CHAT_MODEL_ID=THUDM/GLM-4.1V-9B-Thinking
+
+[siliconflow_embed]
+# 硅基流动 Embedding 模型配置
+SLCF_EMBED_SERVER_URL=https://api.siliconflow.cn/v1
+SLCF_EMBED_API_KEY=sk-rdabeukkgfwyelstbqlcupsrwfkmduqvadztvxeyumvllstt
+SLCF_EMBED_MODEL_ID=Qwen/Qwen3-Embedding-8B
+SLCF_EMBED_DIMENSIONS=4096
+
+[lq_qwen3_8b]
+QWEN_LOCAL_1_5B_SERVER_URL=http://192.168.91.253:9002/v1
+QWEN_LOCAL_1_5B_MODEL_ID=Qwen3-8B
+QWEN_LOCAL_1_5B_API_KEY=dummy
+
+# 本地部署的Qwen3-Embedding-8B配置
+[lq_qwen3_8b_emd]
+LQ_EMBEDDING_SERVER_URL=http://192.168.91.253:9003/v1
+LQ_EMBEDDING_MODEL_ID=Qwen3-Embedding-8B
+LQ_EMBEDDING_API_KEY=dummy
+
+[lq_qwen3_4b]
+QWEN_LOCAL_1_5B_SERVER_URL=http://192.168.91.253:9001/v1
+QWEN_LOCAL_1_5B_MODEL_ID=Qwen3-4B
+QWEN_LOCAL_1_5B_API_KEY=dummy
+
+# 本地部署的Qwen3-Reranker-8B配置
+[lq_rerank_model]
+LQ_RERANKER_SERVER_URL=http://192.168.91.253:9004/v1/rerank
+LQ_RERANKER_MODEL=Qwen3-Reranker-8B
+LQ_RERANKER_API_KEY=dummy
+LQ_RERANKER_TOP_N=10
+
+# 硅基流动API的Qwen3-Reranker-8B配置
+[silicoflow_rerank_model]
+SILICOFLOW_RERANKER_API_URL=https://api.siliconflow.cn/v1/rerank
+SILICOFLOW_RERANKER_API_KEY=sk-rdabeukkgfwyelstbqlcupsrwfkmduqvadztvxeyumvllstt
+SILICOFLOW_RERANKER_MODEL=Qwen/Qwen3-Reranker-8B
+
+# BGE Reranker配置
+[bge_rerank_model]
+BGE_RERANKER_SERVER_URL=http://192.168.91.253:9004/rerank
+BGE_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
+BGE_RERANKER_API_KEY=dummy
+BGE_RERANKER_TOP_N=10
+
+[lq_qwen3_8B_lora]
+LQ_QWEN3_8B_LQ_LORA_SERVER_URL=http://192.168.91.253:9006/v1
+LQ_QWEN3_8B_LQ_LORA_MODEL_ID=Qwen3-8B-lq-lora
+LQ_QWEN3_8B_LQ_LORA_API_KEY=dummy
+
+
+
+[mysql]
+MYSQL_HOST=192.168.92.61
+MYSQL_PORT=13306
+MYSQL_USER=root
+MYSQL_PASSWORD=lq@123
+MYSQL_DB=lq_db
+MYSQL_MIN_SIZE=1
+MYSQL_MAX_SIZE=5
+MYSQL_AUTO_COMMIT=True
+
+
+[pgvector]
+PGVECTOR_HOST=124.223.140.149
+PGVECTOR_PORT=7432
+PGVECTOR_DB=vector_db
+PGVECTOR_USER=vector_user
+PGVECTOR_PASSWORD=pg16@123
+
+
+[milvus]
+MILVUS_HOST=192.168.92.96
+MILVUS_PORT=30129
+MILVUS_DB=lq_db
+MILVUS_COLLECTION=first_bfp_collection_test
+MILVUS_USER=
+MILVUS_PASSWORD=
+
+
+[hybrid_search]
+# 混合检索权重配置
+DENSE_WEIGHT=0.3
+SPARSE_WEIGHT=0.7
+
+
+# ============================================================
+# DashScope Qwen3.5 系列模型配置
+# ============================================================
+
+# DashScope Qwen3.5-35B-A3B 模型
+[qwen3_5_35b_a3b]
+DASHSCOPE_SERVER_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+DASHSCOPE_MODEL_ID=qwen3.5-35b-a3b
+DASHSCOPE_API_KEY=sk-98cca096416a41d5a6cec68b824486c5
+
+# DashScope Qwen3.5-27B 模型
+[qwen3_5_27b]
+DASHSCOPE_SERVER_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+DASHSCOPE_MODEL_ID=qwen3.5-27b
+DASHSCOPE_API_KEY=sk-98cca096416a41d5a6cec68b824486c5
+
+# DashScope Qwen3.5-122B-A10B 模型
+[qwen3_5_122b_a10b]
+DASHSCOPE_SERVER_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+DASHSCOPE_MODEL_ID=qwen3.5-122b-a10b
+DASHSCOPE_API_KEY=sk-98cca096416a41d5a6cec68b824486c5
+
+# ============================================================
+# LLM 通用配置
+# ============================================================
+
+[llm_keywords]
+TIMEOUT=60
+MAX_RETRIES=2
+CONCURRENT_WORKERS=20
+STREAM=false
+TEMPERATURE=0.3
+MAX_TOKENS=1024
+
+
+

+ 143 - 129
core/construction_review/component/doc_worker/classification/hierarchy_classifier.py

@@ -1,194 +1,208 @@
 """
 目录分类模块(基于LLM API智能识别)
 
-适配 file_parse 的配置系统,通过异步并发调用LLM API来判断一级目录的分类。
+使用 config/config.ini 中的通用 LLM 配置,通过异步并发调用 LLM API 来判断一级目录的分类。
 """
 
 from __future__ import annotations
 
-from collections import Counter
 import asyncio
 import json
+import re
+from collections import Counter
 from typing import Any, Dict, List, Optional
 
+from foundation.infrastructure.config.config import config_handler
+from foundation.observability.logger.loggering import review_logger as logger
+from foundation.ai.agent.generate.model_generate import generate_model_client
+
 from ..interfaces import HierarchyClassifier as IHierarchyClassifier
 from ..config.provider import default_config_provider
-from ..utils.llm_client import LLMClient
 from ..utils.prompt_loader import PromptLoader
 
 
+def _extract_json(text: str) -> Optional[Dict[str, Any]]:
+    """从字符串中提取第一个有效 JSON 对象"""
+    for pattern in [r"```json\s*(\{.*?})\s*```", r"```\s*(\{.*?})\s*```"]:
+        m = re.search(pattern, text, re.DOTALL)
+        if m:
+            try:
+                return json.loads(m.group(1))
+            except json.JSONDecodeError:
+                pass
+    try:
+        for candidate in re.findall(r"(\{.*?\})", text, re.DOTALL):
+            try:
+                return json.loads(candidate)
+            except json.JSONDecodeError:
+                pass
+    except Exception:
+        pass
+    return None
+
+
 class HierarchyClassifier(IHierarchyClassifier):
-    """基于层级结构的目录分类器(通过LLM API智能识别来分类一级目录)"""
+    """基于层级结构的目录分类器(通过 LLM API 智能识别来分类一级目录)"""
 
     def __init__(self):
-        """初始化分类器"""
         self._cfg = default_config_provider
-        
-        # 获取分类配置
+        self._concurrency = int(config_handler.get("llm_keywords", "CONCURRENT_WORKERS", "20"))
+
         self.category_mapping = self._cfg.get("categories.mapping", {})
-        
-        # 初始化LLM客户端和提示词加载器
-        self.llm_client = LLMClient(config_provider=self._cfg)
         self.prompt_loader = PromptLoader()
-        
-        # 获取标准类别列表(从CSV动态加载)
         self.standard_categories = self.prompt_loader.get_standard_categories()
 
+    # ------------------------------------------------------------------
+    # 内部 LLM 调用
+    # ------------------------------------------------------------------
+
+    async def _call_once(self, messages: List[Dict[str, str]]) -> Optional[Dict[str, Any]]:
+        """单次异步 LLM 调用,失败返回 None"""
+        system_prompt = next((m["content"] for m in messages if m["role"] == "system"), "")
+        user_prompt   = next((m["content"] for m in messages if m["role"] == "user"),   "")
+        try:
+            content = await generate_model_client.get_model_generate_invoke(
+                trace_id="hierarchy_classifier",
+                system_prompt=system_prompt,
+                user_prompt=user_prompt,
+            )
+            result = _extract_json(content)
+            return result if result is not None else {"raw_content": content}
+        except Exception as e:
+            logger.error(f"[HierarchyClassifier] LLM 调用失败: {e}")
+            return None
+
+    async def _batch_call(self, requests: List[List[Dict[str, str]]]) -> List[Optional[Dict[str, Any]]]:
+        """并发批量调用 LLM"""
+        semaphore = asyncio.Semaphore(self._concurrency)
+
+        async def bounded(msgs):
+            async with semaphore:
+                return await self._call_once(msgs)
+
+        return list(await asyncio.gather(*[bounded(r) for r in requests]))
+
+    # ------------------------------------------------------------------
+    # 公开接口
+    # ------------------------------------------------------------------
+
     async def classify_async(
         self, toc_items: List[Dict[str, Any]], target_level: int = 1
     ) -> Dict[str, Any]:
-        """
-        异步版本的目录分类(推荐在已有事件循环中使用)。
-        """
-        print(f"\n正在对{target_level}级目录进行智能分类(基于LLM API识别)...")
-        
-        # 筛选出指定层级的目录项
+        """异步版目录分类(推荐在已有事件循环中使用)"""
+        logger.debug(f"[HierarchyClassifier] 开始对 {target_level} 级目录进行智能分类...")
+
         level1_items = [item for item in toc_items if item["level"] == target_level]
-        
         if not level1_items:
-            print(f"  警告: 未找到{target_level}级目录项")
-            return {
-                "items": [],
-                "total_count": 0,
-                "target_level": target_level,
-                "category_stats": {},
-            }
-        
-        print(f"  找到 {len(level1_items)} 个{target_level}级目录项")
-        
-        # 构建层级结构:为每个一级目录找到其对应的二级目录
+            logger.warning(f"[HierarchyClassifier] 未找到 {target_level} 级目录项")
+            return {"items": [], "total_count": 0, "target_level": target_level, "category_stats": {}}
+
+        logger.debug(f"[HierarchyClassifier] 找到 {len(level1_items)} 个 {target_level} 级目录项,准备 LLM 分类")
+
+        # 构建带二级子目录的层级结构
         level1_with_children = []
-        
         for i, level1_item in enumerate(level1_items):
-            # 找到当前一级目录在原列表中的索引
             level1_idx = toc_items.index(level1_item)
-            
-            # 找到下一个一级目录的索引(如果存在)
-            if i < len(level1_items) - 1:
-                next_level1_item = level1_items[i + 1]
-                next_level1_idx = toc_items.index(next_level1_item)
-            else:
-                next_level1_idx = len(toc_items)
-            
-            # 提取当前一级目录下的二级目录
-            level2_children = [
-                item
-                for item in toc_items[level1_idx + 1 : next_level1_idx]
+            next_idx = toc_items.index(level1_items[i + 1]) if i < len(level1_items) - 1 else len(toc_items)
+            children = [
+                item for item in toc_items[level1_idx + 1: next_idx]
                 if item["level"] == target_level + 1
             ]
-            
-            level1_with_children.append(
-                {"level1_item": level1_item, "level2_children": level2_children}
-            )
-        
-        print(f"  正在使用LLM API进行异步并发识别分类...")
-        
-        # 准备LLM API请求
+            level1_with_children.append({"level1_item": level1_item, "level2_children": children})
+
+        # 构造 LLM 请求
         llm_requests = []
-        for item_with_children in level1_with_children:
-            level1_item = item_with_children["level1_item"]
-            level2_children = item_with_children["level2_children"]
-            
-            # 准备二级目录标题列表
-            level2_titles = "\n".join([f"- {child['title']}" for child in level2_children])
-            if not level2_titles:
-                level2_titles = "(无二级目录)"
-            
-            # 渲染提示词模板
+        for entry in level1_with_children:
+            level1_item   = entry["level1_item"]
+            level2_titles = "\n".join(f"- {c['title']}" for c in entry["level2_children"]) or "(无二级目录)"
             prompt = self.prompt_loader.render(
                 "toc_classification",
                 level1_title=level1_item["title"],
-                level2_titles=level2_titles
+                level2_titles=level2_titles,
             )
-            # 构建消息列表
-            messages = [
+            llm_requests.append([
                 {"role": "system", "content": prompt["system"]},
-                {"role": "user", "content": prompt["user"]}
-            ]
-            # 添加打印语句,用于调试
-            print(f"\n--- LLM Request for '{level1_item['title']}' ---")
-            print(f"System Prompt:\n{messages[0]['content']}")
-            print(f"User Prompt:\n{messages[1]['content']}")
-            print("---------------------------------------\n")
-
-            llm_requests.append(messages)
-
-        # 批量异步调用LLM API
-        llm_results = await self.llm_client.batch_call_async(llm_requests)
-        
-        # 处理分类结果
+                {"role": "user",   "content": prompt["user"]},
+            ])
+
+        # 批量调用
+        llm_results = await self._batch_call(llm_requests)
+
+        # 解析结果
         classified_items = []
-        category_stats = Counter()
-        
-        for i, (item_with_children, llm_result) in enumerate(zip(level1_with_children, llm_results)):
-            level1_item = item_with_children["level1_item"]
-            level2_children = item_with_children["level2_children"]
-            
-            print(f"  DEBUG: LLM raw result for '{level1_item['title']}': {llm_result}")
-            # 解析LLM返回结果
+        category_stats: Counter = Counter()
+
+        for entry, llm_result in zip(level1_with_children, llm_results):
+            level1_item   = entry["level1_item"]
+            level2_children = entry["level2_children"]
+
+            logger.debug(f"[HierarchyClassifier] '{level1_item['title']}' LLM 返回: {llm_result}")
+
             if llm_result and isinstance(llm_result, dict):
-                category_cn = llm_result.get("category_cn", "")
+                category_cn   = llm_result.get("category_cn", "")
                 category_code = llm_result.get("category_code", "")
-                confidence = llm_result.get("confidence", 0.0)
-                
-                # 强制移除无效的类别代码,但保留"非标准项"作为有效的兜底类别
-                if category_code in ["non_standard_invalid", "unknown"]:
-                    category_cn = ""
-                    category_code = ""
-                
-                # 验证类别是否在标准类别列表中("非标准项"是特殊的兜底类别,也是有效的)
-                if not category_cn or (category_cn not in self.standard_categories and category_cn != "非标准项"):
-                    # 如果不在标准类别中,强制使用"非标准项"作为兜底
+                confidence    = llm_result.get("confidence", 0.0)
+
+                if category_code in ("non_standard_invalid", "unknown"):
+                    category_cn = category_code = ""
+
+                if not category_cn or (
+                    category_cn not in self.standard_categories and category_cn != "非标准项"
+                ):
                     if category_cn and category_cn != "非标准项":
-                        print(f"  警告: LLM返回的类别 '{category_cn}' 不在标准类别中,归类为'非标准项'")
-                    elif not category_cn:
-                        print(f"  警告: LLM返回的类别为空或无效,归类为'非标准项'")
+                        logger.warning(
+                            f"[HierarchyClassifier] '{level1_item['title']}' "
+                            f"LLM 返回类别 '{category_cn}' 不在标准列表,归为'非标准项'"
+                        )
+                    else:
+                        logger.warning(
+                            f"[HierarchyClassifier] '{level1_item['title']}' "
+                            f"LLM 返回类别为空或无效,归为'非标准项'"
+                        )
                     category_cn = "非标准项"
                     category_code = "non_standard"
-                
-                # 确保category_code与mapping一致
+
                 if category_cn in self.category_mapping:
                     category_code = self.category_mapping.get(category_cn, category_code)
                 elif category_cn == "非标准项":
                     category_code = "non_standard"
             else:
-                # LLM调用失败,使用"非标准项"作为兜底
-                print(f"  警告: 一级目录 '{level1_item['title']}' 的LLM分类失败,归类为'非标准项'")
+                logger.error(
+                    f"[HierarchyClassifier] '{level1_item['title']}' LLM 分类失败,归为'非标准项'"
+                )
                 category_cn = "非标准项"
                 category_code = "non_standard"
                 confidence = 0.0
-            
-            classified_items.append(
-                {
-                    "title": level1_item["title"],
-                    "page": level1_item["page"],
-                    "level": level1_item["level"],
-                    "category": category_cn,
-                    "category_code": category_code,
-                    "original": level1_item.get("original", ""),
-                    "level2_count": len(level2_children),
-                    "level2_titles": [child["title"] for child in level2_children],
-                    "confidence": confidence if llm_result else 0.0,
-                }
-            )
-            
+
+            classified_items.append({
+                "title":        level1_item["title"],
+                "page":         level1_item["page"],
+                "level":        level1_item["level"],
+                "category":     category_cn,
+                "category_code": category_code,
+                "original":     level1_item.get("original", ""),
+                "level2_count": len(level2_children),
+                "level2_titles": [c["title"] for c in level2_children],
+                "confidence":   confidence if llm_result else 0.0,
+            })
             category_stats[category_cn] += 1
-        
-        print(f"  分类完成!共分类 {len(classified_items)} 个目录项")
-        
+
+        logger.debug(
+            f"[HierarchyClassifier] 分类完成,共 {len(classified_items)} 个目录项,"
+            f"分布: {dict(category_stats)}"
+        )
+
         return {
-            "items": classified_items,
-            "total_count": len(classified_items),
-            "target_level": target_level,
+            "items":          classified_items,
+            "total_count":    len(classified_items),
+            "target_level":   target_level,
             "category_stats": dict(category_stats),
         }
 
     def classify(
         self, toc_items: List[Dict[str, Any]], target_level: int = 1
     ) -> Dict[str, Any]:
-        """
-        同步包装,内部调用异步实现。适合无事件循环的同步场景。
-        """
+        """同步包装,内部调用异步实现。适合无事件循环的同步场景。"""
         try:
             return asyncio.run(self.classify_async(toc_items, target_level))
         except RuntimeError as exc:

+ 10 - 21
core/construction_review/component/doc_worker/utils/llm_client.py

@@ -23,6 +23,7 @@ except ImportError:
 
 from ..config.provider import default_config_provider
 from foundation.infrastructure.config.config import config_handler
+from foundation.observability.logger.loggering import review_logger as logger
 
 
 class LLMClient:
@@ -245,7 +246,7 @@ class LLMClient:
             else:
                 return None
         except Exception as e:
-            print(f"  LLM API调用错误: {e}")
+            logger.error(f"[LLMClient] LLM API调用错误: {e}")
             return None
 
     async def batch_call_async(self, requests: List[List[Dict[str, str]]]) -> List[Optional[Dict[str, Any]]]:
@@ -261,7 +262,7 @@ class LLMClient:
         if not HAS_AIOHTTP:
             # 回退到同步调用(在异步环境中)
             if HAS_REQUESTS:
-                print("  警告: 未安装aiohttp,在异步环境中使用同步调用(性能较差)")
+                logger.warning("[LLMClient] 未安装aiohttp,在异步环境中使用同步调用(性能较差)")
                 results = []
                 for req in requests:
                     try:
@@ -283,7 +284,7 @@ class LLMClient:
                         else:
                             results.append(None)
                     except Exception as e:
-                        print(f"  LLM API调用错误: {e}")
+                        logger.error(f"[LLMClient] LLM API调用错误: {e}")
                         results.append(None)
                 return results
             else:
@@ -304,7 +305,7 @@ class LLMClient:
             processed_results = []
             for result in results:
                 if isinstance(result, Exception):
-                    print(f"  LLM API调用异常: {result}")
+                    logger.error(f"[LLMClient] LLM API调用异常: {result}")
                     processed_results.append(None)
                 else:
                     processed_results.append(result)
@@ -314,31 +315,19 @@ class LLMClient:
     def batch_call(self, requests: List[List[Dict[str, str]]]) -> List[Optional[Dict[str, Any]]]:
         """
         同步批量调用LLM API(兼容接口)
-        
-        参数:
-            requests: 请求列表,每个请求是一个消息列表
-            
-        返回:
-            结果列表,与输入请求一一对应
-        
+
         注意: 此方法使用 workflow_manager.py 的全局事件循环,不再自行初始化事件循环
         """
         if HAS_AIOHTTP:
-            # 使用异步实现
             try:
-                # 获取当前事件循环
                 loop = asyncio.get_event_loop()
-                # 如果事件循环已在运行,避免 run_until_complete 引发异常,直接回退同步
                 if loop.is_running():
-                    print("检测到运行中的事件循环,batch_call 请改用 await batch_call_async;本次回退同步调用")
+                    logger.warning("[LLMClient] 检测到运行中的事件循环,batch_call 请改用 await batch_call_async;本次回退同步调用")
                     return self._batch_call_sync_fallback(requests)
-
-                # 事件循环存在但未运行,可以直接使用 run_until_complete
-                print("异步调用LLM API进行目录分类处理")
+                logger.debug("[LLMClient] 异步调用LLM API进行目录分类处理")
                 return loop.run_until_complete(self.batch_call_async(requests))
             except RuntimeError:
-                # 如果没有事件循环,回退到同步调用
-                print("同步调用LLM API进行目录分类处理(无事件循环)")
+                logger.debug("[LLMClient] 同步调用LLM API进行目录分类处理(无事件循环)")
                 return self._batch_call_sync_fallback(requests)
         else:
             return self._batch_call_sync_fallback(requests)
@@ -367,7 +356,7 @@ class LLMClient:
                 else:
                     results.append(None)
             except Exception as e:
-                print(f"  LLM API调用错误: {e}")
+                logger.error(f"[LLMClient] LLM API调用错误: {e}")
                 results.append(None)
         return results
 

+ 17 - 16
core/construction_review/component/reviewers/utils/llm_content_classifier_v2.py

@@ -36,6 +36,7 @@ from openai import AsyncOpenAI
 
 # 导入统一配置处理器
 from foundation.infrastructure.config.config import config_handler
+from foundation.observability.logger.loggering import review_logger as logger
 
 
 # ==================== 配置类 ====================
@@ -476,7 +477,7 @@ class EmbeddingClient:
                 return response.data[0].embedding
             return None
         except Exception as e:
-            print(f"    Embedding API调用失败: {e}")
+            logger.error(f"Embedding API调用失败: {e}")
             return None
 
     async def get_embeddings_batch(self, texts: List[str]) -> List[Optional[List[float]]]:
@@ -491,7 +492,7 @@ class EmbeddingClient:
                 results.append(item.embedding)
             return results
         except Exception as e:
-            print(f"    Embedding API批量调用失败: {e}")
+            logger.error(f"Embedding API批量调用失败: {e}")
             return [None] * len(texts)
 
     def cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
@@ -635,7 +636,7 @@ class ContentClassifierClient:
                 )
 
                 if is_similar:
-                    print(f"  [{section.section_name}] 相似度检查通过 ({similarity:.3f} >= {EMBEDDING_SIMILARITY_THRESHOLD}),跳过LLM分类,默认包含所有三级分类")
+                    logger.debug(f"[{section.section_name}] 相似度检查通过 ({similarity:.3f} >= {EMBEDDING_SIMILARITY_THRESHOLD}),跳过LLM分类,默认包含所有三级分类")
                     # 生成默认分类结果:包含所有三级分类
                     all_contents = self._generate_default_classification(section)
                     total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, all_contents)
@@ -653,9 +654,9 @@ class ContentClassifierClient:
                         coverage_rate=coverage_rate
                     )
                 else:
-                    print(f"  [{section.section_name}] 相似度检查未通过 ({similarity:.3f} < {EMBEDDING_SIMILARITY_THRESHOLD}),继续LLM分类")
+                    logger.debug(f"[{section.section_name}] 相似度检查未通过 ({similarity:.3f} < {EMBEDDING_SIMILARITY_THRESHOLD}),继续LLM分类")
             else:
-                print(f"  [{section.section_name}] 未在construction_plan_standards.csv中找到对应标准,继续LLM分类")
+                logger.debug(f"[{section.section_name}] 未在construction_plan_standards.csv中找到对应标准,继续LLM分类")
 
         # 如果内容过长,分块处理
         MAX_LINES_PER_CHUNK = 150  # 每个块最多150行
@@ -668,7 +669,7 @@ class ContentClassifierClient:
         # 内容过长,无重叠分块处理
         # 不使用 overlap:有重叠时边界行被两块各看一次反而容易两头都不认领,
         # 无重叠时每行只属于唯一一块,prompt 里的"必须分类每一行"约束更有效。
-        print(f"  [{section.section_name}] 内容较长({total_lines}行),分块处理...")
+        logger.debug(f"[{section.section_name}] 内容较长({total_lines}行),分块处理...")
         all_contents = []
         chunk_size = MAX_LINES_PER_CHUNK
 
@@ -680,9 +681,9 @@ class ContentClassifierClient:
             chunk_result = await self._classify_single_chunk(chunk_section, 0, is_chunk=True)
 
             if chunk_result.error:
-                print(f"    块 {chunk_start+1}-{chunk_end} 处理失败: {chunk_result.error[:50]}")
+                logger.error(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 处理失败: {chunk_result.error[:50]}")
             else:
-                print(f"    块 {chunk_start+1}-{chunk_end} 成功: {len(chunk_result.classified_contents)} 个分类")
+                logger.debug(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 成功: {len(chunk_result.classified_contents)} 个分类")
                 all_contents.extend(chunk_result.classified_contents)
 
             # 无重叠:下一块从当前块末尾紧接开始
@@ -863,12 +864,12 @@ class ContentClassifierClient:
         # 解析成功(包括空结果,表示模型判定内容不符合任何分类标准)
         if parse_success:
             if not contents:
-                print(f"  [{section.section_name}] 模型判定无匹配内容,记录为未分类")
+                logger.debug(f"[{section.section_name}] 模型判定无匹配内容,记录为未分类")
             return contents, None
 
         # 解析失败(JSON格式错误),尝试让模型修复(最多3次)
-        print(f"  [{section.section_name}] JSON解析失败,请求模型修复...")
-        print(f"    原始响应前200字符: {response[:200]}...")
+        logger.warning(f"[{section.section_name}] JSON解析失败,请求模型修复...")
+        logger.debug(f"[{section.section_name}] 原始响应前200字符: {response[:200]}...")
 
         original_response = response
 
@@ -882,17 +883,17 @@ class ContentClassifierClient:
                 # 尝试解析修复后的输出
                 contents, parse_success = self._parse_response(fixed_response, section)
                 if parse_success:
-                    print(f"  [{section.section_name}] 模型修复成功(第{attempt+1}次)")
+                    logger.debug(f"[{section.section_name}] 模型修复成功(第{attempt+1}次)")
                     if not contents:
-                        print(f"  [{section.section_name}] 修复后模型判定无匹配内容,记录为未分类")
+                        logger.debug(f"[{section.section_name}] 修复后模型判定无匹配内容,记录为未分类")
                     return contents, None
                 else:
-                    print(f"    第{attempt+1}次修复失败,继续重试...")
+                    logger.debug(f"[{section.section_name}] 第{attempt+1}次修复失败,继续重试...")
                     original_response = fixed_response
             except Exception as e:
                 return [], f"请求模型修复失败: {str(e)}"
 
-        print(f"  [{section.section_name}] 模型修复3次后仍无法解析JSON")
+        logger.error(f"[{section.section_name}] 模型修复3次后仍无法解析JSON")
         return [], "模型修复3次后仍无法解析JSON"
 
     def _build_fix_prompt(self, original_response: str) -> str:
@@ -1094,7 +1095,7 @@ class ContentClassifierClient:
                     if attempt < max_retries - 1:
                         # 指数退避: 2^attempt * (1 + random)
                         delay = base_delay * (2 ** attempt) + (hash(prompt) % 1000) / 1000
-                        print(f"    API限流(429),等待 {delay:.1f}s 后重试 ({attempt + 1}/{max_retries})...")
+                        logger.warning(f"API限流(429),等待 {delay:.1f}s 后重试 ({attempt + 1}/{max_retries})...")
                         await asyncio.sleep(delay)
                         continue
                 # 其他错误或重试次数用完,抛出异常

+ 15 - 69
foundation/ai/models/model_handler.py

@@ -10,7 +10,6 @@ AI模型处理器
 - doubao: 豆包模型
 - qwen: 通义千问模型
 - deepseek: DeepSeek模型
-- gemini: Gemini模型
 - lq_qwen3_8b: 本地Qwen3-8B模型
 - lq_qwen3_8b_lq_lora: 本地Qwen3-8B-lq-lora模型
 - lq_qwen3_4b: 本地Qwen3-4B模型
@@ -18,7 +17,7 @@ AI模型处理器
 - lq_qwen3_8b_emd: 本地Qwen3-Embedding-8B嵌入模型
 - siliconflow_embed: 硅基流动Qwen3-Embedding-8B嵌入模型
 - lq_bge_reranker_v2_m3: 本地BGE-reranker-v2-m3重排序模型
-- qwen3_5_35b_a3b: DashScope Qwen3.5-35B-A3B模型
+- qwen3_5_35b_a3b: DashScope Qwen3.5-35B-A3B模型(默认兜底模型)
 - qwen3_5_27b: DashScope Qwen3.5-27B模型
 - qwen3_5_122b_a10b: DashScope Qwen3.5-122B-A10B模型
 """
@@ -158,8 +157,6 @@ class ModelHandler:
         try:
             if model_type == "doubao":
                 model = self._get_doubao_model()
-            elif model_type == "gemini":
-                model = self._get_gemini_model()
             elif model_type == "qwen":
                 model = self._get_qwen_model()
             elif model_type == "qwen3_30b":
@@ -181,9 +178,8 @@ class ModelHandler:
             elif model_type == "qwen3_5_122b_a10b":
                 model = self._get_qwen3_5_122b_a10b_model()
             else:
-                # 默认返回gemini
-                logger.warning(f"未知的模型类型 '{model_type}',使用默认gemini模型")
-                model = self._get_gemini_model()
+                logger.warning(f"未知的模型类型 '{model_type}',使用默认 qwen3_5_35b_a3b 模型")
+                model = self._get_qwen3_5_35b_a3b_model()
 
             if model:
                 self._model_cache[cache_key] = model
@@ -195,14 +191,14 @@ class ModelHandler:
         except Exception as e:
             logger.error(f"获取模型失败 [{model_type}]: {e}")
 
-            # 尝试使用gemini作为降级方案
-            if model_type != "gemini":
-                logger.info("尝试使用Gemini模型作为降级方案")
+            # 使用 qwen3_5_35b_a3b 作为兜底降级方案
+            if model_type != "qwen3_5_35b_a3b":
+                logger.info("尝试使用 qwen3_5_35b_a3b 模型作为降级方案")
                 try:
-                    fallback_model = self._get_gemini_model()
+                    fallback_model = self._get_qwen3_5_35b_a3b_model()
                     if fallback_model:
                         self._model_cache[cache_key] = fallback_model
-                        logger.warning(f"已切换到Gemini降级模型")
+                        logger.warning("已切换到 qwen3_5_35b_a3b 降级模型")
                         return fallback_model
                 except Exception as fallback_error:
                     logger.error(f"降级模型也失败: {fallback_error}")
@@ -245,8 +241,6 @@ class ModelHandler:
         try:
             if model_type == "doubao":
                 model = self._get_doubao_model()
-            elif model_type == "gemini":
-                model = self._get_gemini_model()
             elif model_type == "qwen":
                 model = self._get_qwen_model()
             elif model_type == "qwen3_30b":
@@ -268,9 +262,8 @@ class ModelHandler:
             elif model_type == "qwen3_5_122b_a10b":
                 model = self._get_qwen3_5_122b_a10b_model()
             else:
-                # 默认返回gemini
-                logger.warning(f"未知的模型类型 '{model_type}',使用默认gemini模型")
-                model = self._get_gemini_model()
+                logger.warning(f"未知的模型类型 '{model_type}',使用默认 qwen3_5_35b_a3b 模型")
+                model = self._get_qwen3_5_35b_a3b_model()
 
             if model:
                 self._model_cache[cache_key] = model
@@ -282,14 +275,14 @@ class ModelHandler:
         except Exception as e:
             logger.error(f"动态获取模型失败 [{model_type}]: {e}")
 
-            # 尝试使用gemini作为降级方案
-            if model_type != "gemini":
-                logger.info("尝试使用Gemini模型作为降级方案")
+            # 使用 qwen3_5_35b_a3b 作为兜底降级方案
+            if model_type != "qwen3_5_35b_a3b":
+                logger.info("尝试使用 qwen3_5_35b_a3b 模型作为降级方案")
                 try:
-                    fallback_model = self._get_gemini_model()
+                    fallback_model = self._get_qwen3_5_35b_a3b_model()
                     if fallback_model:
                         self._model_cache[cache_key] = fallback_model
-                        logger.warning(f"已切换到Gemini降级模型")
+                        logger.warning("已切换到 qwen3_5_35b_a3b 降级模型")
                         return fallback_model
                 except Exception as fallback_error:
                     logger.error(f"降级模型也失败: {fallback_error}")
@@ -537,53 +530,6 @@ class ModelHandler:
             error = ModelAPIError(f"DeepSeek模型初始化异常: {e}")
             return self._handle_model_error("deepseek", error)
 
-    def _get_gemini_model(self):
-        """
-        获取Gemini模型
-
-        Returns:
-            ChatOpenAI: 配置好的Gemini模型实例
-        """
-        try:
-            gemini_url = self.config.get("gemini", "GEMINI_SERVER_URL")
-            gemini_model_id = self.config.get("gemini", "GEMINI_MODEL_ID")
-            gemini_api_key = self.config.get("gemini", "GEMINI_API_KEY")
-
-            # 验证配置完整性
-            if not all([gemini_url, gemini_model_id, gemini_api_key]):
-                missing = []
-                if not gemini_url:
-                    missing.append("GEMINI_SERVER_URL")
-                if not gemini_model_id:
-                    missing.append("GEMINI_MODEL_ID")
-                if not gemini_api_key:
-                    missing.append("GEMINI_API_KEY")
-                raise ModelConfigError(f"Gemini模型配置不完整,缺少: {', '.join(missing)}")
-
-            # 检查连接
-            if not self._check_connection(gemini_url, gemini_api_key):
-                logger.warning(f"Gemini模型服务连接失败: {gemini_url}")
-                raise ModelConnectionError(f"无法连接到Gemini模型服务: {gemini_url}")
-
-            llm = ChatOpenAI(
-                base_url=gemini_url,
-                model=gemini_model_id,
-                api_key=gemini_api_key,
-                temperature=0.7,
-                timeout=self.REQUEST_TIMEOUT,
-            )
-
-            logger.info(f"Gemini模型初始化成功: {gemini_model_id}")
-            return llm
-
-        except ModelConfigError:
-            raise
-        except ModelConnectionError:
-            raise
-        except Exception as e:
-            error = ModelAPIError(f"Gemini模型初始化异常: {e}")
-            return self._handle_model_error("gemini", error)
-
     def _get_lq_qwen3_8b_model(self):
         """
         获取本地Qwen3-8B-Instruct模型