概要
WebページをLangChainやLlamaIndexにクリーンなMarkdownとして読み込むには?
WebReaderAPIの/readエンドポイントを呼び出し、各レスポンスをDocumentでラップします。Markdownをcontentに、final_urlとタイトルをmetadataに入れます。エージェントには同じ呼び出しをread_urlツールとして公開します。ページは1ページ1クレジット、ブラウザ使用時は2クレジットで、2026年10月時点では$0.07/1Kページから利用できます。当社のパッケージは不要です。KYC不要、暗号資産で支払い。
生のHTMLではなくMarkdownドキュメントを使うのはなぜですか?
検索はテキストに対して機能し、Markdownは重要な構造を残しつつそれ以外を削除するからです。見出しが残るので、スプリッターはトピックの変化で分割できます。リストやテーブルが残るので、手順や仕様がまとまったまま保たれます。ナビゲーション、Cookieバナー、広告は削除されるため、埋め込みを汚染したりトークンを無駄に消費したりしません。
メタデータは同じ呼び出しから取得します。引用元としてのfinal_url、フィルタリング用のtitle、published_at、languageです。
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で見出しごとに分割します。各チャンクはメタデータに見出しパスを保持し、ソースURLを追加することで、すべての回答が元のページを指し示せるようになります。
robots.txtで拒否されたページなどスキップされたページは、コードとともにPageErrorを発生させます。ログに記録して処理を続けてください。
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エージェントにread_urlツールを与えるには?
langchain_core.toolsの@toolで関数をデコレートします。docstringがモデルに読まれるツールの説明になるため、ツールが何を返すか、いつ使うかを書いてください。プレーンテキストを返します。タイトル行、ソース、Markdownを並べ、モデルに合わせてmax_charsで切り詰めます。
ツール呼び出しをサポートする任意のチャットモデルにツールをバインドするか、エージェントのコンストラクタに渡します。モデルはその後、会話内または検索結果のURLを使って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の設定で指定した埋め込みモデルを使用します。
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インデックスにはどのくらいの規模のクロールが必要ですか?
思うより少なくて済みます。製品のドキュメントは数百ページであることが多く、include_pathsをドキュメントのセクションに設定したクロールはその部分だけを読みます。1ページ1クレジットなら、300ページのドキュメントサイトは約300クレジットで、10,000ページ ($3.75) なら何度でも更新できます。
情報源の変化頻度に合わせたスケジュールで再クロールし、前回の実行とfinal_urlおよびword_countを比較して、変更されたページだけを再埋め込みします。プリペイドクレジットに有効期限はありません。未使用のページは使うまでアカウントに残ります。
LangChainとLlamaIndex: よくある質問
LangChainやLlamaIndex用のWebReaderAPIパッケージはありますか?
いいえ。このページのヘルパーとツールが統合のすべてであり、自由に改変できます。WebReaderAPIはプレーンなHTTPS APIで、任意のPython HTTPクライアントから呼び出せます。
必要なLangChainのバージョンは?
langchain_core、langchain-text-splitters、init_chat_modelを備えた最近のバージョンであればどれでも構いません。ツールデコレーターとDocumentクラスは最近のリリースで安定しています。
変更されていないページの再読み取りを避けるには?
final_url、word_count、各読み取りの日付を保存します。スケジュールに従って再読み取りし、ページの語数またはコンテンツハッシュが変わった場合にのみ再埋め込みします。再読み取りごとに1クレジットかかるので、情報源に合ったペースを選んでください。
一部のページが拒否された場合は?
blocked_by_robotsなどのコードとともにPageErrorが発生し、クレジットは消費されません。サイトが当社のリクエストを拒否した場合、その回答をそのまま返します。CAPTCHAを解いたり、アカウントにログインしたり、ブロックやペイウォールを回避しようとしたりすることは決してありません。