""" llm_client.py — LLM API 适配层(多 Provider 支持) 支持:DeepSeek / OpenAI / 通义千问 / 混元(均支持 OpenAI-compatible 接口) 配置方式:.env 文件或环境变量 LLM_API_KEY=xxx # 必填 LLM_BASE_URL=xxx # API 端点(默认 OpenAI) LLM_MODEL=gpt-4o-mini # 模型名 LLM_PROVIDER=openai # openai / deepseek / qwen / hunyuan """ import json, os, re, hashlib from pathlib import Path from typing import Optional try: from dotenv import load_dotenv except ModuleNotFoundError: # python-dotenv is optional for deployed demos. def load_dotenv(*args, **kwargs): return False # Provider 默认 Base URL PROVIDER_URLS = { "deepseek": "https://api.deepseek.com/v1", "openai": "https://api.openai.com/v1", "qwen": "https://dashscope.aliyuncs.com/compatible-mode/v1", "hunyuan": "https://api.hunyuan.cloud.tencent.com/v1", } class LLMClient: """LLM API 适配器。未配置 API Key 时 available=False,所有方法 fallback。""" def __init__(self) -> None: load_dotenv() self.api_key = os.getenv("LLM_API_KEY") self.base_url = os.getenv("LLM_BASE_URL") self.model = os.getenv("LLM_MODEL", "gpt-4o-mini") provider = os.getenv("LLM_PROVIDER", "openai").lower() self.available = bool(self.api_key) self._client = None # 自动探测 base_url if not self.base_url: self.base_url = PROVIDER_URLS.get(provider, PROVIDER_URLS["openai"]) if self.available: try: from openai import OpenAI self._client = OpenAI(api_key=self.api_key, base_url=self.base_url) except Exception: self.available = False def chat(self, system_prompt: str, user_prompt: str, temperature: float = 0.25) -> Optional[str]: """通用 LLM 调用,返回文本或 None(失败时)。""" if not self.available or self._client is None: return None # 缓存检查 cache_key = f"{hash(system_prompt)}_{hash(user_prompt)}_{temperature}" if cache_key in _RESPONSE_CACHE: return _RESPONSE_CACHE[cache_key] try: resp = self._client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=temperature, ) text = resp.choices[0].message.content _RESPONSE_CACHE[cache_key] = text return text except Exception: return None def chat_json(self, system_prompt: str, user_prompt: str) -> Optional[dict]: """LLM 调用并强制 JSON 输出。返回 dict 或 None。""" text = self.chat(system_prompt, user_prompt, temperature=0.1) if text is None: return None return _extract_json(text) def enhance_matches(self, profile: dict, ranked_jobs: list[dict]) -> list[dict]: if not self.available or self._client is None: return ranked_jobs prompt_template = ( Path(__file__).resolve().parents[1] / "prompts" / "match_analysis.md" ).read_text(encoding="utf-8") enhanced = [] for job in ranked_jobs: prompt = prompt_template.format( profile=json.dumps(profile, ensure_ascii=False), job=json.dumps(job, ensure_ascii=False), ) try: response = self._client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": "你是专业的大模型应用算法求职匹配 Agent。"}, {"role": "user", "content": prompt}, ], temperature=0.25, ) job = dict(job) job["llm_analysis"] = response.choices[0].message.content except Exception as exc: job = dict(job) job["llm_analysis"] = f"LLM 调用失败,已保留规则版诊断:{exc}" enhanced.append(job) return enhanced def _extract_json(text: str) -> Optional[dict]: """从 LLM 输出中提取 JSON(容忍 markdown code block 包裹)。""" # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 尝试 ```json ... ``` 包裹 m = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) if m: try: return json.loads(m.group(1).strip()) except json.JSONDecodeError: pass # 尝试找到第一个 { 到最后一个 } start = text.find("{") end = text.rfind("}") if start >= 0 and end > start: try: return json.loads(text[start:end + 1]) except json.JSONDecodeError: pass return None