Files
diary-news/backend/app/services/llm/providers.py
xiaji bc36a1fc38 feat(commentary): 双 provider 评论 — Angel(Agnes) + 美团大模型(LongCat)
- 新增 articles.commentary_meituan{_status,_model,_error} 4 列 + commentary_engine
- LlmSetting 加 meituan_api_key/base_url/chat_model/interval_sec/enabled/commentary_prompt
- 新 app/services/llm/providers.py 工厂,支持多 provider 客户端
- enrichment 流程改为 commentary_angel + commentary_meituan 并行(asyncio.gather),
  任一 provider 失败不影响另一个
- enrichment_loop 状态判定:任一 provider 状态不是 ok 都视为待 enrich
- alembic 0004_dual_commentary 迁移
- 前端 Feed 卡片 + ArticleDetail 详情页各加一条'美团评论'卡
- AdminLlmSettings 加美团 provider 配置卡(独立 api_key 编辑器,不回显明文)
- LlmSettingOut.meituan_api_key_set (bool) 替代直接回传 key
- 默认 URL https://api.longcat.chat/openai/v1 / 默认模型 LongCat-2.0-Preview
2026-06-12 19:00:00 +08:00

103 lines
3.6 KiB
Python

"""LLM provider 工厂。
历史:全站只用一个 LlmClient(单例)指 Agnes。
现在:支持多个 provider,各自独立 base_url / api_key / model / 节流。
- `get_angel_client(setting)` — Agnes 客户端(原 LlmClient 等价)
- `get_meituan_client(setting)` — 美团大模型客户端(OpenAI 兼容,LongCat)
设计:
- 工厂每次返回新实例(无状态;节流靠 client 内部 Semaphore 自带)
- Provider 不可用(api_key 空)= 返回 None
- `get_provider_commentary_defaults()` 暴露 Angel / 美团 的 temperature / max_tokens / system 差异。
"""
from __future__ import annotations
import logging
from typing import Any
from app.models.llm_setting import LlmSetting
from app.services.llm.client import LlmClient
logger = logging.getLogger("news.llm.providers")
# === Provider 名常量(供 enrichment/前端/日志统一引用)===
PROVIDER_ANGEL = "angel" # Agnes(原 LlmClient 默认端点)
PROVIDER_MEITUAN = "meituan" # 美团大模型(LongCat,OpenAI 兼容)
def get_angel_client(setting: LlmSetting) -> LlmClient:
"""Agnes 客户端 — 与 LlmClient 单例行为完全一致。"""
return LlmClient(
chat_model=setting.chat_model,
image_model=setting.image_model,
interval_sec=setting.interval_sec,
)
def get_meituan_client(setting: LlmSetting) -> LlmClient | None:
"""美团大模型(LongCat)客户端。
配置来源:llm_settings 表里 meituan_* 字段(API key / base_url / model / interval / enabled)。
"""
from app.config import settings as app_settings # 延迟导入,避免循环
api_key = getattr(setting, "meituan_api_key", "") or app_settings.meituan_api_key
base_url = (
getattr(setting, "meituan_base_url", "") or app_settings.meituan_base_url
)
model = (
getattr(setting, "meituan_chat_model", "") or app_settings.meituan_chat_model
)
interval = (
getattr(setting, "meituan_interval_sec", None) or app_settings.meituan_interval_sec
)
if not api_key:
return None
return LlmClient(
base_url=base_url or "https://api.longcat.chat/openai/v1",
api_key=api_key,
chat_model=model or "LongCat-2.0-Preview",
interval_sec=float(interval or 2.0),
)
def get_provider_client(provider: str, setting: LlmSetting) -> LlmClient | None:
"""统一入口:按 provider 名取客户端。不可用时返回 None。"""
if provider == PROVIDER_ANGEL:
c = get_angel_client(setting)
return c if c.is_configured() else None
if provider == PROVIDER_MEITUAN:
return get_meituan_client(setting)
raise ValueError(f"unknown provider: {provider}")
def is_provider_enabled(provider: str, setting: LlmSetting) -> bool:
"""provider 是否启用 + 配置齐全。"""
if not setting.enabled:
return False
if provider == PROVIDER_ANGEL:
return get_provider_client(PROVIDER_ANGEL, setting) is not None
if provider == PROVIDER_MEITUAN:
if not bool(getattr(setting, "meituan_enabled", True)):
return False
return get_provider_client(PROVIDER_MEITUAN, setting) is not None
return False
# === Provider 评论差异(温度 / max_tokens / system)===
# Angel: temperature=0.6, max_tokens=600, system="你是资深新闻评论员。"
# 美团: temperature=0.7, max_tokens=1000, system=None(用户示例无 system 字段)
PROVIDER_COMMENTARY_DEFAULTS: dict[str, dict[str, Any]] = {
PROVIDER_ANGEL: {
"temperature": 0.6,
"max_tokens": 600,
"system": "你是资深新闻评论员。",
},
PROVIDER_MEITUAN: {
"temperature": 0.7,
"max_tokens": 1000,
"system": None,
},
}