Files
ephron-ren-prd/prd-ai-daily-semantic-dedupe-recall-fix.md
2026-06-10 11:29:27 +08:00

356 lines
11 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.
# PRDAI 日报语义重复召回修复
## 1. 背景
2026-06-10 发布的 AI 日报出现多条语义上重复或高度重叠的新闻。用户反馈后,对发布页面与本地运行产物进行排查:
- 发布页面:`https://blog.ephron.ren/posts/ai-2026-06-10`
- 本地 Markdown`~/.hermes/scripts/ai_morning_out/2026-06-10/blog_markdown.md`
- 本地运行报告:`~/.hermes/scripts/ai_morning_out/2026-06-10/run_report.json`
## 2. 当前问题
### 2.1 直接现象
6/10 日报中至少存在以下重复/高度重叠样本:
| 编号 | 标题 | 问题类型 |
|---|---|---|
| 1 | Anthropic 发布 Claude Fable 5 与 Claude Mythos 5 | 与 7、8 属于同一模型发布事件链,信息重叠 |
| 7 | Claude Mythos 与 Claude Fable 即将发布消息流出 | 与 1、8 重叠,且“即将发布”在正式发布后应被合并或降级 |
| 8 | Claude Mythos 5 发布,主打更强代码能力 | 与 1 属于同一发布事件的补充报道 |
| 18 | OpenRouter 推出 Advisor 工具 | 与 37 同属 OpenRouter 产品/集成动态,需判断是否合并为一条 OpenRouter 动态 |
| 37 | OpenRouter 发布 Cursor 集成指南 | 与 18 弱相关,至少应进入语义去重/合并审查候选 |
> 18/37 是否最终合并需要 LLM 或规则判断,但它们不应完全绕过去重审查。
### 2.2 运行报告暴露的问题
`run_report.json` 中关键字段:
```json
"stage2": {
"input_count": 50,
"output_count": 50,
"removed_count": 0,
"groups": [],
"possible_duplicates": []
},
"stage3": {
"input_count": 41,
"candidate_group_count": 0,
"removed_count": 0,
"duplicate_groups": [],
"uncertain": [],
"errors": []
}
```
含义:
1. Stage 2 没有召回任何可能重复候选;
2. Stage 3 语义去重完全没有候选可审;
3. 因此当天所有语义重复都不是 LLM 判断错误,而是候选召回阶段失败。
## 3. 根因分析
### 3.1 Stage 3 被 Stage 2 候选完全限制
当前流水线:
```python
# ai_daily_report/pipeline.py
candidates = [
candidate
for candidate in stage2_5_result["reports"]["stage2"].get("possible_duplicates", [])
if set(candidate.get("item_ids", [])).issubset(remaining_ids)
]
semantic_items, stage3_report = semantic_dedup_items(items, candidates, ...)
```
Stage 3 只处理 Stage 2 的 `possible_duplicates`。如果 Stage 2 召回为 0Stage 3 不会主动发现任何语义重复。
### 3.2 Stage 2 召回方式过窄
当前 Stage 2 候选召回只基于标题字符串相似度:
```python
TITLE_SIMILARITY_THRESHOLD = 0.50
TOKEN_JACCARD_THRESHOLD = 0.40
TOKEN_EDIT_DISTANCE_THRESHOLD = 0.40
if ratio >= 0.50 or (ratio >= 0.40 and jaccard >= 0.40):
possible.append(...)
```
问题:
- 对中文标题改写、英文品牌词、爆料/正式发布/媒体解读这种同事件不同表述不敏感;
- 只看标题不看摘要、URL 域名、实体、模型名、公司名;
- 阈值实际代码为 `TOKEN_JACCARD_THRESHOLD = 0.40`,高于技能文档里曾记录的 0.25,召回更保守;
- 只在改写前做召回Stage 4 改写后标题更统一、更容易看出重复,但没有二次复检。
### 3.3 缺少“同事件合并”概念
当前去重只删除“同一新闻”,没有明确处理:
- 爆料 + 正式发布;
- 官方发布 + 媒体补充;
- 同一模型/产品的多个角度;
- 同一公司当天多个弱相关小动态。
因此,系统倾向于保留所有不同 URL即使读者看到的是同一事件的拆碎版本。
## 4. 产品目标
### 4.1 目标
将 AI 日报的去重从“标题相似去重”升级为“候选召回 + 语义审查 + 同事件合并”的两层机制:
1. 提高语义重复候选召回率;
2. 让 Stage 3 审查更多合理候选,而不是 0 候选直接跳过;
3. 对同一事件的爆料、发布、媒体解读进行合并或主次处理;
4. 保持用户偏好:不做精选,不按重要性删新闻,只删除/合并重复信息。
### 4.2 非目标
- 不引入主观新闻精选;
- 不按重要性筛掉独立新闻;
- 不为了减少条数而牺牲覆盖率;
- 不做跨天语义去重,跨天仍以 URL 历史去重为主,避免误删后续进展。
## 5. 需求设计
### 5.1 新增 Stage 2.8:语义候选召回
在 Stage 2.5 之后、Stage 3 之前新增候选召回层:
```text
Stage 0 采集
Stage 1 归一化
Stage 2 硬去重 + 标题候选
Stage 2.5 跨天 URL 去重
Stage 2.8 语义候选召回(新增)
Stage 3 LLM 语义去重/合并
Stage 4 改写
...
```
Stage 2.8 输入Stage 2.5 后剩余 items。
Stage 2.8 输出:候选 pair/group合并进 Stage 3 的 candidates。
### 5.2 候选召回特征
Stage 2.8 应至少使用以下非 LLM 特征:
| 特征 | 用途 |
|---|---|
| 标题字符相似度 | 保留现有能力 |
| 标题 token Jaccard | 保留现有能力,但阈值可调 |
| 摘要 token Jaccard | 识别标题不同但摘要实体重叠的新闻 |
| 公司/产品/模型实体重叠 | 识别 Claude Fable/Mythos、OpenRouter、Cursor 等同主题事件 |
| URL 域名与来源类型 | 区分官方源、媒体源、社交源 |
| 时间/状态词 | 识别“即将发布 / 发布 / 亮相 / 报道称”等事件阶段 |
### 5.3 实体抽取规则(第一版)
先用规则实现,不新增 LLM 调用:
- 英文连续词/型号:`Claude Fable 5``Claude Mythos 5``OpenRouter Advisor``Gemma 4 12B`
- 常见公司/平台词典OpenAI、Anthropic、Google DeepMind、Cohere、OpenRouter、Cursor、Claude、Gemini、Gemma、MiMo
- 中文产品/公司可从标题和摘要中按词典匹配;
- 同一 pair 满足以下任一条件进入候选:
- 共享 2 个以上强实体;
- 共享 1 个强实体,且摘要 Jaccard 超过阈值;
- 同公司 + 同产品家族 + 标题/摘要出现发布阶段词。
### 5.4 候选数量控制
为避免 Stage 3 prompt 过大:
- 每条新闻最多关联 5 个候选;
- 全局候选 pair 默认上限 80
- 候选按分数排序:实体重叠 > 摘要相似 > 标题相似;
- 分数低于阈值的 pair 不进入 Stage 3
- run_report 记录被截断数量。
### 5.5 Stage 3 输出语义增强
Stage 3 prompt 需要区分三类结果:
```json
{
"duplicate_groups": [],
"merge_groups": [],
"not_duplicates": [],
"uncertain": []
}
```
含义:
- `duplicate_groups`:同一新闻,删除重复项;
- `merge_groups`:同一事件不同角度,保留一条主项,并把其他来源作为补充来源/补充要点;
- `not_duplicates`:独立新闻;
- `uncertain`:不确定,默认保留。
### 5.6 同事件合并策略
当 Stage 3 返回 `merge_groups`
- 选择官方源或信息更完整的项作为主项;
- 被合并项不单独出现在日报条目中;
- 主项 summary 增加一两句补充信息;
- 主项链接保留主链接,并在 `duplicate_sources` 或新增 `merged_sources` 中记录补充来源;
- 不改变“只去重不精选”的原则:这是合并重复信息,不是筛选新闻。
## 6. 6/10 样本验收用例
实现后,使用 2026-06-10 的 41 条最终样本回放:
### 6.1 必须进入候选审查
| 样本 | 期望 |
|---|---|
| 1 Anthropic 发布 Claude Fable 5 与 Claude Mythos 5 + 7 Claude Mythos 与 Claude Fable 即将发布消息流出 | 必须进入 Stage 3 候选 |
| 1 Anthropic 发布 Claude Fable 5 与 Claude Mythos 5 + 8 Claude Mythos 5 发布,主打更强代码能力 | 必须进入 Stage 3 候选 |
| 7 Claude Mythos 与 Claude Fable 即将发布消息流出 + 8 Claude Mythos 5 发布,主打更强代码能力 | 必须进入 Stage 3 候选 |
| 18 OpenRouter 推出 Advisor 工具 + 37 OpenRouter 发布 Cursor 集成指南 | 应进入 Stage 3 候选,由 LLM 判断是否合并或保留 |
### 6.2 必须合并或删除
| 样本 | 期望 |
|---|---|
| 7 + 1 | 7 作为爆料消息,在 1 官方发布存在时不应独立成条 |
| 8 + 1 | 8 如果只是 1 的媒体补充,应合并进 1如含独立重要信息也应作为 1 的补充来源而非独立条目 |
### 6.3 必须保留
以下不能因为共享公司/平台词而误删:
| 样本 | 原因 |
|---|---|
| 4 Gemini 3.5 Live Translate + 5 Gemma 4 12B | 同公司但不同模型、不同能力,必须保留 |
| 20 Claude Managed Agents + 35 AgentsView 可为 Claude Fable 5 设置自定义价格 | 都含 Claude但一个是平台功能一个是第三方工具必须保留 |
| 21 Cursor Evals + 27 Cursor 欧洲总部 | 同公司但一个是产品功能,一个是组织扩张,必须保留 |
## 7. 报告与可观测性
`run_report.json` 需新增:
```json
"stage2_8": {
"input_count": 41,
"candidate_pair_count": 12,
"candidate_group_count": 5,
"truncated_count": 0,
"reasons": {
"entity_overlap": 6,
"summary_similarity": 3,
"title_similarity": 3
},
"candidates": [
{
"item_ids": ["...", "..."],
"reason": "entity_overlap",
"score": 0.78,
"shared_entities": ["Claude Mythos 5", "Claude Fable 5"]
}
]
}
```
Stage 3 报告需新增:
```json
"merge_group_count": 1,
"merged_count": 2,
"not_duplicate_count": 8,
"uncertain_count": 0
```
## 8. 配置项
`config/pipeline.json` 增加:
```json
{
"semantic_candidate_recall": {
"enabled": true,
"max_pairs": 80,
"max_pairs_per_item": 5,
"title_similarity_threshold": 0.45,
"title_jaccard_threshold": 0.25,
"summary_jaccard_threshold": 0.18,
"strong_entity_overlap_threshold": 2,
"weak_entity_with_summary_threshold": 0.12
}
}
```
## 9. 实施建议
### 9.1 模块拆分
新增模块:
```text
ai_daily_report/candidate_recall.py
```
建议函数:
```python
def build_semantic_candidates(items: list[NewsItem], config: CandidateRecallConfig) -> tuple[list[dict], dict]:
...
```
### 9.2 与现有 Stage 2 兼容
- 保留 `dedupe.py` 中硬去重逻辑;
- `stage2.possible_duplicates` 继续存在;
- Stage 2.8 合并 Stage 2 的标题候选与新增语义候选,去重后传入 Stage 3
- 不在 Stage 2.8 直接删除任何新闻。
### 9.3 测试
新增测试:
```text
tests/test_stage2_8_candidate_recall.py
tests/test_stage3_merge_groups.py
tests/test_pipeline_semantic_duplicate_regression.py
```
必须覆盖:
- Claude Fable/Mythos 三条样本;
- Google DeepMind 同公司不同模型不得误删;
- Cursor 产品功能与公司扩张不得误删;
- 候选数上限与 report 字段;
- Stage 3 返回 `merge_groups` 后 Markdown 只出现主条目。
## 10. 验收标准
1. 6/10 回放中Stage 3 `candidate_group_count` 不再为 0
2. Claude Fable/Mythos 三条不会全部独立出现在最终日报;
3. Google DeepMind Gemini/Gemma 两条仍独立保留;
4. `run_report.json` 能解释每个候选的召回原因;
5. 新增测试全部通过;
6. 不增加默认 LLM 调用次数,仍使用现有 Stage 3 LLM 调用完成语义判断;
7. 当 Stage 2.8 失败时,流水线应降级为原逻辑并在 report 中记录错误,不阻断发布。
## 11. 风险与防护
| 风险 | 防护 |
|---|---|
| 实体重叠导致误合并 | Stage 2.8 只召回不删除,最终由 Stage 3 判断 |
| 候选过多导致 prompt 过长 | 全局和单 item 上限、按分数截断 |
| LLM 过度合并 | 只接受 high confidenceuncertain 默认保留 |
| 官方发布与媒体深度解读被误合并 | prompt 明确:有独立新增事实/分析角度则保留 |
| 报告不可审计 | report 记录 shared_entities、score、reason |
## 12. 优先级
P0。该问题直接影响 AI 日报质量,且当前 Stage 3 在候选为 0 时完全失效,属于去重链路架构缺陷。