No KYC — no ID, no selfie, no documents. Pay in crypto: BTC, USDT, USDC, ETH, SOL, LTC
Language: English
Get an API key

Docs · LangChain · LlamaIndex

Web search in LangChain and LlamaIndex: a tool and a retriever

Two shapes cover most frameworks. A tool lets an agent decide when to search. A retriever plugs the live web into a retrieval chain, where each page that was read becomes a document with its URL in the metadata.

Last updated: · 5 min read

Quick answer

How do I add live web search to LangChain or LlamaIndex?

Wrap LLMWebSearch's /search call in a LangChain @tool, a LangChain retriever or a LlamaIndex FunctionTool. Each returns ranked sources with the top 3 pages as Markdown and their URLs, ready for citations. No package of ours to install: the code below is the integration. A search costs 1 credit, from $1.20/1K searches as of October 2026. No KYC; pay in crypto.

What each wrapper is for

LangChain toolAn agent calls search_web when it decides it needs the web
LangChain retrieverA chain fetches fresh documents for every question, without asking
LlamaIndex FunctionToolThe same search_web function, for LlamaIndex agents
Packageslangchain-core or llama-index-core, plus requests
KeyLLMWEBSEARCH_API_KEY in the environment
BillingOne credit pays for one search: up to 20 ranked results, with the pages of the first 3 read and returned as Markdown. Reading 4 to 10 pages makes it 2 credits. A call that ends in an error costs nothing

How do I build a search tool for a LangChain agent?

Decorate a function with @tool from langchain_core.tools. LangChain reads the name, the typed arguments and the docstring to describe the tool to the model, so keep the docstring short and precise. The function returns one string with numbered sources, which suits every chat model and keeps the URLs next to the text.

Call it directly with .invoke() to test it, then pass it in the tools list of your agent.

LangChain tool: search_web
import os

import requests
from langchain_core.tools import tool

API = "https://llmwebsearch.com/api/v1/search"
HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}


@tool
def search_web(query: str, read: int = 3) -> str:
    """Search the live web. Returns numbered sources with URLs; the first `read` include page text."""
    body = {"q": query, "max_results": 5, "read": max(0, min(read, 10)), "max_chars": 5000}
    resp = requests.post(API, headers=HEADERS, json=body, timeout=60)
    resp.raise_for_status()
    parts = []
    for i, r in enumerate(resp.json()["results"], start=1):
        text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
        parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
    return "\n\n".join(parts)


print(search_web.invoke({"query": "postgres connection pool size"})[:500])

How do I use the live web as a retriever?

Subclass BaseRetriever and return one Document per page that was read. The page text goes in page_content; the URL, title, rank and date go in metadata, where chains that cite their sources look for them. Results without content are skipped, because a retriever should hand over text the model can quote.

A retriever runs on every question. That is right for a question-answering chain about current topics, and wasteful for a chat where most turns need no search; use the tool in that case.

LangChain retriever: LLMWebSearchRetriever
import os
from typing import List

import requests
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from langchain_core.retrievers import BaseRetriever


class LLMWebSearchRetriever(BaseRetriever):
    """Live web search results as Documents, one per page that was read."""

    max_results: int = 5
    read: int = 3
    max_chars: int = 6000

    def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> List[Document]:
        resp = requests.post(
            "https://llmwebsearch.com/api/v1/search",
            headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
            json={"q": query, "max_results": self.max_results, "read": self.read, "max_chars": self.max_chars},
            timeout=60,
        )
        resp.raise_for_status()
        docs = []
        for r in resp.json()["results"]:
            if r["content_status"] != "ok":
                continue
            meta = {"source": r["url"], "title": r["title"], "position": r["position"], "published_at": r.get("published_at")}
            docs.append(Document(page_content=r["content"], metadata=meta))
        return docs


retriever = LLMWebSearchRetriever(read=3)
for doc in retriever.invoke("pgbouncer transaction mode limitations"):
    print(doc.metadata["position"], doc.metadata["source"], len(doc.page_content))

How do I give the same search to a LlamaIndex agent?

Write a plain function and wrap it with FunctionTool.from_defaults from llama_index.core.tools. LlamaIndex builds the tool's name and description from the function's name, signature and docstring. Calling the tool returns a ToolOutput whose content is the text the agent will read.

The function is the same as the LangChain one, so a team that uses both frameworks can share it.

LlamaIndex: FunctionTool
import os

import requests
from llama_index.core.tools import FunctionTool


def search_web(query: str, read: int = 3) -> str:
    """Search the live web and return numbered sources; the first `read` include their page text."""
    resp = requests.post(
        "https://llmwebsearch.com/api/v1/search",
        headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
        json={"q": query, "max_results": 5, "read": max(0, min(read, 10)), "max_chars": 5000},
        timeout=60,
    )
    resp.raise_for_status()
    parts = []
    for i, r in enumerate(resp.json()["results"], start=1):
        text = r["content"] if r["content_status"] == "ok" else "(snippet only) " + r["snippet"]
        parts.append(f"[{i}] {r['title']} - {r['url']}\n{text}")
    return "\n\n".join(parts)


search_tool = FunctionTool.from_defaults(fn=search_web)
print(search_tool.metadata.name)
print(search_tool.call(query="postgres connection pool size").content[:300])

How do I keep the context window under control?

Set max_chars on the API call rather than trimming afterwards: less data travels and the framework never holds the long version. 5,000 characters per page and 3 pages is about 4,000 tokens, comfortable for most models.

For retrieval chains that combine the live web with your own documents, give web documents a lower weight or a separate prompt section. Web pages change and can be wrong; your own documents are usually the reference.

Can I store what the retriever returns in a vector database?

Yes, as far as we are concerned. We do not train any model on your queries or on what the API returns, and we add no rule of our own against storing results; the rights of the sites whose pages you read still apply. Store the URL and published_at with every chunk, so the agent can cite the page and you can drop chunks that grow old.

Most teams cache the web results of frequent questions for a day or a week. That cuts repeat searches, and each search avoided is a credit saved.

LangChain and LlamaIndex: common questions

Is there a langchain-llmwebsearch package?

No. We publish no package. The code on this page is short enough to own: copy it, change the defaults, and keep it in your repository.

Can I make the tool async?

Yes. Replace requests with httpx.AsyncClient and define the function with async def. LangChain's @tool decorates async functions directly; in LlamaIndex, pass the async version to FunctionTool.from_defaults as async_fn.

How do I read a page the agent found elsewhere?

Add a second tool that posts URLs to https://llmwebsearch.com/api/v1/extract. Reading pages whose URLs you already have costs 1 credit for each started group of 3 pages returned with content, up to 20 URLs per call; refused or failed URLs are not counted. The extract reference shows the request.

From $1.20/1K searches · no KYC

Get an API key