""" LLM客户端封装 统一使用OpenAI格式调用,支持提供商预设(DeepSeek、Xiaomi MiMo等) """ import json import re from typing import Optional, Dict, Any, List from openai import OpenAI from ..config import Config # 标签的正则表达式 # 匹配 ... 标签及其内容(支持多行,非贪婪匹配) # 也处理 ... 变体(某些模型可能使用略微不同的格式) THINK_TAG_PATTERN = re.compile(r'[\s\S]*?\s*', re.IGNORECASE) # 某些提供商的推理模型会在响应中包含标签 PROVIDERS_WITH_THINK_TAGS = {"deepseek", "xiaomi_mimo", "minimax"} class LLMClient: """LLM客户端,支持提供商预设配置""" def __init__( self, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, provider: Optional[str] = None, ): self.api_key = api_key or Config.LLM_API_KEY self.base_url = base_url or Config.LLM_BASE_URL self.model = model or Config.LLM_MODEL_NAME self.provider = provider or Config.LLM_PROVIDER if not self.api_key: raise ValueError("LLM_API_KEY 未配置") self.client = OpenAI( api_key=self.api_key, base_url=self.base_url ) # 判断是否需要强制清理标签 # 如果是已知的推理模型提供商,总是清理;否则也清理(安全兜底) self._should_strip_think = ( self.provider in PROVIDERS_WITH_THINK_TAGS or True # 总是清理,因为不影响正常输出 ) def _strip_think_tags(self, content: str) -> str: """ 移除响应中的思考内容标签。 某些模型(如DeepSeek Reasoner、Xiaomi MiMo、MiniMax M2.5) 会在响应中包含...标签,需要移除以获得纯净输出。 """ if not content: return content return THINK_TAG_PATTERN.sub('', content).strip() def chat( self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 4096, response_format: Optional[Dict] = None ) -> str: """ 发送聊天请求 Args: messages: 消息列表 temperature: 温度参数 max_tokens: 最大token数 response_format: 响应格式(如JSON模式) Returns: 模型响应文本 """ kwargs = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } if response_format: kwargs["response_format"] = response_format response = self.client.chat.completions.create(**kwargs) content = response.choices[0].message.content or "" # 移除思考内容标签 content = self._strip_think_tags(content) return content def chat_json( self, messages: List[Dict[str, str]], temperature: float = 0.3, max_tokens: int = 4096 ) -> Dict[str, Any]: """ 发送聊天请求并返回JSON Args: messages: 消息列表 temperature: 温度参数 max_tokens: 最大token数 Returns: 解析后的JSON对象 """ response = self.chat( messages=messages, temperature=temperature, max_tokens=max_tokens, response_format={"type": "json_object"} ) # 清理markdown代码块标记 cleaned_response = response.strip() cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response) cleaned_response = cleaned_response.strip() try: return json.loads(cleaned_response) except json.JSONDecodeError: raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}") def get_info(self) -> Dict[str, Any]: """返回当前客户端配置信息(用于日志/调试)""" return { "provider": self.provider or "custom", "base_url": self.base_url, "model": self.model, }