AI 工具

秘塔 AI 读取网页正文,再用本地 Ollama 分块总结

给一个网址,用秘塔的读取网页接口(POST /api/v1/reader,返回 Markdown 正文)拿到干净的正文,再交给本地 Ollama 上的千问做总结,这样正文不会再发给第二家云服务。长文按小标题切块,每块单独总结,再按原文顺序拼接。

BINBHO · 图文教程

给一个网址,用秘塔的读取网页接口(POST /api/v1/reader,返回 Markdown 正文)拿到干净的正文,再交给本地 Ollama 上的千问做总结,这样正文不会再发给第二家云服务。长文按小标题切块,每块单独总结,再按原文顺序拼接。

先看清成品

  • 测试: pytest 5 项通过。
  • 模拟服务器加本地 qwen2.5 1.5B,总结一个 3 条要点的示例页面: 连跑 2 次,3 条都完整保留。
  • 修复前: 漏掉了一条要点,还编出了原文没有的“品牌信誉和售后服务”(见调试示例)。

没有在 box 上跑的部分: box 上没有可用的秘塔 API Key,唯一对真实接口做过的调用是用无效 Key 测错误返回。

  • 实测结果: 错误也是 HTTP 200,body 里带 errCode:
    • search 返回 errCode 2005“API密钥无效”。
    • reader 返回 errCode 500“服务器错误”。
  • 其余部分: 都在我自己写的模拟服务器 fake_metaso.py 上跑。它返回的是 3 条虚构结果。
  • 字段名的来源: 响应字段(webpages 里的 title、link、snippet、score、date)参考的是开放平台 playground 页面和第三方集成的说明,没有找到正式的官方字段文档,接入前请先用真实 Key 打印一次完整响应。

合规提醒:METASO_API_KEY 只放在环境变量里。搜索结果和网页正文的版权属于原作者:笔记里保留来源链接,引用时注明出处,不要整篇转载。使用读取网页接口时,遵守对方网站的规则(服务条款、robots),不要批量抓取,也不要抓取需要登录才能看的页面。调用会消耗积分,价格和积分规则以秘塔开放平台的页面为准,本文不列价格。

做完之后你会得到什么?

成果 说明
read_and_summarize.py reader 获取正文(errCode 500 时提示检查 Key)→ 按小标题切块(每块 1 节)→ 按小标题数和列表项数确定要点条数 → 分块总结 → 按原文顺序拼接
metaso_search.py 和 A 篇相同,复用 post() 和 MetasoError
test_chunks.py 切块大小、每块的小节数、没有小标题的短文不切块、要点数跟随列表项

需要准备哪些环境?

项目 要求 说明
秘塔 API Key 同 A 篇
Ollama 一个本地中文模型 用 SUM_MODEL 指定;导入方法见 41-deepseek-B
Python 3.9 以上 pip install requests pytest

文件结构

45-metaso-B-src/
├── read_and_summarize.py
├── metaso_search.py
└── test_chunks.py

第 1 步:代码

read_and_summarize.py:

"""网页 → 干净正文 → 本地模型总结:用秘塔“读取网页”接口(POST /api/v1/reader,返回 Markdown 正文)抓正文,
再交给本地 Ollama 上的千问总结(正文不再发给第二家云服务)。长文按段落切块,分块总结后按原文顺序拼接(不让模型再合并,见下文调试示例)。
实测:key 无效时 reader 返回的是 HTTP 200 + errCode=500“服务器错误”,并不会直接说 key 不对 → 遇到 500 先检查 key。
环境变量:METASO_API_KEY、METASO_BASE、OLLAMA_URL(默认 http://127.0.0.1:11434)、SUM_MODEL(默认 qwen2.5-local:1.5b)
用法:python read_and_summarize.py 网址 [输出.md]"""
import os, re, sys, time
import requests
from metaso_search import MetasoError, post

OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.environ.get("SUM_MODEL", "qwen2.5-local:1.5b")
CHUNK = 3000   # 每块字符数;小模型上下文有限,分块更稳

def read(url):
    try: return post("reader", {"url": url}, accept="text/plain")
    except MetasoError as e:
        if "errCode=500" in str(e): raise MetasoError(f"{e}(实测 key 无效时也会返回 500,请先检查 METASO_API_KEY)")
        raise

def llm(prompt):
    r = requests.post(f"{OLLAMA}/v1/chat/completions", timeout=600, json={"model": MODEL, "temperature": 0, "reasoning_effort": "none",
        "messages": [{"role": "system", "content": "你是严谨的摘要助手,只根据给出的文字总结,不添加原文没有的信息。"}, {"role": "user", "content": prompt}]})
    return re.sub(r"<think>.*?</think>", "", r.json()["choices"][0]["message"]["content"], flags=re.S).strip()

MAX_SECTIONS = 1   # 每块只放 1 个小节:实测每块 2 节或更多时,小模型总会漏掉排在后面的小节

def chunks(text):
    """先按 Markdown 标题切成小节,再把小节装进块里(不超过 CHUNK 字、不超过 MAX_SECTIONS 节);没有标题的长文退回按段落切"""
    secs = [x for x in re.split(r"\n(?=#+ )", text) if x.strip()]
    limit = MAX_SECTIONS
    if len(secs) == 1:   # 没有小标题:按段落切,只按字数装块(段落不算“小节”,否则 100 字的短文也会被切成好几块)
        secs, limit = [x + "\n\n" for x in text.split("\n\n") if x.strip()], 10**9
    cur, n = "", 0
    for sec in secs:
        if cur and (len(cur) + len(sec) > CHUNK or n >= limit): yield cur; cur, n = "", 0
        cur += sec.rstrip() + "\n\n"; n += 1
    if cur.strip(): yield cur

