feat: Token Plan 迁移与 .env 热加载,并修复日报 AI 摘要为空
Token Plan 迁移 / 配置热加载:
- configs/llm_models.yaml: 各场景切到 Token Plan(deepseek-v4.1-flash / qwen3.6-flash)
- 新增 configs/runtime_env.py: .env 按 (mtime_ns, size) 热加载并同步 os.environ,
统一 env_get 取值;llm / embedding / vectorstore / mcp / pipeline 改用 env_get
- configs/loader.py / scripts/run_scheduler.py 等配套调整
- 新增 tests/test_hot_reload.py
日报 AI 摘要为空修复(2026-09-25):
- 根因: 推理模型的 reasoning token 与正文共用 max_tokens, 预算 1500 被"思考"
占满 -> text_tokens=0 / finish_reason=length, 摘要静默为空且不重试
- daily_report 场景新增 max_tokens(默认 4000, YAML 保存即热生效);
LLMConfig 支持可选 max_tokens; 分块预算 800 -> 2000
- _llm_call 拆出 _call_once, 正文为空时自动加倍预算重试(上限 16000),
用尽才降级返回空串; 网络异常重试语义不变
- docs/user-guide.md 新增 FAQ; continuation.md 记录本次排查
- 已重跑 2026-09-25 日报(report_id=357)补回 466 字摘要
测试: 相关用例 56 passed(test_hot_reload 12 passed);
ruff 无新增问题; 3 个 crawler 既有失败与本改动无关
This commit is contained in:
+14
-5
@@ -19,13 +19,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from loguru import logger
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
|
||||
from configs.loader import load_defaults, load_scene_config
|
||||
from configs.runtime_env import env_get
|
||||
|
||||
# 默认基址
|
||||
_DEEPSEEK_DEFAULT_BASE = "https://api.deepseek.com"
|
||||
@@ -53,6 +53,9 @@ class LLMConfig:
|
||||
timeout_sec: float = DEFAULT_TIMEOUT_SEC
|
||||
temperature: float = DEFAULT_TEMPERATURE
|
||||
max_attempts: int = DEFAULT_MAX_ATTEMPTS # 单次任务失败重试次数
|
||||
# 单次输出预算(可选,场景配置 scenes.<scene>.max_tokens);None = 调用方用内置默认。
|
||||
# 注意:推理模型(deepseek-v4.1-flash 等)的 reasoning token 与正文共用该预算。
|
||||
max_tokens: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.api_key:
|
||||
@@ -60,10 +63,8 @@ class LLMConfig:
|
||||
|
||||
|
||||
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()
|
||||
"""读取环境变量(先热加载 .env,改文件后无需重启进程)。"""
|
||||
return env_get(key, default)
|
||||
|
||||
|
||||
def _first_env(keys: list[str | None]) -> str | None:
|
||||
@@ -147,6 +148,7 @@ def load_llm_config(
|
||||
timeout = _pick_float(sc, dflt, "timeout_sec", "LLM_TIMEOUT_SEC", DEFAULT_TIMEOUT_SEC)
|
||||
temperature = _pick_float(sc, dflt, "temperature", "LLM_TEMPERATURE", DEFAULT_TEMPERATURE)
|
||||
max_attempts = _pick_int(sc, "max_attempts", DEFAULT_MAX_ATTEMPTS)
|
||||
max_tokens = _pick_optional_int(sc, "max_tokens")
|
||||
|
||||
return LLMConfig(
|
||||
provider=p,
|
||||
@@ -156,6 +158,7 @@ def load_llm_config(
|
||||
timeout_sec=timeout,
|
||||
temperature=temperature,
|
||||
max_attempts=max_attempts,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
||||
|
||||
@@ -182,6 +185,12 @@ def _pick_int(sc: dict, sc_key: str, default: int) -> int:
|
||||
return int(v) if v is not None else default
|
||||
|
||||
|
||||
def _pick_optional_int(sc: dict, sc_key: str) -> int | None:
|
||||
"""可选整数场景配置;未配置或非法时返回 None(调用方回退各自内置默认)。"""
|
||||
v = _num(sc.get(sc_key))
|
||||
return int(v) if v is not None else None
|
||||
|
||||
|
||||
def make_sync_client(config: LLMConfig) -> OpenAI:
|
||||
"""构造同步 OpenAI 客户端(指向 DeepSeek/Qwen 兼容端点)。"""
|
||||
logger.debug(
|
||||
|
||||
Reference in New Issue
Block a user