392 lines
8.1 KiB
Markdown
392 lines
8.1 KiB
Markdown
# Content Extract MCP Service 设计草案
|
||
|
||
## 状态说明
|
||
|
||
本文件主要记录早期 “content extract MCP” 的设计抽象。
|
||
|
||
它仍有背景参考价值,但不是当前生产事实入口。
|
||
当前生产能力已经演进为更完整的 workflow service,正式口径请优先看:
|
||
|
||
- `README.md`
|
||
- `docs/current/context-reset-brief.md`
|
||
- `docs/openclaw/README.md`
|
||
- `docs/openclaw/openclaw-handoff.md`
|
||
- `docs/openclaw/openclaw-orchestration-flow.md`
|
||
|
||
## 1. 文档目的
|
||
|
||
本文档用于定义当前仓库中已经落地的 MCP 服务设计,即“内容提取 MCP”。
|
||
|
||
目标是明确:
|
||
|
||
- 这个 MCP 服务当前真正负责什么
|
||
- 它对外暴露哪些 tool
|
||
- 每个 tool 的输入输出结构是什么
|
||
- validator 与 LLM 摘要如何接在 MCP 之后
|
||
- 当前 MVP 已完成到哪一层
|
||
|
||
这份文档主要记录当时实现阶段的设计取向,而不是当前生产阶段的唯一事实来源。
|
||
|
||
---
|
||
|
||
## 2. 服务定位
|
||
|
||
这个 MCP 服务的角色是:为上层 Agent、OpenClaw 或其他自动化流程提供统一的“文章内容提取”能力。
|
||
|
||
它在整条链路中的位置是:
|
||
|
||
`RSS 聚合 -> Content Extract MCP -> LLM 摘要 -> 校验 -> 规则过滤 -> 入库 -> 推送`
|
||
|
||
它不是完整阅读流系统,也不是摘要服务本身,而是阅读流中的“结构化正文提取层”。
|
||
|
||
### 2.1 服务负责的事情
|
||
|
||
- 接收 URL 或标准化 `item`
|
||
- 获取正文或补全正文
|
||
- 抽取文章标题
|
||
- 抽取正文纯文本
|
||
- 返回结构化文章对象
|
||
- 返回质量标记和结构化错误
|
||
|
||
### 2.2 服务不负责的事情
|
||
|
||
- 管理 RSS 订阅源
|
||
- 生成摘要
|
||
- 主题分类
|
||
- 价值判断
|
||
- 规则过滤
|
||
- 写入知识库
|
||
- 发送通知
|
||
- 执行全流程编排
|
||
|
||
结论:
|
||
|
||
当前 MCP 服务边界应保持干净,只负责把网页或 item 转成结构化文章数据。
|
||
|
||
---
|
||
|
||
## 3. 总体架构
|
||
|
||
建议并且当前实现采用的是:
|
||
|
||
```text
|
||
MCP Server Layer
|
||
-> Tool Handlers
|
||
-> extraction core
|
||
-> normalizer
|
||
-> content_loader
|
||
-> extractor
|
||
-> quality_checker
|
||
-> mapper
|
||
```
|
||
|
||
### 3.1 外层 MCP 层职责
|
||
|
||
- 注册 tools
|
||
- 接收和校验 tool 输入
|
||
- 调用内部 extraction core
|
||
- 将结果包装成 MCP tool 输出
|
||
|
||
### 3.2 内层 extraction core 职责
|
||
|
||
- 处理正文获取、标题提取、正文抽取、质量检查、结果映射
|
||
- 与具体 MCP SDK 解耦
|
||
|
||
### 3.3 为什么必须分层
|
||
|
||
如果把内容提取逻辑直接写死在 MCP handler 里,后续会出现这些问题:
|
||
|
||
- 业务逻辑难测试
|
||
- 协议层与抽取逻辑耦合
|
||
- 以后想补 CLI、批处理或其他入口时需要重复实现
|
||
|
||
因此当前原则是:
|
||
|
||
- MCP 是外壳
|
||
- extraction core 是内核
|
||
|
||
---
|
||
|
||
## 4. 当前暴露的 Tools
|
||
|
||
当前实现只暴露两个 tool:
|
||
|
||
- `extract_url_content`
|
||
- `extract_item_content`
|
||
|
||
### 4.1 `extract_url_content`
|
||
|
||
角色:
|
||
|
||
- 直接输入 URL
|
||
- 适合单条文章测试
|
||
- 适合手动调试或上层 Agent 直接调用
|
||
|
||
建议输入:
|
||
|
||
```json
|
||
{
|
||
"url": "https://example.com/post/1",
|
||
"language_hint": "zh"
|
||
}
|
||
```
|
||
|
||
建议输出:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"article": {
|
||
"extract_id": "sha256:yyy",
|
||
"item_id": null,
|
||
"source_id": null,
|
||
"url": "https://example.com/post/1",
|
||
"title": "文章标题",
|
||
"language": "zh",
|
||
"content_kind": "article",
|
||
"plain_text": "抽取后的正文",
|
||
"quality_flags": {
|
||
"is_paywalled": false,
|
||
"is_truncated": false,
|
||
"is_low_content": false
|
||
},
|
||
"metadata": {
|
||
"content_source": "fetched_html",
|
||
"extractor": "trafilatura",
|
||
"char_count": 1234
|
||
},
|
||
"pipeline_state": "extracted"
|
||
},
|
||
"warnings": [],
|
||
"debug": {
|
||
"content_source": "fetched_html",
|
||
"extractor": "trafilatura"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 `extract_item_content`
|
||
|
||
角色:
|
||
|
||
- 输入标准化 `item`
|
||
- 与上游 RSS 聚合层对接
|
||
- 保留 `item_id`、`source_id` 等追踪信息
|
||
|
||
建议输入:
|
||
|
||
```json
|
||
{
|
||
"item": {
|
||
"item_id": "sha256:xxx",
|
||
"source_id": "my-blog",
|
||
"title": "文章标题",
|
||
"url": "https://example.com/post/1",
|
||
"published_at": "2026-03-23T08:00:00Z",
|
||
"raw_summary": "RSS 摘要",
|
||
"raw_content": "RSS 正文片段",
|
||
"content_kind": "article",
|
||
"language": "zh"
|
||
}
|
||
}
|
||
```
|
||
|
||
建议输出:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"article": {
|
||
"extract_id": "sha256:yyy",
|
||
"item_id": "sha256:xxx",
|
||
"source_id": "my-blog",
|
||
"url": "https://example.com/post/1",
|
||
"title": "文章标题",
|
||
"published_at": "2026-03-23T08:00:00Z",
|
||
"language": "zh",
|
||
"content_kind": "article",
|
||
"plain_text": "抽取后的正文",
|
||
"quality_flags": {
|
||
"is_paywalled": false,
|
||
"is_truncated": false,
|
||
"is_low_content": false
|
||
},
|
||
"metadata": {
|
||
"content_source": "item.raw_content",
|
||
"extractor": "inline",
|
||
"char_count": 1234
|
||
},
|
||
"pipeline_state": "extracted"
|
||
},
|
||
"warnings": [],
|
||
"debug": {
|
||
"content_source": "item.raw_content",
|
||
"extractor": "inline"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 统一输出规范
|
||
|
||
当前两个 tool 都复用同一个输出结构,即 `ExtractionOutput`。
|
||
|
||
成功时:
|
||
|
||
- `success = true`
|
||
- 返回 `article`
|
||
- 可附带 `warnings`
|
||
- 可附带 `debug`
|
||
|
||
失败时:
|
||
|
||
- `success = false`
|
||
- 返回结构化 `error`
|
||
- 不返回不完整的 `article`
|
||
|
||
这样做的好处是:
|
||
|
||
- 上层流程只需要消费一个稳定 schema
|
||
- 后续 LLM 摘要和 validator 可以独立接上
|
||
- OpenClaw 或其他 Agent 更容易编排
|
||
|
||
---
|
||
|
||
## 6. 错误返回规范
|
||
|
||
建议错误结构如下:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": {
|
||
"code": "CONTENT_FETCH_FAILED",
|
||
"message": "Failed to fetch article content",
|
||
"retryable": true,
|
||
"stage": "fetch",
|
||
"details": {
|
||
"url": "https://example.com/post/1"
|
||
}
|
||
},
|
||
"warnings": []
|
||
}
|
||
```
|
||
|
||
当前错误码包括:
|
||
|
||
- `INVALID_INPUT`
|
||
- `CONTENT_FETCH_FAILED`
|
||
- `CONTENT_EXTRACTION_FAILED`
|
||
- `CONTENT_TOO_SHORT`
|
||
- `UNKNOWN_ERROR`
|
||
|
||
使用原则:
|
||
|
||
- 抓取失败不等于程序崩溃
|
||
- 可以重试的错误应标记 `retryable = true`
|
||
- 错误应指出具体阶段
|
||
|
||
---
|
||
|
||
## 7. 当前技术选型
|
||
|
||
当前 MVP 使用:
|
||
|
||
- Python
|
||
- MCP Python SDK (`FastMCP`)
|
||
- `httpx` 进行网页抓取
|
||
- `trafilatura` 进行正文抽取
|
||
- `beautifulsoup4` 作为 HTML 兜底解析
|
||
- `pydantic` 进行输入输出约束
|
||
|
||
设计原则是:
|
||
|
||
- 外层 MCP
|
||
- 内层 extraction core
|
||
- LLM 摘要和校验作为 MCP 之后的独立环节
|
||
|
||
---
|
||
|
||
## 8. 当前目录结构建议
|
||
|
||
当前实现大致如下:
|
||
|
||
```text
|
||
summary_mcp/
|
||
server.py
|
||
core/
|
||
normalizer.py
|
||
content_loader.py
|
||
extractor.py
|
||
quality_checker.py
|
||
mapper.py
|
||
pipeline.py
|
||
models/
|
||
item.py
|
||
document.py
|
||
summary_io.py
|
||
llm_result.py
|
||
validators/
|
||
llm_result.py
|
||
```
|
||
|
||
说明:
|
||
|
||
- `server.py`:MCP 服务入口
|
||
- `core/`:提取内核
|
||
- `models/`:提取和校验相关模型
|
||
- `validators/`:LLM 输出校验逻辑
|
||
|
||
---
|
||
|
||
## 9. 与 LLM 摘要层的关系
|
||
|
||
当前 MCP 不负责摘要,但它与摘要层的接口已经明确:
|
||
|
||
1. MCP 产出结构化 `article`
|
||
2. 上层 LLM 根据 `article.title`、`article.url`、`article.plain_text` 等生成摘要 JSON
|
||
3. validator 对摘要 JSON 做 schema 校验与业务校验
|
||
4. 若不合格,进入修复重试
|
||
|
||
这条闭环当前已经通过本地脚本验证:
|
||
|
||
- `scripts/run_summary_loop.py`
|
||
|
||
因此,当前正确的职责拆分是:
|
||
|
||
- MCP 负责提取
|
||
- LLM 负责摘要
|
||
- validator 负责验收
|
||
|
||
---
|
||
|
||
## 10. 当前阶段验收结论
|
||
|
||
当前 MVP 已完成以下验证:
|
||
|
||
- 真实 URL 可以提取为结构化文章 JSON
|
||
- 提取结果可以保存为文件
|
||
- 提取结果可以喂给 LLM 生成摘要 JSON
|
||
- 摘要 JSON 可以通过 validator 校验
|
||
- 整条“提取 -> 摘要 -> 校验”最小闭环已经跑通
|
||
|
||
---
|
||
|
||
## 11. 后续扩展方向
|
||
|
||
后续建议按下面顺序推进:
|
||
|
||
- 接入真实 RSS 聚合结果并映射为 `item`
|
||
- 定义规则过滤层 schema
|
||
- 设计知识库入库格式
|
||
- 增加批量处理能力
|
||
- 引入 Playwright 作为动态页面兜底方案
|
||
- 重命名包和项目名,使其与当前职责一致
|
||
|
||
---
|
||
|
||
## 12. 当前阶段一句话结论
|
||
|
||
当前仓库中的 MCP 已经从早期“摘要 MCP”演进为“内容提取 MCP”,它的职责是稳定产出结构化文章 JSON,并把摘要与验收环节留给后续的 LLM 和 validator 流程。
|