青玉案
d0e66a1dc3
feat: 新闻检索为空时在报告中如实标注 (#2229)
* feat: 新闻检索为空时在报告中如实标注
消息面章节此前是「有内容才渲染」,检索一条没拿到时整段直接消失,
读报告的人无从判断是确实没新闻,还是检索静默失败了(搜索源限流、
未配置可用渠道等)。这把「抓取失败」呈现成了「确实没有新闻」。
- src/analyzer.py: AnalysisResult 新增 news_result_count,默认 None
- src/core/pipeline.py: 把 Step 4 已算好的计数交给结果对象
(此前只进了 diagnostic context snapshot,报告层拿不到)
- src/notification.py: news_lines 为空且计数为 0 时,渲染明确提示,
并说明结论未纳入新闻维度证据
- tests: 新增 5 条用例,含两条负例——计数为 None 时不得报警
(那是未配置搜索渠道,不是失败)、拿到新闻时行为与改动前一致
不触碰任何检索路径,纯展示层增量。
* fix: 把新闻缺失提示放进真实渲染路径,并独立于模型输出判定
按 review 三条意见修正:
P1-1 提示只存在于 generate_daily_report,而正常流程从不调用它——
_send_single_stock_notification 与聚合报告走的是 dashboard / brief /
single_stock。原实现对所有标准 REPORT_TYPE 都不生效。
改为抽出共享判定 _empty_news_disclosure,四个渲染器统一接入。
P2 检索零命中但模型按 schema 写出了 market_sentiment / hot_topics 时,
原 elif 分支被跳过,报告会展示模型生成的情绪判断却隐瞒无新闻证据。
改为独立判定 news_result_count == 0,与模型是否产出文字无关。
P1-2 补 docs/CHANGELOG.md [Unreleased] 条目,并在
docs/data-source-stability.md 的「用户可见提示建议」一节记录该行为,
含 None / 0 / >0 三态语义表。
测试从 5 条增至 10 条,新增覆盖 dashboard、brief、single_stock 三个真实
渲染器,以及「模型有输出但检索为空」这一最糟组合。39 passed
* fix: 把新闻零命中披露覆盖到模板链路与企业微信入口
按 review 指出的 blocker 修正。此前只接了字符串拼接分支,遗漏两类活路径:
1. REPORT_RENDERER_ENABLED=true 时,generate_dashboard_report /
generate_brief_report / generate_wechat_dashboard 会先 return render(...),
模板链路一路不渲染披露;
2. generate_wechat_dashboard 的非模板 fallback 从未接入,而 pipeline 在
企业微信非 brief 场景会直接调用它。
后果是同一份分析结果在部分渠道披露、在另一些渠道沉默。
改法不再逐点打补丁,而是抽出单一事实来源:
- 新增 src/services/empty_news.py 持有判定与中英文案
- src/notification.py 的 _empty_news_disclosure 改为委托该模块
- src/services/report_renderer.py 为每条结果预计算 empty_news_disclosure,
三个平台模板共用
- templates/report_markdown.j2 / report_brief.j2 / report_wechat.j2 各加渲染分支
- generate_wechat_dashboard 的 fallback 正文接入披露
新增 6 条回归测试:模板链路三个平台各一条、企业微信入口一条,
外加两条负例(未执行检索时模板与企业微信均不得提示)。
本文件测试 10 → 16 全过;全量 5824 passed,9 个既有失败与本 PR 无关
(干净 main 上同样失败,属测试顺序依赖)。
* fix: 修正计数源头的两处缺口(自查发现)
按 review 的 merge-base..HEAD 方法自查全链路,发现此前几轮都只盯着渲染出口,
从未核对计数源头,而源头本身在两条路径上是错的:
1. src/core/pipeline.py: news_result_count 只在 intel_results 非空时赋值,
搜索服务整体失败(正是所有搜索源限流全挂的场景)时停留在 None,
语义为「未执行检索」,于是本 PR 想解决的头号场景反而不提示。
改为检索一发起即置 0。
2. _analyze_with_agent: Agent 模式自行调用 search_stock_news 完成检索,
却从不回写计数,该路径下零命中永远静默。改为按检索结果回写 0 或实际条数。
渲染层再周全,源头数据不对则全部落空。
新增 2 条测试锁住这两处语义(18 passed,此前 16)。
全量 5826 passed,9 个既有失败与本 PR 无关。
* fix: disclose missing news search configuration
* chore: remove unrelated agent guidance
* test: run all empty news tests directly
* fix: preserve empty news disclosure across reports
* fix: 让 Agent 模式的新闻披露跟随实际消费的证据
原问题:agent_arch=multi 等受支持的 Agent 配置下,报告可能声称「未纳入新闻
面证据」而分析其实用了新闻,或反过来该提示而不提示。
根因:news_result_count 取自 executor.run() 结束后为持久化情报补打的一次
search_stock_news()。真实情报由 IntelAgent 通过 search_comprehensive_intel
取得,两者不等价,因此披露与真实证据链可能相反。
修复点:新增 src/agent/news_evidence.py,以运行期证据作用域收集 Agent 搜索
工具的真实返回条数;搜索渠道不可用为 None(未执行检索),可用则从 0 起步、
拿到多少算多少。pipeline 在 executor.run() 前后开启并读取该作用域,事后的
持久化补查不再回写计数。
回归风险:工具在 ThreadPoolExecutor 中执行,runner.py 以
contextvars.copy_context() 提交,故作用域中必须是可变累加器对象,换成不可变
值会让父线程读不到;已加回归测试锁住该机制。原 test_agent_path_records_count
断言的正是被修复的错误行为,已替换为反向断言。
Refs #2225
* fix: 让新闻披露以实际证据为准而非搜索命中数
原问题:本地已落库的资讯池或社交情绪进入 news_context 参与分析后,报告仍可能
声称「未配置搜索渠道,本次分析未纳入新闻面证据」或「零命中」。
根因:news_context 由三路来源拼成——实时检索、社交情绪(美股)、本地资讯池,
但只有实时检索会更新 news_result_count。披露断言的是「结论有没有用到新闻面
证据」,而计数只是「搜索命中了几条」,两者是不同命题,后两路参与时必然失真。
修复点:AnalysisResult 新增 news_evidence_present,由 news_context 是否非空
得出,pipeline 两条路径共用 src/services/empty_news.news_evidence_present()
这一个判定函数。披露改为先看有无证据;确无证据时才用计数解释原因
(None=未配置渠道,0=检索零命中)。历史重建同步恢复该字段。
回归风险:旧记录没有该字段,按计数回退推断,与该记录当时的报告表现一致,不会
追溯改变旧报告;已有用例锁住。review 只点名了本地资讯池,社交情绪属同一缺陷类,
本次一并修复并加测试。另加源码断言:任一 pipeline 路径改回只传计数即失败。
Refs #2225
* fix: 按来源登记新闻证据,不让零命中占位文本冒充证据
原问题:普通分析链路在「搜索已执行但一条证据都没拿到」时,报告不再显示零命中
披露——正是本 PR 要修的核心场景,反而比改动前更差。
根因:src/search_service.py 的 format_intel_report() 即使所有维度失败或为空,
也会输出「【XX 情报搜索结果】」标题和每个维度的「未找到相关信息」占位文本,
整段永远非空。上一版把拼好的 news_context 整段交给 news_evidence_present()
判定,于是 news_result_count == 0 时 evidence 被翻成 true,披露被吞掉,错误
状态还会经 to_dict() 持久化,继续影响历史、详情 API 与 Web。
修复点:判定改为按来源逐个登记——实时检索的真实命中数、社交情绪内容、本地
资讯池内容,任一为真才算有证据;两条 pipeline 路径都不再传拼好的整段。
news_evidence_present() 的契约随之改为接收各来源,并在文档串里写明为什么不能
传整段。
回归风险:新增反例用真实的 format_intel_report() 产出占位文本(不用 mock),
断言其不得被判成证据、且报告必须出现零命中披露。另有源码断言:谁把整段
news_context 交回判定函数即失败。上一版两条测试实际在保护该缺陷(一条名为
「任何非空 context 都算证据」,一条要求必须传入 news_context),已一并纠正。
Refs #2225
---------
Co-authored-by: Mach-Chan <zz-b240@zz-b240deMacBook-Air.local>
2026-08-22 21:27:04 +08:00
..
2026-08-17 21:41:19 +08:00
2026-05-31 19:45:12 +08:00
2026-02-02 22:04:01 +08:00
2026-06-14 18:47:41 +08:00
2026-06-17 18:46:01 +08:00
2026-04-02 23:05:08 +08:00
2026-07-03 19:53:31 +08:00
2026-07-19 16:31:54 +08:00
2026-07-19 16:31:54 +08:00
2026-08-05 23:12:42 +08:00
2026-08-05 23:12:42 +08:00
2026-08-05 23:12:42 +08:00
2026-07-19 16:31:54 +08:00
2026-05-21 22:40:08 +08:00
2026-06-30 22:33:02 +08:00
2026-08-05 23:12:42 +08:00
2026-07-19 19:46:14 +08:00
2026-07-22 23:44:59 +08:00
2026-07-08 22:12:24 +08:00
2026-04-03 22:09:32 +08:00
2026-07-03 19:37:51 +08:00
2026-07-19 16:31:54 +08:00
2026-07-25 20:34:35 +08:00
2026-05-27 21:18:23 +08:00
2026-05-22 22:42:05 +08:00
2026-06-28 20:23:02 +08:00
2026-06-19 09:43:40 +08:00
2026-06-04 20:30:38 +08:00
2026-05-17 21:24:34 +08:00
2026-08-22 21:27:04 +08:00
2026-06-04 09:17:39 +08:00
2026-06-04 20:30:38 +08:00
2026-06-13 10:33:04 +08:00
2026-06-01 20:30:02 +08:00
2026-05-31 23:10:30 +08:00
2026-08-22 21:27:04 +08:00
2026-06-03 19:51:07 +08:00
2026-03-20 11:35:00 +08:00
2026-07-03 20:24:13 +08:00
2026-08-05 19:14:40 +08:00
2026-06-05 21:26:06 +08:00
2026-06-05 21:26:06 +08:00
2026-06-05 21:26:06 +08:00
2026-07-25 22:14:15 +08:00
2026-04-03 13:15:27 +08:00
2026-08-01 14:33:38 +08:00
2026-03-17 21:35:17 +08:00
2026-03-13 23:15:40 +08:00
2026-05-17 22:59:49 +08:00
2026-06-18 21:23:11 +08:00
2026-07-30 22:12:57 +08:00
2026-02-08 14:49:50 +08:00
2026-06-14 17:57:16 +08:00
2026-05-08 20:32:26 +08:00
2026-06-09 01:08:55 +08:00
2026-05-02 17:03:47 +08:00
2026-06-14 17:55:52 +08:00
2026-08-02 18:32:30 +08:00
2026-07-19 16:31:54 +08:00
2026-06-03 22:36:50 +08:00
2026-06-14 17:57:16 +08:00
2026-05-20 23:32:14 +08:00
2026-08-05 22:33:50 +08:00
2026-08-05 22:33:50 +08:00
2026-07-19 16:31:54 +08:00
2026-07-19 16:31:54 +08:00
2026-08-02 16:45:37 +08:00
2026-06-22 20:49:37 +08:00
2026-08-10 21:18:03 +08:00
2026-08-19 22:52:10 +08:00
2026-08-05 23:12:42 +08:00
2026-03-26 23:08:26 +08:00
2026-08-05 19:15:08 +08:00
2026-06-05 08:56:42 +08:00
2026-06-13 10:33:04 +08:00
2026-06-28 18:53:39 +08:00
2026-07-26 20:32:12 +08:00
2026-05-10 23:28:30 +08:00
2026-04-25 10:24:34 +08:00
2026-04-03 13:14:37 +08:00
2026-03-13 16:36:52 +08:00
2026-03-15 20:19:11 +08:00
2026-07-11 14:04:34 +08:00
2026-07-16 19:22:14 +08:00
2026-07-16 19:22:14 +08:00
2026-06-11 22:13:24 +08:00
2026-07-02 18:56:08 +08:00
2026-07-23 19:21:23 +08:00
2026-07-12 11:30:59 +08:00
2026-07-23 19:21:23 +08:00
2026-07-23 19:21:23 +08:00
2026-07-10 22:05:10 +08:00
2026-07-12 11:30:59 +08:00
2026-07-12 16:07:01 +08:00
2026-05-15 20:58:56 +08:00
2026-06-04 13:42:13 +08:00
2026-07-12 18:56:21 +08:00
2026-08-02 16:45:37 +08:00
2026-04-01 11:43:32 +08:00
2026-08-01 14:13:23 +08:00
2026-04-19 22:27:25 +08:00
2026-06-24 21:52:07 +08:00
2026-03-27 21:29:50 +08:00
2026-08-05 19:14:40 +08:00
2026-05-25 20:26:24 +08:00
2026-05-09 19:57:42 +08:00
2026-07-26 20:32:12 +08:00
2026-05-17 21:24:34 +08:00
2026-08-09 19:13:57 +08:00
2026-03-17 21:38:31 +08:00
2026-05-21 20:04:32 +08:00
2026-08-05 22:33:50 +08:00
2026-07-20 22:45:45 +08:00
2026-06-19 13:34:21 +08:00
2026-08-05 19:15:08 +08:00
2026-06-30 22:33:02 +08:00
2026-04-04 10:18:51 +08:00
2026-07-11 14:04:34 +08:00
2026-04-24 22:06:40 +08:00
2026-03-16 22:40:45 +08:00
2026-08-09 13:53:00 +08:00
2026-03-16 00:24:54 +08:00
2026-07-26 16:33:28 +08:00
2026-08-05 19:15:08 +08:00
2026-03-09 19:21:50 +08:00
2026-07-04 15:03:05 +08:00
2026-06-19 09:42:42 +08:00
2026-08-05 19:14:40 +08:00
2026-06-27 22:10:08 +08:00
2026-05-20 20:30:53 +08:00
2026-06-17 18:46:01 +08:00
2026-08-05 19:15:08 +08:00
2026-05-16 21:31:28 +08:00
2026-07-19 18:56:08 +08:00
2026-06-22 22:42:53 +08:00
2026-07-30 22:49:54 +08:00
2026-06-09 01:08:55 +08:00
2026-07-31 19:59:31 +08:00
2026-08-14 22:08:51 +08:00
2026-08-19 23:46:49 +08:00
2026-08-22 21:20:59 +08:00
2026-06-28 20:23:02 +08:00
2026-06-28 20:23:02 +08:00
2026-05-25 20:27:24 +08:00
2026-06-21 14:33:36 +08:00
2026-05-10 15:05:31 +08:00
2026-08-19 21:59:10 +08:00
2026-08-02 16:45:37 +08:00
2026-06-28 18:53:39 +08:00
2026-07-12 11:30:59 +08:00
2026-06-27 22:10:08 +08:00
2026-08-09 21:40:13 +08:00
2026-07-22 23:36:06 +08:00
2026-08-05 23:12:42 +08:00
2026-08-22 21:23:55 +08:00
2026-06-14 18:26:30 +08:00
2026-03-16 22:40:45 +08:00
2026-05-31 19:45:12 +08:00
2026-05-13 19:19:54 +08:00
2026-08-22 21:27:04 +08:00
2026-05-11 07:45:41 +08:00
2026-05-31 19:45:12 +08:00
2026-05-13 19:19:54 +08:00
2026-08-05 19:14:40 +08:00
2026-08-05 19:14:40 +08:00
2026-08-02 16:45:37 +08:00
2026-06-02 23:19:10 +08:00
2026-04-01 11:22:17 +08:00
2026-06-13 10:33:04 +08:00
2026-04-01 11:22:17 +08:00
2026-06-27 23:37:56 +08:00
2026-08-05 19:14:40 +08:00
2026-04-05 10:26:06 +08:00
2026-06-13 10:33:04 +08:00
2026-05-31 21:05:12 +08:00
2026-07-12 13:09:25 +08:00
2026-05-11 07:45:41 +08:00
2026-08-15 21:59:25 +08:00
2026-05-21 19:15:24 +08:00
2026-07-04 12:10:41 +08:00
2026-06-19 23:04:52 +08:00
2026-07-04 12:10:41 +08:00
2026-08-05 19:15:08 +08:00
2026-06-17 18:46:01 +08:00
2026-05-31 21:05:12 +08:00
2026-05-31 21:05:12 +08:00
2026-05-25 20:26:24 +08:00
2026-06-27 22:41:44 +08:00
2026-06-30 19:02:46 +08:00
2026-06-30 19:02:46 +08:00
2026-07-03 20:24:13 +08:00
2026-07-19 18:55:42 +08:00
2026-06-24 20:19:19 +08:00
2026-06-12 20:58:58 +08:00
2026-06-05 23:19:52 +08:00
2026-06-28 17:09:28 +08:00
2026-06-30 19:04:32 +08:00
2026-06-21 22:24:19 +08:00
2026-08-05 19:15:08 +08:00
2026-08-01 14:13:23 +08:00
2026-08-05 19:15:08 +08:00
2026-08-02 18:32:30 +08:00
2026-08-02 18:32:30 +08:00
2026-03-24 20:03:05 +08:00
2026-08-19 21:59:10 +08:00
2026-04-01 11:12:50 +08:00
2026-08-02 18:32:30 +08:00
2026-03-20 11:41:20 +08:00
2026-04-25 10:24:34 +08:00
2026-08-19 21:59:10 +08:00
2026-08-09 21:40:13 +08:00
2026-06-27 22:41:44 +08:00
2026-06-27 22:41:44 +08:00
2026-06-27 22:41:44 +08:00
2026-06-27 22:41:44 +08:00
2026-07-31 19:59:48 +08:00
2026-07-31 19:59:48 +08:00
2026-07-30 22:12:57 +08:00
2026-07-22 21:35:32 +08:00
2026-07-31 19:59:48 +08:00
2026-03-31 22:53:56 +08:00
2026-06-27 17:09:06 +08:00
2026-07-03 20:24:13 +08:00
2026-05-19 23:21:35 +08:00
2026-05-09 21:48:37 +08:00
2026-07-27 21:40:38 +08:00
2026-08-22 21:21:15 +08:00
2026-07-27 21:40:38 +08:00
2026-06-20 10:39:40 +08:00
2026-08-04 19:38:22 +08:00
2026-07-04 11:57:43 +08:00
2026-03-12 20:43:27 +08:00
2026-08-22 21:21:15 +08:00
2026-07-22 23:44:59 +08:00
2026-07-22 23:44:59 +08:00
2026-08-05 19:15:08 +08:00
2026-08-19 21:59:10 +08:00
2026-05-01 13:30:16 +08:00
2026-06-20 10:39:40 +08:00
2026-07-26 20:32:12 +08:00
2026-06-14 18:47:41 +08:00
2026-07-12 18:56:21 +08:00
2026-06-28 17:09:28 +08:00
2026-06-09 01:08:55 +08:00
2026-06-28 17:09:28 +08:00
2026-07-27 21:40:38 +08:00
2026-05-27 21:18:23 +08:00
2026-05-31 09:27:54 +08:00
2026-07-22 20:54:40 +08:00
2026-07-01 19:11:27 +08:00
2026-07-01 19:10:45 +08:00
2026-06-30 19:05:25 +08:00
2026-06-26 21:21:38 +08:00
2026-07-01 19:11:27 +08:00
2026-02-18 11:56:25 +08:00
2026-06-19 09:44:09 +08:00
2026-07-25 22:26:39 +08:00
2026-08-14 22:40:59 +08:00
2026-07-26 11:20:12 +08:00
2026-04-28 22:25:29 +08:00
2026-06-28 17:03:15 +08:00
2026-05-29 23:08:16 +08:00
2026-02-19 11:45:47 +08:00
2026-06-30 19:05:25 +08:00