Files
ephron-ren-prd/prd-ai-daily-cross-day-dedup.md

174 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI日报跨天去重修复 PRD
**日期:** 2026-06-08
**状态:** 草稿
**相关仓库:** [`ephron_ren/ai-daily-report`](https://gitea.ephron.ren/ephron_ren/ai-daily-report)
---
## 1. 问题陈述
AI日报 pipeline 的重复新闻问题,经排查由三个缺陷叠加导致:
### 1.1 无跨天去重(主因,影响面 ~50%
pipeline 是纯 daily function每次运行从零开始采集 → 当天内去重 → 改写 → 发布。没有任何机制检查某条新闻的 URL 或内容是否已在之前某天的日报中发过。
**证据:** 对比 6/7 和 6/8 的发布内容,约 50-60% 的条目重复。这些重复几乎全部来自 InfoQ RSS每天固定 15 条)和 MIT Tech Review RSS每天固定 10 条),它们的 feed 会持续包含多天前的文章。URL 完全相同(`canonicalize_url` 已剥离 tracking params但因为跨天没有数据可对比。
### 1.2 同天内 title 相似度阈值过于严格(次因,影响面 2-5 条/天)
`dedupe.py` 中的 `_possible_duplicates()` 使用 `difflib.SequenceMatcher` 阈值 0.65 判断标题是否相似。对于中英文混排的标题,这个阈值过高,导致同一天内不同来源报道同一新闻时无法被识别为候选重复。
**例子6/8**
- Item 49「OpenAI定制芯片核心成员Clive Chan跳槽至Anthropic」来源 X
- Item 50「OpenAI芯片核心叛逃Anthropic就在量产前夜」来源 量子位)
- → 同一件事normalize 后阈值约 0.55-0.60,未被 Stage 2 标记为 possible duplicateStage 3 语义去重也看不到。
### 1.3 AI HOT 与 RSS 同源(影响有限)
AI HOT 聚合的内容与 pipeline 单独抓取的 RSS 有重叠。同 URL 的情况已被当天 URL 去重兜底(`_2` 后缀),但同一事件不同 URL 的文章无法覆盖。
---
## 2. 目标
1. **消除跨天重复**:同一 URL 的新闻在发布过后,后续日报不再收录
2. **提升同天去重召回率**:修复 title 相似度阈值,让 Stage 3 语义去重能覆盖更多跨来源重复
3. **零误杀**:去重机制只能移除真正重复的内容,不能因为标题或 URL 相似而误删独立新闻
---
## 3. 方案设计
### 3.1 跨天去重Stage 2.5 — 新增 stage
在 Stage 2硬去重和 Stage 3语义去重之间插入一个新的 **Stage 2.5:跨天去重**
**机制:** 维护一个历史 URL 集合文件(`~/.hermes/scripts/ai_morning_out/published_urls.json`),每次 publish 后追加当天所有已发布 items 的 `canonical_url`。每次运行 Stage 2.5 时加载该文件,过滤掉 `canonical_url` 已存在于历史集合中的 items。
**文件格式(`published_urls.json`**
```json
{
"version": 1,
"urls": {
"https://infoq.com/news/2026/06/google-litertlm-gemma4": {
"first_seen": "2026-06-07",
"last_published": "2026-06-07",
"titles": ["Google LiteRT-LM 利用 Gemma 4 多令牌预测实现 2.2 倍推理加速"]
},
...
},
"updated_at": "2026-06-08T10:02:00+08:00"
}
```
**设计要点:**
- URL 作为 dedup key`canonical_url`),和 Stage 2 的 URL 去重逻辑一致
- 只记录**已发布**的 items即跑完 Stage 7 assemble 后,最终出现在 blog_markdown.md 中的 items
- 写入时机Stage 8 publish 成功后追加到历史文件
- 不对标题做跨天语义去重(防误杀),仅对 URL
- 配置化:可通过 `sources.json``pipeline.json` 控制「跨天去重窗口」,默认 7 天
- 空 canonical_url 的 item 跳过跨天去重
**为什么只对 URL 不对 title 做跨天?** 同一事件在不同天可能有不同角度的报道(例如第一天简短报道发布,第二天深度分析),不同 URL 说明是不同文章,不应视为重复。
### 3.2 标题相似度阈值下调
**改动:`dedupe.py` → `_possible_duplicates()`**
将 title 相似度阈值从 `0.65` 下调至 **`0.50`**,并针对中英文混排场景做优化:
```python
# 旧
ratio >= 0.65
# 新
ratio >= 0.50
```
**原因:** 下调后更多候选对进入 Stage 3 语义去重,由 LLM 判断是否真的重复。Stage 3 已有 `max_deletion_ratio=0.5` 的安全阀,不会误删过多。
同时考虑增加一种基于**共享 token 比例**的辅助判断(不必完全替换 edit distance
- 计算两个 normalize 后标题的 token 交集大小/并集大小Jaccard 相似度)
- 如果 Jaccard ≥ 0.4 且 edit distance ≥ 0.4,则标记为 possible duplicate
### 3.3 rss.py 增加 published_at 过滤
**改动:`sources/rss.py` → `fetch_rss()`**
对于 RSS 源,利用 `<pubDate>` 字段过滤掉发布日期超过 `N` 天的条目:
```python
CROSS_DAY_FILTER_DAYS = {
"InfoQ AI": 3, # 只保留 3 天内发布的 InfoQ 文章
"MIT科技评论AI": 5,
# 量子位、橘鸦等不需要过滤,因为它们的 feed 本身已经当天
}
```
在 RSS fetcher 解析 `<pubDate>` 后,如果 `published_at` 距今超过配置的天数,直接丢弃。这能**从源头上减少** InfoQ/MIT 的陈旧条目进入 pipeline。
**注意:** 这个过滤是跨天去重的补充优化,不是替代。即使 RSS 过滤后仍有少量陈旧条目Stage 2.5 的 URL 去重会兜底。
---
## 4. 实现计划
### Phase 1Stage 2.5 跨天去重
| 步骤 | 文件 | 改动 |
|---|---|---|
| 1.1 | `ai_daily_report/models.py` | 新增 `PublishedUrls` 数据类version, urls, updated_at |
| 1.2 | `ai_daily_report/dedupe.py` | 新增 `cross_day_dedup_items(items, published_urls, max_age_days=7)` 函数 |
| 1.3 | `ai_daily_report/pipeline.py` | 修改 `run_stage0_to_stage2()` 接受 `published_urls` 参数;新增 `run_stage0_to_stage2_5()`(含 stage 2.5 |
| 1.4 | `ai_daily_report/publish.py` | 修改 `publish_markdown()` 返回已发布的 items 列表;新增 `update_published_urls()` 写入历史文件 |
| 1.5 | `ai_daily_report/runner.py` | 加载 `published_urls.json` 传入 pipelinepublish 成功后更新 |
| 1.6 | 新文件 | `published_urls.json` 初始为空文件 |
| 1.7 | `config/pipeline.json` | 新增 `cross_day_dedup` 配置段(`enabled`, `max_age_days`, `history_path` |
### Phase 2标题相似度优化
| 步骤 | 文件 | 改动 |
|---|---|---|
| 2.1 | `ai_daily_report/dedupe.py` | 将 `_possible_duplicates()` 的阈值从 0.65 改为 0.50;增加 Jaccard 相似度辅助判断 |
| 2.2 | 测试 | 更新 `_possible_duplicates` 的单元测试阈值 |
### Phase 3RSS 源过期过滤
| 步骤 | 文件 | 改动 |
|---|---|---|
| 3.1 | `ai_daily_report/sources/rss.py` | 在 `fetch_rss()` 中按 `published_at` 过滤,丢弃超期条目 |
| 3.2 | `config/sources.json` | 为 InfoQ、MIT 等 RSS 源增加 `max_item_age_days` 字段 |
---
## 5. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 跨天去重误杀不同 URL 的同事件新闻 | 只对 URL 做 key不碰 title。不同 URL 说明是不同的文章,保留。 |
| title 阈值下调后 Stage 3 误删 | `max_deletion_ratio=0.5` 安全阀保底LLM 判断语义重复而非关键词匹配 |
| `published_urls.json` 文件损坏 | JSON 解析失败时回退到无跨天去重log warning不影响当天发布 |
| 历史文件无限增长 | 默认保留 7 天窗口,超过自动清理;或限制文件大小上限 |
| 跨天去重后某天稿件太少 | 不改变。宁可少发也不发重复内容。 |
---
## 6. 验收标准
1. **6/7 的重复文章(如 Google LiteRT-LM、Claude Code Dynamic Workflows、Dropbox Nova 等)在 6/9 的日报中不应出现**
2. **同一天内同一新闻的跨来源报道(如 量子位 + X + TechCrunch 报道同一事件)应在 Stage 3 语义去重中被识别并去重**
3. **`published_urls.json` 在 publish 成功后正确更新,内容格式符合预期**
4. **RSS 源中超过配置天数的文章不再出现在采集结果中**
5. **所有现有测试用例仍能通过**
6. **手动运行一次全流程dry-run 或 draft 模式)验证去重效果**
---
## 7. 未纳入范围
- **跨天语义去重**:不比较不同天 items 的标题/摘要相似度,仅用 URL 去重。防止误判。
- **增量采集**不改变采集方式。RSS feed 全量拉取,在后面 stage 中过滤。
- **历史数据回填**:不追溯清理已有的已发布日报,只影响从部署日开始的运行。