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:
2026-09-25 11:13:37 +08:00
parent 2eaea2ee81
commit ff911cf6f7
19 changed files with 1024 additions and 200 deletions
+8 -2
View File
@@ -543,9 +543,15 @@ ReportData (Pydantic)
| 文件 | 格式 | 用途 | 热更新 |
|------|------|------|--------|
| `configs/sources.yaml` | YAML | 14 个新闻源配置 | 每次抓取重读 |
| `configs/llm_models.yaml` | YAML | 4 个 LLM 场景配置 | 每次调用重读 |
| `configs/llm_models.yaml` | YAML | 4 个 LLM 场景配置 | 每次调用重读(mtime 缓存失效) |
| `configs/watchlist.yaml` | YAML | cninfo 公告关注列表 | 每次操作重读 |
| `.env` | dotenv | API Key + 调度/超时/DB 配置 | 重启服务生效 |
| `.env` | dotenv | API Key + 调度/超时/DB 配置 | ~2s 内自动生效(常驻进程无需重启) |
> 热加载实现见 `configs/runtime_env.py`:常驻进程(调度器 / MCP server)启动后
> 会以 2s 周期比对 `.env` 的 `(mtime, size)`,变化即写入 `os.environ`;
> `configs/loader.py` 对 YAML 做同样的 mtime 失效。
> 进程环境里**显式设置且与文件不同**的变量优先(如 `LLM_PROVIDER=qwen ...`),
> `.env` 中删除的键也会同步从环境中移除。
### 5.2 配置优先级(LLM 场景)
+21 -3
View File
@@ -507,17 +507,25 @@ tail -f logs/scheduler.log # 文件日志
| 18:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
| 22:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
### 7.4 修改调度时间
### 7.4 修改调度时间(无需重启)
```bash
# 编辑 .env 中的 SCHEDULE_TIMES,格式: HH:MM,HH:MM,...
nano /home/pi/news/.env
# 例: SCHEDULE_TIMES=08:00,14:00,20:00
# 重启生效
sudo systemctl restart a-share-research
# 保存即生效:常驻调度器每 30s 比对一次,自动重新注册定时任务
# 日志确认:grep "定时任务已同步" /home/pi/news/logs/scheduler.log
```
> **配置热加载**:`.env`(模型 / Key / 端点 / 超时 / DB / 调度时间)与
> `configs/llm_models.yaml`(各场景 provider / model / 温度)都是**改完保存即生效**,
> 不需要 `systemctl restart`。调度时间最长 30s 生效,其余配置约 2s 生效。
>
> 只有两种情况需要重启:
> 1. 部署/更新了 Python 代码本身;
> 2. 正在执行中的那一步(子进程)会继续用旧配置跑完,下一步才用新配置。
### 7.5 前台守护模式(调试)
```bash
@@ -703,6 +711,16 @@ uv run a-share stock-report
检查 `.env` 中 `DASHSCOPE_API_KEY` 是否填写。可用 `--provider qwen` 切换到百炼测试。模型名缺失时直接报错,检查 `configs/llm_models.yaml` 中 `event_extraction` 场景的 `model` 字段。
**Q: 日报没有 AI 摘要(ai_summary 为空)?**
先查 `logs/scheduler.log` 是否有 `AI 摘要可能被截断: ... finish_reason=length 实际输出 0 字符`。根因通常是**推理模型的 reasoning token 与正文共用 `max_tokens`**:预算过小时"思考"占满配额,正文一个字都没有。处理办法:
1. 调大 `configs/llm_models.yaml` 中 `daily_report.max_tokens`(默认 4000,YAML 保存即热生效,无需重启);
2. 代码已内置兜底:正文为空时自动加倍预算重试(上限 16000),仍失败才降级为无摘要;
3. 补生成某天摘要:`uv run a-share report --date <YYYYMMDD>`(按 `(report_date, report_type, file_name)` 幂等 upsert,不会新增记录)。
注意:`report` 步骤由调度器**进程内**执行(`scheduler/pipeline.py`),改动 Python 代码后需 `sudo systemctl restart a-share-research` 才会生效;只改 YAML / `.env` 则无需重启。
**Q: Qdrant 搜索不到结果?**
```bash