def n_points(c):
    """要点数:至少每个小标题、每个列表项一条,长段落再按约每 150 字一条补,2~8 条"""
    items = len(re.findall(r"^\s*(?:\d+[.、]|[-*]) ", c, re.M))
    return min(8, max(2, len(re.findall(r"^#+ ", c, re.M)), items, len(c) // 150))

def summarize(text):
    parts = list(chunks(text))
    if len(parts) == 1: return llm(f"用 {n_points(text)} 条要点总结下面的文字,每个小标题至少一条,每条一句话:\n\n{text}"), 1
    # 不再让模型做“合并”:实测合并这一步会整段丢主题(报销、VPN 都被丢了),按原文顺序拼接各块要点更可靠
    partial = [llm(f"用 {n_points(c)} 条要点总结这一段,每个小标题至少一条,每条一句话:\n\n{c}") for c in parts]
    return "\n".join(partial), len(parts)

if __name__ == "__main__":
    url = sys.argv[1]
    try: text = read(url)
    except MetasoError as e: sys.exit(f"✗ {e}")
    t0 = time.time(); s, n = summarize(text)
    md = f"# 网页摘要\n\n- 原文:{url}\n- 读取时间:{time.strftime('%Y-%m-%d %H:%M')};正文 {len(text)} 字,分 {n} 块总结,用时 {time.time()-t0:.1f}s\n- 摘要模型:{MODEL}(本地)\n\n{s}\n\n> 机器摘要,可能遗漏或误读,重要内容请看原文。\n"
    if len(sys.argv) > 2: open(sys.argv[2], "w", encoding="utf-8").write(md); print(f"✓ 已写入 {sys.argv[2]}")
    print(md)

test_chunks.py:

import read_and_summarize as r

def test_chunks_respect_size():
    text = "\n\n".join("段落" * 200 for _ in range(20))
    parts = list(r.chunks(text))
    assert len(parts) > 1 and all(len(p) <= r.CHUNK + 500 for p in parts)
    assert "".join(parts).replace("\n", "") == text.replace("\n", "")

def test_heading_sections_limit():
    text = "\n".join(f"## 节{i}\n内容{i}" for i in range(5))
    parts = list(r.chunks(text))
    assert len(parts) == 5 and all(p.count("## ") <= r.MAX_SECTIONS for p in parts)

def test_points_follow_headings():
    assert r.n_points("## a\nx\n## b\ny\n## c\nz\n## d\nw") == 4

def test_short_text_without_subheadings_is_one_chunk():
    assert len(list(r.chunks("# 标题\n\n1. 一\n2. 二\n\n结尾一段。"))) == 1

def test_points_follow_list_items():
    assert r.n_points("# t\n1. a\n2. b\n3. c") == 3

第 2 步:运行

$ python read_and_summarize.py http://127.0.0.1:18400/page/1 摘要.md
✓ 已写入 摘要.md
# 网页摘要

- 原文:http://127.0.0.1:18400/page/1
- 读取时间:2026-10-21 17:04;正文 107 字,分 1 块总结,用时 1.4s
- 摘要模型:qwen2.5-local:1.5b(本地)

1. 硬盘位:至少两盘位,方便做 RAID1。
2. CPU:需要转码 4K 视频时选带核显的 Intel 型号。
3. 内存:跑 Docker 的话建议 8GB 以上。

> 机器摘要,可能遗漏或误读,重要内容请看原文。

调试示例

1. 合并这一步丢了主题。 早期版本先分块总结,再让模型把各块的要点“合并成一份”。测一份较长的员工手册时,合并后报销和 VPN 两部分整段消失了。另外,每块放 2 个以上小节时,排在后面的小节也经常被漏掉。改成每块 1 个小节、不再让模型合并,而是按原文顺序拼接之后,两次运行都覆盖了全部 7 个关键词。

2. 短文被切碎,还编了内容。 107 字的示例页面只有一个标题,程序退回到按段落切块,而“每块 1 节”的限制把每个段落都当成一节,结果标题单独成了一块。模型对着一个光秃秃的标题,编出了“考虑品牌信誉和售后服务”。另外,要点数只按字数计算,得出 2 条,于是 3 条要点里丢了一条。

修复:没有小标题时只按字数装块;要点数至少等于列表项的数量。两项都加了测试,复测 2 次都完整保留了 3 条,没有编造。

3. 错误码有误导性。 用无效 Key 调用真实的 reader 接口,返回的是 errCode 500“服务器错误”,并不会说是 Key 有问题。程序遇到 500 时会附带提示“先检查 METASO_API_KEY”。

常见问题

现象 原因 处理
errCode 500 Key 无效(实测),或网页本身读取失败 先检查 Key,再换一个网址试试
摘要里有原文没有的内容 小模型编造 抽查原文;换更大的模型
长文很慢 块多,CPU 上跑本地模型 换小模型或使用 GPU

模板规则

  • 正文只交给本地模型处理。
  • 切块按小标题,要点数跟随原文的结构。
  • 拼接交给程序,不交给模型。

项目资源

用途 地址
秘塔 AI 搜索开放平台 https://metaso.cn/search-api/playground
官方 SDK(metaso-sdk) https://github.com/meta-sota/metaso-sdk
Ollama OpenAI 兼容接口 https://docs.ollama.com/api/openai-compatibility

摘要好不好,看的是有没有漏、有没有编,这两点都要用测试固定下来。