Add Live Web Search to a LangChain Agent
Wrap the serpex Python SDK in a LangChain @tool, hand that tool to create_agent, and your agent can run a live web search whenever it decides it needs one. Turn on include_content in the same tool and you also get each result's page back as markdown, so a second fetch step disappears. Below is the whole pattern: install, tool, agent, and a RAG-ready variant that returns page content instead of just snippets.
A lot of agents need two calls to get anything useful from the web: one to search, one to go fetch the pages that looked promising. It's not hard, it's just extra code and an extra round trip your agent has to reason through. Folding both into one tool call removes a whole class of glue code, and that's the version we're showing here.
What do you need before you start?
- Python 3.10+ (current LangChain needs it)
pip install serpex langchain- A Serpex API key from app.serpex.dev. New accounts get 200 free credits, no card required.
- The key stored in an environment variable,
SERPEX_API_KEY, never hardcoded in the script.
How do you wrap Serpex as a LangChain tool?
The serpex package exports SerpexClient. Its search() method takes a dict (or a SearchParams object) and returns a SearchResponse with a results list and a metadata object. Each SearchResult has title, url, snippet, position, and engine.
import osfrom langchain.tools import toolfrom serpex import SerpexClientclient = SerpexClient(os.environ["SERPEX_API_KEY"])@tooldef web_search(query: str) -> str:"""Search the live web and return titles, URLs, and snippets for a query."""response = client.search({"q": query})if not response.results:return "No results found."lines = [f"{r.title}\n{r.url}\n{r.snippet}" for r in response.results]return "\n\n".join(lines)
The docstring on web_search is doing real work here. LangChain reads it as the tool's description, and it's how the model decides when to reach for it. Keep it plain and specific.
How do you plug the tool into an agent?
create_agent from langchain.agents is the current way to build a tool-calling agent in LangChain. Give it a model, a list of tools, and a system prompt.
from langchain.agents import create_agentagent = create_agent(model="openai:gpt-4o-mini",tools=[web_search],system_prompt=("You are a research assistant. Use web_search whenever the user ""asks about something current or something you're not sure about."),)result = agent.invoke({"messages": [{"role": "user", "content": "What changed in the latest LangGraph release?"}]})print(result["messages"][-1].content)
That's the whole loop. The agent decides whether to call web_search, gets the results back as a tool message, and folds them into its answer. No manual routing, no separate "did the model ask for a tool" check.
How do you get page content back for RAG?
Plain search gives you snippets, which are fine for a quick answer but thin for anything that needs grounding. Set include_content on the same search() call and Serpex fetches the page for each top result and returns it as markdown in that result's content field. A result that couldn't be fetched carries content_error instead (content and content_error never both appear on the same result). This is best-effort: content comes back for roughly 79% of the results you ask for, so plan for a few misses.
import osfrom langchain.tools import toolfrom serpex import SerpexClientclient = SerpexClient(os.environ["SERPEX_API_KEY"])@tooldef web_search_with_content(query: str) -> str:"""Search the live web and return page content as markdown for the topresults, ready to chunk and pass to a retriever."""response = client.search({"q": query,"include_content": True,"content_results": 5,})chunks = []for r in response.results:if r.content:chunks.append(f"# {r.title}\n{r.url}\n\n{r.content}")elif r.content_error:chunks.append(f"# {r.title}\n{r.url}\n(content unavailable: {r.content_error})")return "\n\n---\n\n".join(chunks)
Swap this tool in for web_search in the create_agent call above and the agent now has full page text to reason over, not just a one-line snippet. That's the shape you want for RAG: chunk the returned markdown per result and index it, or hand it straight to the model if the query is narrow enough.
What does this cost per call?
A plain search is 1 credit. With include_content on, billing is based on how many pages actually came back, not how many you asked for: 3 credits when 1 to 5 pages are delivered, 6 credits when 6 to 10 are delivered, and if content delivery fails for every result in the request you're only charged the plain-search rate of 1 credit. Your own organization repeating the same query within about 5 minutes costs 0 credits either way. Full breakdown is in the Search API reference.
FAQ
Does this replace LangGraph entirely? No. create_agent is built on LangGraph under the hood, so you're still getting a graph-based agent loop, just without hand-wiring the nodes yourself.
Can I use the same tool with a different agent framework? Yes. SerpexClient and the tool function have nothing LangChain-specific in them beyond the @tool decorator, so the same client.search() call works in any Python agent setup.
What if a page fails to fetch? You get content_error on that result instead of content, with a reason like blocked, timeout, or robots_disallowed. The result still has its title, URL, and snippet, and a failed fetch alone is never billed.
Do I need to handle GET versus POST myself? No. SerpexClient.search() sends the request for you; you just pass a dict or a SearchParams object.
Get an API key at app.serpex.dev and you'll have 200 free credits to wire this up yourself, no card needed. Full install and setup steps are in the quickstart.