- system.yaml: llm.max_retries(死配置) 改为 llm.max_attempts;embedding 段新增 max_attempts - llm/client.py: LLMConfig 新增 max_attempts(默认 3 兜底),load_llm_config 读取配置 - llm/extractor.py: translate_and_extract(_async) max_attempts=None 时取 config.max_attempts - embedding/client.py: load_embedding_config 读取 embedding.max_attempts - scheduler/reporter.py: _call_llm_simple max_retries=None 时取 max_attempts-1 - 新增 2 个配置读取测试;已同步 pi5 验证
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""LLM 客户端抽象与工厂。
|
|
|
|
支持 DeepSeek 和 Qwen(百炼),两者均为 OpenAI 兼容接口,共用 openai SDK。
|
|
|
|
配置来源:
|
|
- .env → API Key / Base URL(密钥和地址)
|
|
- configs/system.yaml → provider / model / timeout / temperature(功能配置)
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from openai import AsyncOpenAI, OpenAI
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Provider 默认基址
|
|
_DEEPSEEK_DEFAULT_BASE = "https://api.deepseek.com"
|
|
_QWEN_DEFAULT_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
|
|
# 默认模型
|
|
_DEEPSEEK_DEFAULT_MODEL = "deepseek-chat"
|
|
_QWEN_DEFAULT_MODEL = "qwen-plus"
|
|
|
|
# 默认单次调用总尝试次数(含首次;system.yaml llm.max_attempts 未配置时兜底)
|
|
_DEFAULT_MAX_ATTEMPTS = 3
|
|
|
|
|
|
def _load_system_config() -> dict:
|
|
"""加载 configs/system.yaml 中 llm 段配置。"""
|
|
config_path = Path("configs/system.yaml")
|
|
if config_path.exists():
|
|
try:
|
|
with open(config_path, encoding="utf-8") as f:
|
|
raw = yaml.safe_load(f)
|
|
return raw.get("llm", {})
|
|
except Exception:
|
|
logger.warning("加载 llm 配置失败,使用空配置")
|
|
return {}
|
|
|
|
|
|
@dataclass
|
|
class LLMConfig:
|
|
"""LLM 调用配置(provider / model / api_key / base_url / 参数)。"""
|
|
|
|
provider: str # "deepseek" / "qwen"
|
|
model: str
|
|
api_key: str
|
|
base_url: str
|
|
timeout_sec: float = 60.0
|
|
temperature: float = 0.1
|
|
max_tokens: int = 8192
|
|
max_attempts: int = _DEFAULT_MAX_ATTEMPTS # 单次调用总尝试次数(含首次)
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.api_key:
|
|
raise ValueError(f"LLM provider={self.provider} 的 API key 为空")
|
|
|
|
|
|
def load_llm_config(
|
|
provider: str | None = None,
|
|
*,
|
|
model: str | None = None,
|
|
) -> LLMConfig:
|
|
"""根据配置文件构造 LLMConfig。
|
|
|
|
provider 为 None 时读 system.yaml llm.provider,默认 deepseek。
|
|
model 为 None 时读 system.yaml 中对应 provider 的 model。
|
|
|
|
Raises:
|
|
ValueError: API key 未配置
|
|
"""
|
|
config = _load_system_config()
|
|
p = (provider or config.get("provider", "deepseek")).lower()
|
|
|
|
if p == "deepseek":
|
|
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
|
base = os.environ.get("DEEPSEEK_BASE_URL", _DEEPSEEK_DEFAULT_BASE)
|
|
m = model or config.get("deepseek_model", _DEEPSEEK_DEFAULT_MODEL)
|
|
elif p in ("qwen", "dashscope"):
|
|
api_key = os.environ.get("QWEN_API_KEY") or os.environ.get("DASHSCOPE_API_KEY") or ""
|
|
base = os.environ.get("QWEN_BASE_URL", _QWEN_DEFAULT_BASE)
|
|
m = model or config.get("qwen_model", _QWEN_DEFAULT_MODEL)
|
|
p = "qwen"
|
|
else:
|
|
raise ValueError(f"未知 LLM provider: {p!r},仅支持 deepseek / qwen")
|
|
|
|
if not api_key:
|
|
raise ValueError(
|
|
f"LLM provider={p} 的 API key 未配置,请检查 .env 中的 "
|
|
f"{'DEEPSEEK_API_KEY' if p == 'deepseek' else 'QWEN_API_KEY'}"
|
|
)
|
|
|
|
timeout = float(config.get("timeout_sec", 60.0))
|
|
temperature = float(config.get("temperature", 0.1))
|
|
max_tokens = int(config.get("max_tokens", 8192))
|
|
max_attempts = int(config.get("max_attempts", _DEFAULT_MAX_ATTEMPTS))
|
|
|
|
return LLMConfig(
|
|
provider=p,
|
|
model=m,
|
|
api_key=api_key,
|
|
base_url=base,
|
|
timeout_sec=timeout,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
max_attempts=max_attempts,
|
|
)
|
|
|
|
|
|
def make_sync_client(config: LLMConfig) -> OpenAI:
|
|
"""构造同步 OpenAI 客户端(指向 DeepSeek/Qwen 兼容端点)。"""
|
|
logger.info(
|
|
"初始化同步 LLM 客户端: provider=%s model=%s base_url=%s",
|
|
config.provider, config.model, config.base_url,
|
|
)
|
|
return OpenAI(
|
|
api_key=config.api_key,
|
|
base_url=config.base_url,
|
|
timeout=config.timeout_sec,
|
|
)
|
|
|
|
|
|
def make_async_client(config: LLMConfig) -> AsyncOpenAI:
|
|
"""构造异步 OpenAI 客户端(用于批处理高并发)。"""
|
|
logger.info(
|
|
"初始化异步 LLM 客户端: provider=%s model=%s base_url=%s",
|
|
config.provider, config.model, config.base_url,
|
|
)
|
|
return AsyncOpenAI(
|
|
api_key=config.api_key,
|
|
base_url=config.base_url,
|
|
timeout=config.timeout_sec,
|
|
)
|