Resposta rápida
Como carrego páginas web no LangChain ou LlamaIndex como Markdown limpo?
Chame o endpoint /read da WebReaderAPI e envolva cada resposta em um Document: o Markdown como conteúdo, final_url e o título como metadados. Para agentes, exponha a mesma chamada como uma ferramenta read_url. Cada página custa 1 crédito, 2 com navegador, a partir de US$ 0,07/1K páginas em outubro de 2026. Nenhum pacote nosso é necessário. Sem KYC; pagar com cripto.
Por que documentos Markdown e não HTML bruto?
Porque a recuperação funciona sobre texto, e o Markdown mantém a estrutura que importa enquanto descarta o resto. Os títulos sobrevivem, então um splitter pode cortar nas mudanças de tópico. Listas e tabelas sobrevivem, então passos e especificações ficam juntos. Navegação, banners de cookies e anúncios somem, então não poluem seus embeddings nem desperdiçam tokens.
Os metadados vêm da mesma chamada: final_url como a fonte a citar, title, published_at e language para filtragem.
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
Como construção um document loader do LangChain?
Transforme cada página em um langchain_core.documents.Document e depois divida por títulos com MarkdownHeaderTextSplitter do pacote langchain-text-splitters. Cada chunk mantém o caminho dos títulos nos metadados, e você adiciona a URL de origem para que toda resposta possa apontar de volta para a página.
Páginas ignoradas, como as recusadas pelo robots.txt, lançam PageError com o código; registre no log e continue.
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 "")
Como dou a um agente LangChain uma ferramenta read_url?
Decore uma função com @tool de langchain_core.tools. A docstring vira a descrição da ferramenta que o modelo lê, então diga o que a ferramenta retorna e quando usá-la. Retorne texto simples: uma linha de título, a fonte e o Markdown, cortado com max_chars para caber no seu modelo.
Vincule a ferramenta a qualquer modelo de chat que suporte tool calling, ou passe-a para o construtor do seu agente. O modelo então chama read_url com uma URL da conversa ou de um resultado de pesquisa.
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)
Como faço o mesmo no LlamaIndex?
O LlamaIndex usa sua própria classe Document com text e metadata, e FunctionTool.from_defaults para transformar uma função em ferramenta. O helper continua o mesmo. Construa um índice a partir dos documentos com VectorStoreIndex.from_documents, que usa o modelo de embedding que suas configurações do LlamaIndex indicarem.
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)
Qual o tamanho de crawl que um pequeno índice RAG precisa?
Menos do que você imagina. A documentação de um produto costuma ter algumas centenas de páginas, e um crawl com include_paths definido para a seção de docs lê apenas essas. A 1 crédito por página, um site de docs com 300 páginas custa cerca de 300 créditos; 10.000 páginas (US$ 3,75) o atualiza muitas vezes.
Re-rastreie em uma frequência que corresponda à frequência com que a fonte muda, e compare final_url e word_count com a execução anterior para re-embedar apenas as páginas que mudaram. Os créditos pré-pagos nunca expiram: as páginas não usadas ficam na sua conta até você usá-las.
LangChain e LlamaIndex: dúvidas frequentes
Existe um pacote do WebReaderAPI para LangChain ou LlamaIndex?
Não. O helper e a ferramenta desta página são a integração inteira, então você pode adaptá-los livremente. WebReaderAPI é uma API HTTPS simples que qualquer cliente HTTP em Python pode chamar.
Qual versão do LangChain isso exige?
Qualquer versão recente com langchain_core, langchain-text-splitters e init_chat_model. O decorador de ferramenta e a classe Document têm se mantido estáveis nas versões recentes.
Como evito reler páginas que não mudaram?
Armazene final_url, word_count e a data de cada leitura. Releia em uma frequência definida e re-embed apenas uma página quando a contagem de palavras ou o hash do conteúdo mudar. Cada releitura custa um crédito, então escolha um ritmo que corresponda à fonte.
E se algumas páginas forem recusadas?
Elas lançam PageError com um código como blocked_by_robots, e não custam nada. Se um site recusa nossa requisição, devolvemos a resposta dele: nunca resolvemos CAPTCHAs, nunca entramos em contas e nunca tentamos contornar um bloqueio ou um paywall.