Risposta rapida
Come carico le pagine web in LangChain o LlamaIndex come Markdown pulito?
Chiama l'endpoint /read di WebReaderAPI e avvolgi ogni risposta in un Document: il Markdown come content, final_url e il titolo come metadata. Per gli agenti, esponi la stessa chiamata come tool read_url. Le pagine costano 1 credito ciascuna, 2 con un browser, a partire da 0,07 USD/1K pagine a ottobre 2026. Non serve nessun nostro pacchetto. Senza KYC; paghi in criptovalute.
Perché documenti Markdown invece di HTML grezzo?
Perché il retrieval lavora sul testo, e il Markdown mantiene la struttura che conta scartando il resto. Le intestazioni sopravvivono, così uno splitter può tagliare ai cambi di argomento. Liste e tabelle sopravvivono, così passaggi e specifiche restano insieme. Navigazione, banner dei cookie e pubblicità spariscono, così non inquinano i tuoi embedding né sprecano token.
I metadata arrivano dalla stessa chiamata: final_url come fonte da citare, title, published_at e language per il filtraggio.
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
Come costruisco un document loader per LangChain?
Trasforma ogni pagina in un langchain_core.documents.Document, poi dividila per intestazioni con MarkdownHeaderTextSplitter dal pacchetto langchain-text-splitters. Ogni chunk conserva il percorso delle intestazioni nei suoi metadata, e aggiungi l'URL di origine così ogni risposta può rimandare alla sua pagina.
Le pagine saltate, come quelle rifiutate da robots.txt, sollevano PageError con il codice; registrale nei log e continua.
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 "")
Come do a un agente LangChain un tool read_url?
Decora una funzione con @tool da langchain_core.tools. La docstring diventa la descrizione del tool che il modello legge, quindi indica cosa restituisce il tool e quando usarlo. Restituisci testo semplice: una riga di titolo, la fonte e il Markdown, tagliato con max_chars in base al tuo modello.
Collega il tool a qualsiasi chat model che supporta il tool calling, oppure passalo al costruttore del tuo agente. Il modello chiama poi read_url con un URL preso dalla conversazione o da un risultato di ricerca.
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)
Come faccio lo stesso in LlamaIndex?
LlamaIndex usa la propria classe Document con text e metadata, e FunctionTool.from_defaults per trasformare una funzione in un tool. L'helper resta lo stesso. Costruisci un indice dai documenti con VectorStoreIndex.from_documents, che usa qualunque embedding model nominino le tue impostazioni di 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)
Quanto deve essere grande un crawling per un piccolo indice RAG?
Meno di quanto pensi. La documentazione di un prodotto è spesso qualche centinaio di pagine, e un crawling con include_paths impostato sulla sezione dei docs legge solo quelle. A 1 credito a pagina, un sito di docs da 300 pagine costa circa 300 crediti; 10.000 pagine (3,75 USD) lo aggiorna molte volte.
Ripeti il crawling con una cadenza che corrisponde a quanto spesso cambia la fonte, e confronta final_url e word_count con l'esecuzione precedente per ri-incorporare solo le pagine cambiate. I crediti prepagati non scadono mai: le pagine non utilizzate restano sul tuo account finché non le usi.
LangChain e LlamaIndex: domande frequenti
Esiste un pacchetto WebReaderAPI per LangChain o LlamaIndex?
No. L'helper e il tool in questa pagina sono tutta l'integrazione, così puoi adattarli liberamente. WebReaderAPI è un'API HTTPS semplice che qualsiasi client HTTP Python può chiamare.
Quale versione di LangChain serve?
Qualsiasi versione recente con langchain_core, langchain-text-splitters e init_chat_model. Il decoratore del tool e la classe Document sono stati stabili nelle release recenti.
Come evito di rileggere pagine che non sono cambiate?
Salva final_url, word_count e la data di ogni lettura. Rileggi con una cadenza regolare, e ri-incorpora una pagina solo quando il suo conteggio parole o l'hash del contenuto cambia. Ogni rilettura costa un credito, quindi scegli un ritmo che corrisponde alla fonte.
E se alcune pagine vengono rifiutate?
Sollevano PageError con un codice come blocked_by_robots, e non costano nulla. Se un sito rifiuta la nostra richiesta, restituiamo la sua risposta: non risolviamo mai CAPTCHA, non accediamo agli account e non tentiamo di aggirare un blocco o un paywall.