- llm/client: 移除内置默认模型兜底(deepseek-chat/qwen-plus), 模型必须显式配置否则报错 - reporter._llm_call: 指数退避重试(LLM_RETRY_TIMES 默认3 / LLM_RETRY_BACKOFF_SEC 默认2s) - pipeline report: report_date 改为当天(原昨天+回溯3天) - reporter._collect_news_events: 读当天+前一天目录, publish_time 30h 回溯(NEWS_LOOKBACK_HOURS=30), 统一时区 - reporter._collect_xwlb: 固定取 day_str 前一日(已播出联播), source_date 同步 - 公告/调研/互动保持近15日设置(CNINFO_DAYS_BACK), 不受 30h 影响 - 测试: 新增 30h回溯/带时区/重试/模型缺失/xwlb 前一日 用例
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""LLM 客户端抽象与工厂。
|
|
|
|
支持 DeepSeek 和 Qwen(百炼),两者均为 OpenAI 兼容接口,共用 openai SDK。
|
|
|
|
环境变量:
|
|
LLM_PROVIDER = deepseek | qwen (默认 deepseek)
|
|
DeepSeek: DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL / DEEPSEEK_MODEL
|
|
Qwen: QWEN_API_KEY / QWEN_BASE_URL / QWEN_MODEL
|
|
(QWEN_API_KEY -> DASHSCOPE_API_KEY 兜底)
|
|
模型必须显式配置(provider 对应的 *_MODEL 或 LLM_MODEL),不再提供内置默认模型。
|
|
LLM_TEMPERATURE / LLM_TIMEOUT_SEC
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
from loguru import logger
|
|
from openai import AsyncOpenAI, OpenAI
|
|
|
|
# 默认基址
|
|
_DEEPSEEK_DEFAULT_BASE = "https://api.deepseek.com"
|
|
_QWEN_DEFAULT_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
|
|
# 抽取任务默认参数
|
|
DEFAULT_TIMEOUT_SEC = 60.0
|
|
DEFAULT_TEMPERATURE = 0.1
|
|
|
|
|
|
@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 = DEFAULT_TIMEOUT_SEC
|
|
temperature: float = DEFAULT_TEMPERATURE
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.api_key:
|
|
raise ValueError(f"LLM provider={self.provider} 的 API key 为空")
|
|
|
|
|
|
def _read_env(key: str, default: str | None = None) -> str | None:
|
|
val = os.environ.get(key)
|
|
if val is None or val.strip() == "":
|
|
return default
|
|
return val.strip()
|
|
|
|
|
|
def load_llm_config(
|
|
provider: str | None = None,
|
|
*,
|
|
model: str | None = None,
|
|
) -> LLMConfig:
|
|
"""根据环境变量构造 LLMConfig。
|
|
|
|
provider 为 None 时读 LLM_PROVIDER 环境变量,默认 deepseek。
|
|
model 为 None 时读 LLM_MODEL 或 provider 默认。
|
|
"""
|
|
p = (provider or _read_env("LLM_PROVIDER", "deepseek") or "deepseek").lower()
|
|
|
|
if p == "deepseek":
|
|
api_key = _read_env("DEEPSEEK_API_KEY") or ""
|
|
base = _read_env("DEEPSEEK_BASE_URL", _DEEPSEEK_DEFAULT_BASE) or _DEEPSEEK_DEFAULT_BASE
|
|
# DEEPSEEK_MODEL → LLM_MODEL;模型必须显式配置,不提供内置默认
|
|
m = model or _read_env("DEEPSEEK_MODEL") or _read_env("LLM_MODEL")
|
|
if not m:
|
|
raise ValueError("未配置 LLM 模型: 请设置 DEEPSEEK_MODEL 或 LLM_MODEL")
|
|
elif p in ("qwen", "dashscope"):
|
|
api_key = _read_env("QWEN_API_KEY") or _read_env("DASHSCOPE_API_KEY") or ""
|
|
base = _read_env("QWEN_BASE_URL", _QWEN_DEFAULT_BASE) or _QWEN_DEFAULT_BASE
|
|
# QWEN_MODEL → LLM_MODEL;模型必须显式配置,不提供内置默认
|
|
m = model or _read_env("QWEN_MODEL") or _read_env("LLM_MODEL")
|
|
if not m:
|
|
raise ValueError("未配置 LLM 模型: 请设置 QWEN_MODEL 或 LLM_MODEL")
|
|
p = "qwen" # 内部统一用 qwen
|
|
else:
|
|
raise ValueError(f"未知 LLM provider: {p!r},仅支持 deepseek / qwen")
|
|
|
|
timeout = float(_read_env("LLM_TIMEOUT_SEC", str(DEFAULT_TIMEOUT_SEC)) or DEFAULT_TIMEOUT_SEC)
|
|
temperature = float(_read_env("LLM_TEMPERATURE", str(DEFAULT_TEMPERATURE)) or DEFAULT_TEMPERATURE)
|
|
|
|
return LLMConfig(
|
|
provider=p,
|
|
model=m,
|
|
api_key=api_key,
|
|
base_url=base,
|
|
timeout_sec=timeout,
|
|
temperature=temperature,
|
|
)
|
|
|
|
|
|
def make_sync_client(config: LLMConfig) -> OpenAI:
|
|
"""构造同步 OpenAI 客户端(指向 DeepSeek/Qwen 兼容端点)。"""
|
|
logger.debug(
|
|
"初始化同步 LLM 客户端: provider={} model={} base_url={}",
|
|
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.debug(
|
|
"初始化异步 LLM 客户端: provider={} model={} base_url={}",
|
|
config.provider, config.model, config.base_url,
|
|
)
|
|
return AsyncOpenAI(
|
|
api_key=config.api_key,
|
|
base_url=config.base_url,
|
|
timeout=config.timeout_sec,
|
|
)
|