无需 KYC — 无身份证、无自拍、无证件。加密货币支付:BTC, USDT, USDC, ETH, SOL, LTC
语言: 简体中文
获取 API 密钥

文档 · LangChain · LlamaIndex

LangChain 与 LlamaIndex:网页加载器和 read_url 工具

在 Python AI 技术栈中使用该 API 有两种方式:将页面加载为文档以供检索,或为 agent 提供一个 read_url 工具。同样的 20 行衔接代码可同时用于 LangChain 和 LlamaIndex。

最后更新:

快速回答

如何将网页以干净的 Markdown 加载到 LangChain 或 LlamaIndex?

调用 WebReaderAPI 的 /read 端点,并将每个响应包装为 Document:Markdown 作为 content,final_url 和标题作为 metadata。对于 agent,将同一调用暴露为 read_url 工具。每个页面 1 积分,使用浏览器则为 2 积分,自 2026 年 10 月起US$0.07/千页。无需使用我们的任何软件包。无需 KYC;可用加密货币支付。

为什么使用 Markdown 文档而不是原始 HTML?

因为检索作用于文本,而 Markdown 保留了重要的结构,同时丢弃其余部分。标题会保留下来,因此拆分器可以按主题变化进行切分。列表和表格会保留下来,因此步骤和规格保持在一起。导航、cookie 横幅和广告会被去除,因此它们不会污染你的 embeddings,也不会浪费 tokens。

metadata 来自同一次调用:final_url 作为引用来源,以及用于过滤的 title、published_at 和 language。

reader.py:共享辅助代码(请将其保存在代码旁边)
import os

import requests

API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}


class PageError(Exception):
    pass


def read_page(url, **options):
    resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
    body = resp.json()
    if resp.status_code != 200:
        raise PageError(body["error"]["code"])
    return body

如何构建 LangChain 文档加载器?

将每个页面转换为 langchain_core.documents.Document,然后使用 langchain-text-splitters 包中的 MarkdownHeaderTextSplitter 按标题进行切分。每个分块都在其 metadata 中保留标题路径,并添加来源 URL,以便每个答案都能指回其页面。

被跳过的页面(例如被 robots.txt 拒绝的页面)会抛出带有代码的 PageError;请记录这些错误并继续。

LangChain:加载并切分页面
from langchain_core.documents import Document
from langchain_text_splitters import MarkdownHeaderTextSplitter

from reader import PageError, read_page

