Respuesta rápida
¿Cómo cargo páginas web en LangChain o LlamaIndex como Markdown limpio?
Llama al endpoint /read de WebReaderAPI y envuelve cada respuesta en un Document: el Markdown como contenido, final_url y el título como metadatos. Para agentes, expón la misma llamada como una herramienta read_url. Cada página cuesta 1 crédito, 2 con navegador, desde 0,07 US$/1K páginas a fecha de octubre de 2026. No se necesita ningún paquete nuestro. Sin KYC; paga con criptomonedas.
¿Por qué documentos Markdown en lugar de HTML sin procesar?
Porque la recuperación funciona sobre texto, y Markdown conserva la estructura que importa mientras descarta el resto. Los encabezados se mantienen, así que un splitter puede cortar por cambios de tema. Las listas y las tablas se mantienen, así que los pasos y las especificaciones permanecen juntos. La navegación, los banners de cookies y los anuncios desaparecen, así que no contaminan tus embeddings ni desperdician tokens.
Los metadatos vienen de la misma llamada: final_url como fuente a citar, title, published_at y language para filtrar.
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
¿Cómo construyo un document loader de LangChain?
Convierte cada página en un langchain_core.documents.Document y luego divídela por encabezados con MarkdownHeaderTextSplitter del paquete langchain-text-splitters. Cada fragmento conserva la ruta de encabezados en sus metadatos, y añades la URL de origen para que cada respuesta pueda señalar su página.
Las páginas omitidas, como las rechazadas por robots.txt, lanzan PageError con el código; regístralas y continúa.
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 "")
¿Cómo doy a un agente de LangChain una herramienta read_url?
Decora una función con @tool de langchain_core.tools. El docstring se convierte en la descripción de la herramienta que lee el modelo, así que indica qué devuelve la herramienta y cuándo usarla. Devuelve texto plano: una línea de título, la fuente y el Markdown, recortado con max_chars para adaptarse a tu modelo.
Vincula la herramienta a cualquier modelo de chat que admita tool calling, o pásala al constructor de tu agente. El modelo entonces llama a read_url con una URL de la conversación o de un resultado de búsqueda.
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)
¿Cómo hago lo mismo en LlamaIndex?
LlamaIndex usa su propia clase Document con text y metadata, y FunctionTool.from_defaults para convertir una función en herramienta. El helper sigue siendo el mismo. Construye un índice a partir de los documentos con VectorStoreIndex.from_documents, que usa el modelo de embeddings que indiquen tus ajustes de LlamaIndex.
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)
¿Qué tamaño de rastreo necesita un índice RAG pequeño?
Menos de lo que crees. La documentación de un producto suele tener unos cientos de páginas, y un rastreo con include_paths ajustado a la sección de docs lee solo esas. A 1 crédito por página, un sitio de docs de 300 páginas cuesta unos 300 créditos; 10.000 páginas (3,75 US$) lo actualiza muchas veces.
Vuelve a rastrear con una frecuencia que coincida con la frecuencia con que cambia la fuente, y compara final_url y word_count con la ejecución anterior para re-embeddear solo las páginas que cambiaron. Los créditos prepago nunca caducan: las páginas sin usar permanecen en tu cuenta hasta que las uses.
LangChain y LlamaIndex: preguntas frecuentes
¿Existe un paquete de WebReaderAPI para LangChain o LlamaIndex?
No. El helper y la herramienta de esta página son toda la integración, así que puedes adaptarlos libremente. WebReaderAPI es una API HTTPS simple que cualquier cliente HTTP de Python puede llamar.
¿Qué versión de LangChain necesita esto?
Cualquiera reciente con langchain_core, langchain-text-splitters y init_chat_model. El decorador de herramientas y la clase Document han sido estables en las versiones recientes.
¿Cómo evito volver a leer páginas que no cambiaron?
Guarda final_url, word_count y la fecha de cada lectura. Vuelve a leer con una frecuencia determinada y re-embeddéa una página solo cuando cambie su recuento de palabras o el hash de su contenido. Cada relectura cuesta un crédito, así que elige un ritmo que coincida con la fuente.
¿Y si algunas páginas son rechazadas?
Lanzan PageError con un código como blocked_by_robots, y no cuestan nada. Si un sitio rechaza nuestra solicitud, devolvemos su respuesta: nunca resolvemos CAPTCHAs, iniciamos sesión en cuentas ni intentamos saltar un bloqueo o un paywall.