367 lines
12 KiB
Markdown
367 lines
12 KiB
Markdown
# 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 与云端增强。
|