splitter = MarkdownHeaderTextSplitter(headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")])


def load_pages(urls):
    chunks = []
    for url in urls:
        try:
            page = read_page(url)
        except PageError as exc:
            print("skipped", url, exc)
            continue
        for part in splitter.split_text(page["markdown"]):
            part.metadata.update({"source": page["final_url"], "title": page.get("title", "")})
            chunks.append(part)
    return chunks


docs = load_pages(["https://example.com/docs/install", "https://example.com/docs/configure"])
print(len(docs), "chunks", isinstance(docs[0], Document) if docs else "")

如何为 LangChain agent 提供 read_url 工具?

用 langchain_core.tools 中的 @tool 装饰一个函数。docstring 会成为模型读取的工具描述,因此请说明该工具返回什么以及何时使用它。返回纯文本:一行标题、来源和 Markdown,并用 max_chars 截断以适配你的模型。

将该工具绑定到任何支持 tool calling 的聊天模型,或将其传递给 agent 构造函数。然后模型会用对话中或搜索结果中的 URL 调用 read_url。

LangChain:read_url 工具
import os

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

from reader import PageError, read_page


@tool
def read_url(url: str, max_chars: int = 20000) -> str:
    """Read a public web page and return its main content as Markdown, with its title and source URL."""
    try:
        page = read_page(url, max_chars=max_chars)
    except PageError as exc:
        return "Could not read the page: " + str(exc)
    return "# " + (page.get("title") or url) + "\nSource: " + page["final_url"] + "\n\n" + page["markdown"]


model = init_chat_model(os.environ.get("CHAT_MODEL", "openai:gpt-4.1-mini")).bind_tools([read_url])
reply = model.invoke("Summarise https://example.com/blog/first-post in three bullet points.")
print(reply.tool_calls)

如何在 LlamaIndex 中做同样的事?

LlamaIndex 使用自己的 Document 类,配合 text 和 metadata,以及 FunctionTool.from_defaults 将函数转换为工具。辅助代码保持不变。使用 VectorStoreIndex.from_documents 从文档构建索引,它会使用你的 LlamaIndex 设置中指定的任何 embedding model。

LlamaIndex:为一次整站抓取 (crawl) 建索引,以及一个 read_url 工具
import os
import time

import requests
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.tools import FunctionTool

from reader import HEADERS, PageError, read_page


def crawl_pages(start_url, limit=200):
    job = requests.post("https://webreaderapi.com/api/v1/crawl", headers=HEADERS, json={"url": start_url, "limit": limit}, timeout=30).json()
    pages, cursor = [], None
    while True:
        params = {"cursor": cursor} if cursor else {}
        res = requests.get("https://webreaderapi.com/api/v1/jobs/" + job["job_id"], headers=HEADERS, params=params, timeout=30).json()
        pages += [r for r in res["results"] if "error" not in r]
        cursor = res.get("next_cursor")
        if not cursor and res["status"] == "done":
            return pages
        if not cursor:
            time.sleep(5)


pages = crawl_pages("https://docs.example.com/")
docs = [Document(text=p["markdown"], metadata={"source": p["final_url"], "title": p.get("title", "")}) for p in pages]
index = VectorStoreIndex.from_documents(docs)
print(index.as_query_engine().query("How do I install the product?"))


def read_url(url: str, max_chars: int = 20000) -> str:
    """Read a public web page and return its main content as Markdown."""
    try:
        return read_page(url, max_chars=max_chars)["markdown"]
    except PageError as exc:
        return "Could not read the page: " + str(exc)


read_tool = FunctionTool.from_defaults(fn=read_url, name="read_url")
print(os.environ.get("CHAT_MODEL", "set CHAT_MODEL for your agent"), read_tool.metadata.name)

小型 RAG 索引需要多大范围的整站抓取 (crawl)?

比你想的少。一个产品的文档通常只有几百页,而将 include_paths 设置为文档分区的整站抓取 (crawl) 只会读取这些页面。按每页 1 积分计算,一个 300 页的文档网站约需 300 积分;10,000 个网页(US$3.75)可以多次刷新它。

按与来源变化频率相匹配的计划重新整站抓取 (crawl),并将 final_url 和 word_count 与上一次运行进行比较,仅对发生变化的页面重新生成 embedding。预付费积分永不过期:未使用的页面数会一直保留在你的账户中,直到你使用。

LangChain 与 LlamaIndex:常见问题

是否有适用于 LangChain 或 LlamaIndex 的 WebReaderAPI 软件包?

没有。本页上的辅助代码和工具就是全部集成内容,因此你可以自由调整它们。WebReaderAPI 是一个普通的 HTTPS API,任何 Python HTTP 客户端都可以调用。

这需要哪个 LangChain 版本?

任何包含 langchain_core、langchain-text-splitters 和 init_chat_model 的近期版本均可。工具装饰器和 Document 类在近期版本中一直保持稳定。

如何避免重新读取未发生变化的页面?

保存 final_url、word_count 以及每次读取的日期。按计划重新读取,仅当页面的单词数或内容哈希发生变化时才重新生成该页面的 embedding。每次重新读取都会消耗一个积分,因此请选择与来源相匹配的节奏。

如果某些页面被拒绝怎么办?

它们会抛出带有代码(例如 blocked_by_robots)的 PageError,且不产生任何费用。如果网站拒绝我们的请求,我们会返回其答复:我们从不破解 CAPTCHA、不登录账户,也不试图绕过封锁或付费墙。

低至 US$0.07/千页 · 无需 KYC

获取 API 密钥