Files
planet/docs/plans/earth-news-cruise-summary-plan.md
2026-04-29 17:27:44 +08:00

367 lines
12 KiB
Markdown
Raw 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.
# Earth 新闻巡航摘要增强计划
## 背景
`Earth` 的新闻巡航模式目前直接消费 `/api/v1/news/earth-feed` 返回的 `items[].summary`。这个字段主要来自 RSS/Atom 的 `description``summary``content`,再经过 HTML 清理与长度截断。
这个实现足够轻量,但在巡航展示里有三个问题:
- 不是所有新闻源都会提供摘要,部分源只返回标题和链接。
- 聚合源的摘要质量不稳定,可能只是重复标题、来源署名或片段文本。
- 巡航模式需要更稳定的“态势说明”,否则新闻卡片的 `SUMMARY` 区域会显得空或信息密度不足。
目标不是把所有新闻都交给大模型,而是建立一个分层摘要管线:能用新闻源自带内容时零成本处理,需要增强时优先用本地模型,云端 LLM 只作为可控兜底。
## 当前相关实现
| 文件 | 作用 |
| --- | --- |
| `backend/app/services/earth_news.py` | 拉取 RSS/Atom 新闻源、解析标题/摘要、按区域聚合并返回 Earth 新闻 payload |
| `backend/app/api/v1/news.py` | 暴露 `/api/v1/news/earth-feed` |
| `frontend/public/earth/js/news.js` | 拉取新闻 payload 并渲染媒体面板新闻列表 |
| `frontend/public/earth/js/news-cruise-adapter.js` | 将新闻条目映射为巡航事件,并把 `summary` 传给信息卡 |
| `frontend/public/earth/js/info-card.js` | 展示新闻巡航卡片中的 `SUMMARY` |
当前摘要生成逻辑集中在 `earth_news.py`
```python
summary = _extract_item_text(node, "description", "content")
clean_summary = _truncate(_strip_html(summary), 180)
```
这意味着后端还没有区分“摘要来自哪里”“质量是否足够”“是否需要异步增强”。
## 总体方案
采用四级摘要来源:
| 优先级 | 来源 | 成本 | 适用情况 | 风险 |
| --- | --- | --- | --- | --- |
| 1 | 新闻源自带 summary / description | 低 | RSS/Atom 已提供可读摘要 | 字段可能为空或重复标题 |
| 2 | 本地规则提取 | 低 | 有正文片段但没有可靠摘要 | 只能抽取,不能真正概括 |
| 3 | 本地 Gemma/Ollama 摘要 | 中 | 巡航会展示且前两级质量不足 | 本地模型质量与机器性能相关 |
| 4 | 云端 LLM 兜底 | 高 | 用户手动增强、重点新闻、失败补偿 | 成本与网络依赖 |
推荐默认策略:
```text
provider summary -> extractive summary -> cached local model summary -> async local model summary -> optional cloud LLM
```
巡航 UI 永远先展示已有摘要,不等待模型调用。模型摘要在后台补齐,写入缓存后下一轮巡航或刷新时使用。
## 数据结构
后端应把原来的 `summary: str` 升级为可追踪的摘要元信息,同时为了兼容前端保留顶层 `summary` 字段。
建议新增结构:
```json
{
"summary": "短摘要文本",
"summary_meta": {
"source": "provider",
"quality": "good",
"generated_at": "2026-04-29T00:00:00Z",
"content_hash": "sha256:...",
"model": null,
"language": "zh-CN"
}
}
```
字段说明:
| 字段 | 可选值 | 说明 |
| --- | --- | --- |
| `source` | `provider` / `extractive` / `local_llm` / `cloud_llm` / `fallback` | 摘要来源 |
| `quality` | `good` / `partial` / `poor` | 后端对摘要可用性的判断 |
| `generated_at` | ISO 时间 | 模型或规则生成时间 |
| `content_hash` | SHA-256 | 用于缓存命中和判断内容变化 |
| `model` | 字符串或 `null` | 例如 `gemma3:4b` |
| `language` | 语言代码 | 默认 `zh-CN`,也可跟随新闻语言 |
前端第一阶段不需要显示 `summary_meta`,但可以用于后续调试面板或质量标记。
## 后端设计
### NewsSummaryService
新增 `backend/app/services/news_summary.py`,提供统一入口:
```python
async def resolve_news_summary(item: ParsedNewsItem, *, mode: str) -> NewsSummaryResult:
...
```
核心职责:
- 标准化新闻输入标题、URL、来源、发布时间、摘要片段、正文片段。
- 判断 provider summary 是否可用。
- 生成本地规则摘要。
- 查询模型摘要缓存。
- 在允许时调用本地 Ollama/Gemma。
- 在增强模式或手动触发时调用云端 LLM。
- 返回摘要文本与 `summary_meta`
### 摘要质量判断
第一版可以用轻量规则:
- 少于 30 个字符:`poor`
- 与标题高度重复:`partial`
- 包含明显来源署名或聚合噪声:`partial`
- 60-180 个字符且不重复标题:`good`
伪代码:
```python
def score_summary(title: str, summary: str) -> SummaryQuality:
if len(summary.strip()) < 30:
return "poor"
if normalized_overlap(title, summary) > 0.75:
return "partial"
if looks_like_source_attribution(summary):
return "partial"
return "good"
```
### 本地规则摘要
如果新闻源没有摘要,但有 `content``description``snippet` 或正文片段:
- 清理 HTML。
- 去掉标题重复内容。
- 去掉来源署名、发布时间、图片说明。
- 优先取前 1-2 个完整句子。
- 控制在 80-140 个中文字符或 40-80 个英文词。
### 本地 Gemma/Ollama Provider
不要在业务里写死 Gemma抽象为 `LocalLLMSummaryProvider`,默认可以指向 Ollama
```env
NEWS_SUMMARY_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
NEWS_SUMMARY_MODEL=gemma3:4b
NEWS_SUMMARY_TIMEOUT_SECONDS=20
NEWS_SUMMARY_MAX_INPUT_CHARS=5000
NEWS_SUMMARY_MAX_PER_HOUR=60
```
Ollama 请求示例:
```http
POST /api/generate
Content-Type: application/json
{
"model": "gemma3:4b",
"prompt": "...",
"stream": false,
"options": {
"temperature": 0.2,
"num_predict": 180
}
}
```
摘要 prompt 要强调“只基于原文”,避免模型补事实:
```text
你是新闻摘要器。只根据输入新闻内容生成摘要,不要添加原文没有的信息。
输出中文1-2 句话80-140 字。
如果原文信息不足,只概括已知事实,不要推测。
标题:{title}
来源:{source}
发布时间:{published_at}
正文或片段:
{content}
```
### 缓存
需要缓存模型摘要,避免重复花时间和费用。
缓存 key
```text
sha256(url + title + published_at + normalized_content)
```
建议新增表或复用系统设置缓存。若要可查询与清理,推荐独立表:
```text
news_summary_cache
- id
- cache_key
- url
- title
- content_hash
- summary
- source
- quality
- provider
- model
- generated_at
- expires_at
- failure_count
- last_error
```
缓存策略:
- 同一 `cache_key` 命中后直接返回。
- `provider` / `extractive` 可以短期缓存。
- `local_llm` / `cloud_llm` 可以长缓存,内容 hash 变化才重算。
- LLM 失败后记录 `failure_count`,短时间内不重复调用。
## 前端与巡航行为
前端第一阶段只需要继续使用 `item.summary`,不阻塞现有逻辑。
后续可选增强:
- `news.js` 在渲染新闻列表时,如果 `summary_meta.quality === "poor"`,可以用更紧凑的标题卡样式。
- `news-cruise-adapter.js` 选择巡航项时,可以优先选择 `summary_meta.quality !== "poor"` 的新闻。
- `info-card.js` 不显示“AI 生成中”这类文案,避免把系统内部状态暴露给用户。
如果后端异步生成完成,可以通过下一次 `/api/v1/news/earth-feed` 刷新自然更新。第一版不需要 WebSocket。
## 调用策略
默认使用“省钱模式”:
- 只处理本次 payload 中即将进入巡航队列的前 N 条。
- `provider``extractive` 达到 `good` 时不调用模型。
- 本地模型失败时不影响新闻 payload。
- 云端 LLM 默认关闭,只允许手动增强或后台配置开启。
推荐限制:
| 配置 | 默认值 | 说明 |
| --- | --- | --- |
| `NEWS_SUMMARY_MODE` | `economy` | `off` / `economy` / `enhanced` / `manual` |
| `NEWS_SUMMARY_CRUISE_PREFETCH_LIMIT` | `10` | 每次新闻 payload 预热多少条巡航摘要 |
| `NEWS_SUMMARY_MAX_PER_HOUR` | `60` | 本地模型每小时最多处理数量 |
| `NEWS_SUMMARY_CLOUD_MAX_PER_DAY` | `20` | 云端 LLM 每天最多处理数量 |
| `NEWS_SUMMARY_TIMEOUT_SECONDS` | `20` | 单条模型摘要超时 |
| `NEWS_SUMMARY_MAX_INPUT_CHARS` | `5000` | 输入截断上限 |
## Gemma 本地部署建议
Gemma 适合作为“本地省钱层”,但不应成为强绑定依赖。建议通过 Ollama 接入,未来可切换 Qwen、Llama 或其他本地模型。
开发环境:
```bash
ollama pull gemma3:4b
ollama serve
```
集成原则:
- 后端只依赖 Ollama HTTP API不直接依赖 Gemma SDK。
- 模型名称来自配置,不写死在代码里。
- 健康检查访问 `/api/tags` 或执行一条极短测试 prompt。
- 如果 Ollama 不可用,摘要管线自动退回 `provider` / `extractive`
中文新闻较多时,需要单独评估 Gemma 与 Qwen 系本地模型的中文摘要质量。不要只看单条效果,至少抽样 50 条新闻比较:
- 事实准确性
- 中文自然度
- 长度稳定性
- 延迟
- 是否会补充原文没有的信息
## 云端 LLM 兜底
云端 LLM 不作为默认路径,只用于:
- 用户点击“增强摘要”。
- 管理员开启增强模式。
- 本地模型连续失败且新闻进入重点巡航队列。
云端结果同样写入 `news_summary_cache`,并受每日限额控制。
## 分阶段实施
### 第一阶段:零成本摘要质量增强
-`earth_news.py` 中引入 `summary_meta`
- 增加 provider summary 质量判断。
- 增加本地规则摘要兜底。
- `/api/v1/news/earth-feed` 保持兼容,继续返回顶层 `summary`
- 前端无需大改。
验收标准:
- 没有摘要的新闻也能尽量得到短摘要。
- `summary_meta.source``summary_meta.quality` 可用于调试。
- 现有新闻面板和巡航模式不破坏。
### 第二阶段:本地 Gemma/Ollama 摘要
- 新增 `LocalLLMSummaryProvider`
- 接入 Ollama `/api/generate`
- 添加超时、输入截断、错误退避。
- 增加模型摘要缓存。
- 巡航 payload 后台预热前 N 条摘要。
验收标准:
- Ollama 可用时,低质量摘要能被本地模型增强。
- Ollama 不可用时,新闻接口仍然正常返回。
- 同一新闻不会重复调用模型。
### 第三阶段:设置与可观测性
- 在设置中增加新闻摘要模式:
- `关闭`
- `省钱模式`
- `增强模式`
- `仅手动`
- 增加本地模型连通性检查。
- 暴露缓存命中率、模型调用次数、失败次数。
- 日志记录摘要来源和失败原因。
验收标准:
- 用户可以不改环境变量就知道本地摘要服务是否可用。
- 管理员能看出成本和失败情况。
### 第四阶段:云端 LLM 兜底
- 接入现有 AI Provider 或新增 cloud summary provider。
- 增加每日限额与手动增强入口。
- 对云端生成结果落缓存。
验收标准:
- 云端调用可控、可关闭、可限流。
- 云端失败不影响巡航。
## 风险与防护
| 风险 | 防护 |
| --- | --- |
| 本地模型生成不存在的事实 | prompt 明确禁止扩写;摘要只作为原文概括;保留来源链接 |
| 本地模型慢导致新闻接口卡住 | 模型摘要异步化;接口先返回已有摘要 |
| 成本失控 | 默认不启用云端;按小时/天限流;缓存命中优先 |
| 摘要语言不一致 | 配置目标语言,默认 `zh-CN` |
| 新闻源正文不足 | 只概括标题和片段,不强行扩写 |
| 模型服务不可用 | 自动回退,不影响巡航主流程 |
## 推荐优先级
先做第一阶段和第二阶段的最小闭环:
1. `summary_meta` + 质量判断。
2. 本地规则摘要。
3. Ollama provider。
4. 缓存。
5. 巡航前 N 条异步预热。
云端 LLM 和设置页可以后置。这样能先验证“摘要缺失比例、本地模型质量、实际延迟”三个关键问题,再决定是否投入更重的 UI 与云端增强。