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

> LangChain 与 LlamaIndex 网页加载器：一个 read_url 工具、一个文档加载器，以及一个由整站抓取 (crawl) 构建的小型 RAG 索引，全部用 Python 实现。无需 KYC，可用加密货币支付。

- URL: https://webreaderapi.com/zh/docs/langchain/
- 最后更新: 2026-10-22
- 发布者: WebReaderAPI (https://webreaderapi.com)

在 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：共享辅助代码（请将其保存在代码旁边）*

```python
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：加载并切分页面*

```python
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 工具*

```python
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 工具*

```python
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、不登录账户，也不试图绕过封锁或付费墙。

---

WebReaderAPI: WebReaderAPI 是一款面向 AI agents 的网页读取 API，可将任何公开网页转换为干净的 Markdown，用加密货币支付，无需上传身份证件。预付费积分低至 US$0.07/千页，永不过期，且遵守 robots.txt。无需 KYC — 无身份证、无自拍、无证件。加密货币支付。
