feat: AI 模型按场景独立配置(llm_scenes)+ 去重多来源合并

- system.yaml 新增 llm_scenes(translation / daily_report,含用途/方法/模型要求说明)
- load_llm_config(scene=...) 场景覆盖;日报摘要 temperature 0.3 硬编码 → 配置
- M3 去重:唯一篇记录 source_ids(跨源重复合并,首个来源为 source_id)
- source_ids 经翻译透传至 events,日报事件 source 多来源拼接展示(≤3 个)
- 已部署 pi5:merge 实证 10 源合并;日报 report_id=224 正常入库
This commit is contained in:
2026-08-12 09:56:55 +08:00
parent 9aa44e610d
commit c72a5ed13a
13 changed files with 331 additions and 20 deletions
+22 -8
View File
@@ -29,14 +29,26 @@ _QWEN_DEFAULT_MODEL = "qwen-plus"
_DEFAULT_MAX_ATTEMPTS = 3
def _load_system_config() -> dict:
"""加载 configs/system.yaml 中 llm 段配置。"""
def _load_system_config(scene: str | None = None) -> dict:
"""加载 llm 配置;scene 指定时与 llm_scenes.{scene} 合并(场景覆盖默认段)。
Args:
scene: 场景名(translation / daily_report)。未配置该场景时回退 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", {})
base = raw.get("llm", {})
if scene:
scene_cfg = (raw.get("llm_scenes", {}) or {}).get(scene, {})
if not scene_cfg:
logger.warning(
"system.yaml 中不存在 llm_scenes.%s,使用 llm 默认段", scene
)
return {**base, **scene_cfg}
return base
except Exception:
logger.warning("加载 llm 配置失败,使用空配置")
return {}
@@ -64,26 +76,28 @@ def load_llm_config(
provider: str | None = None,
*,
model: str | None = None,
scene: str | None = None,
) -> LLMConfig:
"""根据配置文件构造 LLMConfig。
provider 为 None 时读 system.yaml llm.provider,默认 deepseek。
model 为 None 时读 system.yaml 中对应 provider 的 model。
provider 为 None 时读 system.yaml(或场景段)llm.provider,默认 deepseek。
model 为 None 时优先读场景段的 model 键,其次 system.yaml 对应 provider 的 model。
scene 指定时,llm_scenes.{scene} 覆盖 llm 默认段的各参数(按场景独立配置模型)。
Raises:
ValueError: API key 未配置
"""
config = _load_system_config()
config = _load_system_config(scene=scene)
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)
m = model or config.get("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)
m = model or config.get("model") or config.get("qwen_model", _QWEN_DEFAULT_MODEL)
p = "qwen"
else:
raise ValueError(f"未知 LLM provider: {p!r},仅支持 deepseek / qwen")
+2
View File
@@ -264,6 +264,7 @@ def translate_and_extract(
source_name=article.source_name,
url=article.url,
url_hash=article.url_hash,
source_ids=list(article.source_ids),
title=article.title,
title_zh=output.title_zh,
content_en=article.content,
@@ -332,6 +333,7 @@ async def translate_and_extract_async(
source_name=article.source_name,
url=article.url,
url_hash=article.url_hash,
source_ids=list(article.source_ids),
title=article.title,
title_zh=output.title_zh,
content_en=article.content,
+4
View File
@@ -103,6 +103,10 @@ class EnTranslatedArticle(BaseModel):
source_name: str
url: str
url_hash: str
source_ids: list[str] = Field(
default_factory=list,
description="去重合并后的所有来源 ID(透传自 ProcessedArticle.source_ids)",
)
# ── 双语内容 ──
title: str = "" # 英文原标题
+2 -2
View File
@@ -119,8 +119,8 @@ def translate_all_deduped(
logger.warning("去重目录无文章: data/deduped/%s/uniques/", date_str)
return {"date": date_str, "total": 0, "success": 0, "failed": 0, "elapsed_sec": 0}
# 初始化 LLM 客户端
config = load_llm_config(provider=provider, model=model)
# 初始化 LLM 客户端(translation 场景配置见 system.yaml llm_scenes.translation)
config = load_llm_config(provider=provider, model=model, scene="translation")
client = make_sync_client(config)
template = PromptTemplate()