AI agents
Ren Okabe6 min read11 views

AI Agent Web Search: Build a Web Search Agent in Python (2026)

A runnable 2026 tutorial: give an AI agent web search in Python with the Claude web_search server tool. Read citations, cap cost with max_uses, filter domains, and go multi turn.

Minimalist diagram of a Python terminal connected to three cited web result cards on a dark teal background
Minimalist diagram of a Python terminal connected to three cited web result cards on a dark teal background
On this page

> Quick answer (July 2026): The fastest way to give an AI agent web search in Python is the Claude API's built in web_search server tool. You add one entry to the tools array, Claude decides when to search, runs the searches server side, and returns an answer with cited sources. It costs $10 per 1,000 searches plus normal token costs (Anthropic, 2026). This tutorial builds a runnable web search agent, reads its citations, bounds its cost, and turns it into a multi turn agent.

Anthropic logo This is a code first, copy paste tutorial. Everything below runs against the Anthropic logo Anthropic API with the Python logo Python SDK. If you have never wired a tool onto a Claude agent before, read give your agent custom tools first, then come back.

An agent that cannot see the live web is stuck at its training cutoff. It cannot tell you today's release, a current price, or what shipped this morning. The good news: as of 2026 you do not have to build a scraper, a search index, or a retrieval pipeline to fix that. Anthropic runs the search for you and hands Claude cited results.

What you will build

A small Python program where you ask a question, the agent decides on its own whether it needs to search, runs one or more web searches, and answers with links to its sources. Then you will bound its cost, filter which domains it may read, and extend it to hold a conversation.

Prerequisites

  • Python 3.10 or newer
  • An Anthropic API key (set ANTHROPIC_API_KEY in your shell)
  • Five minutes

Install the SDK

bash
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."

The official SDK source lives at github.com/anthropics/anthropic-sdk-python if you want to read along.

The smallest possible web search agent

The web_search tool is a server tool: you declare it, and Anthropic executes the search on its side, feeds the results back to Claude, and loops until Claude is ready to answer. You do not write a search function.

python
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "What changed in the EU AI Act timeline in 2026? Cite your sources."}
    ],
    tools=[
        {"type": "web_search_20250305", "name": "web_search", "max_uses": 5}
    ],
)

for block in response.content:
    if block.type == "text":
        print(block.text)

Run it. Claude notices the question needs current information, issues one or more searches, and prints an answer grounded in what it found. Ask it something stable instead ("what is a hash map?") and it will answer without searching at all. That decision is the whole point of an agent: it picks the tool when the tool helps.

web_search_20250305 is the basic version. Two newer versions exist as of 2026, web_search_20260209 (adds dynamic filtering, so Claude filters results with code before they hit the context window) and web_search_20260318 (adds response inclusion control). Start with the basic one; upgrade when token cost on search heavy runs becomes a concern.

Read the citations

Every web search answer carries citations, and if you show search output to users you are expected to surface them. They hang off each text block:

python
def print_answer_with_sources(response):
    sources = []
    for block in response.content:
        if block.type == "text":
            print(block.text, end="")
            for citation in getattr(block, "citations", None) or []:
                sources.append((citation.title, citation.url))
    print("\n\nSources:")
    for title, url in dict((u, t) for t, u in sources).items():
        print(f"- {sources and ''}{url}")

# usage
print_answer_with_sources(response)

Each citation gives you url, title, and up to 150 characters of cited_text. Those citation fields do not count toward your token bill, so rendering them is free.

Bound the cost and scope

Two things you will want in any real agent: a cap on how many searches a single request may run, and a limit on which sites it may read.

python
RESEARCH_TOOL = {
    "type": "web_search_20250305",
    "name": "web_search",
    "max_uses": 3,                       # hard cap per request
    "allowed_domains": ["arxiv.org", "anthropic.com"],  # only read these
    # "blocked_domains": ["pinterest.com"],  # or ban some (never use both at once)
    "user_location": {                   # localize results
        "type": "approximate",
        "country": "US",
        "timezone": "America/New_York",
    },
}

max_uses is your cost governor. Simple factual questions use one to three searches; open ended research can use ten or more, so an uncapped research agent can quietly run up searches. At $10 per 1,000 searches (Anthropic, 2026), max_uses: 3 keeps a latency sensitive lookup to three cents, worst case. Use allowed_domains or blocked_domains, never both in the same tool, or the API returns a 400.

Turn it into a multi turn agent

A one shot answer is not an agent you can talk to. To hold a conversation, append the assistant's reply back into messages and keep going.

python
messages = [{"role": "user", "content": "Find the latest Claude model and its context window."}]

def run_turn(messages):
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        messages=messages,
        tools=[RESEARCH_TOOL],
    )
    # The API can pause a long search turn. Resend the assistant message to continue.
    while response.stop_reason == "pause_turn":
        messages.append({"role": "assistant", "content": response.content})
        response = client.messages.create(
            model="claude-sonnet-5", max_tokens=1024, messages=messages, tools=[RESEARCH_TOOL]
        )
    return response

response = run_turn(messages)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "How does that compare to the previous version?"})
response = run_turn(messages)

The gotcha most tutorials miss: each search result carries an encrypted_content field, and you must send the assistant's content blocks back exactly as you received them, encrypted content included. Do not rebuild the assistant message by hand or strip fields to "clean it up", the next request will fail with a 400 validation error. Passing response.content straight back, as above, is the safe path.

Built in tool versus rolling your own

You do not have to use Anthropic's search. You can define a client side tool and call any search API yourself, for example Tavily or the Brave Search API. That is the same client tool loop from the custom tools tutorial: Claude emits a tool_use, you run the search, you return a tool_result. Here is the decision, honestly:

Brave logo

Scroll to see more

ApproachWhat you implementCost model (2026)CitationsBest when
Built in web_search toolone line in tools$10 per 1,000 searches + tokensautomatic, source citedyou want working search today
Your own tool + a search APIthe tool loop and the API callsyour search vendor's bill + tokensyou build them yourselfyou need a specific index, an existing search contract, or on prem data

The built in tool wins for most agents because search, iteration, and citations are already wired. Reach for a custom tool when you need a source Anthropic does not search, such as your own document store or an internal wiki.

Doing it inside the Claude Agent SDK

If you are building on the Claude Agent SDK rather than raw Messages calls, you get the same capability as one of the SDK's built in tools. You allow it and the agent uses it inside its own loop:

python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        model="claude-sonnet-5",
        system_prompt="You are a research assistant. Search the web when a fact might be current, and always cite links.",
        allowed_tools=["WebSearch"],
    )
    async for message in query(prompt="What did Anthropic ship this week? Cite sources.", options=options):
        print(message)

asyncio.run(main())

The raw Messages approach in Steps 2 to 5 gives you the most control over cost and domains; the Agent SDK gives you a longer running loop with less plumbing. Pick based on how much of the loop you want to own.

Verify it works

Run this one file end to end. It should print an answer plus at least one real URL. If you see sources, your web search agent is live:

python
r = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "What is today's date and one headline from this week? Cite the source."}],
    tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 2}],
)
cites = [c.url for b in r.content if b.type == "text" for c in (getattr(b, "citations", None) or [])]
print("citations found:", len(cites))
assert len(cites) >= 1, "no citations: check ANTHROPIC_API_KEY and that web search is enabled for your org"
print("OK:", cites[:3])

If the assertion fails with a message that web search is not enabled, an administrator can turn it on in the Claude Console; the tool is enabled for most organizations by default.

FAQ

Covered inline below and mirrored in the page's structured data.

R

Written by

Ren Okabe

Frequently asked questions

How do I add web search to an AI agent in Python?

Install the anthropic SDK, then pass a tool of type web_search_20250305 named web_search in the tools array of client.messages.create. Claude decides when to search, runs the searches server side, and returns a cited answer. You do not implement a search function yourself.

How much does the Claude web search tool cost in 2026?

Web search is $10 per 1,000 searches on the Claude API as of 2026, billed on top of normal input and output token costs. Each search counts as one use regardless of how many results come back, and failed searches are not billed. Use max_uses to cap searches per request.

Does Claude always search the web when the tool is enabled?

No. Claude searches only when a request depends on current, changing, or out of training data information. Stable questions like math or coding concepts are answered directly. You can steer this with your system prompt, and hard cap it with max_uses.

Why do I get a 400 error on the second turn of a web search conversation?

In multi turn conversations you must send the assistant's content blocks back exactly as received, including each search result's encrypted_content field. Rebuilding the message by hand or stripping fields causes a 400 validation error. Append response.content unchanged.

Should I use the built in web_search tool or a search API like Tavily or Brave?

Use the built in web_search tool when you want search, iteration, and citations working immediately for $10 per 1,000 searches. Roll your own client side tool with a search API such as Tavily or Brave when you need a specific index, an existing search contract, or access to private or on prem data the public web tool cannot reach.

Can I limit which websites the agent reads?

Yes. Add allowed_domains to restrict search to a whitelist, or blocked_domains to ban specific sites, on the tool definition. Use one or the other, never both in the same tool, or the API returns a 400 error. You can also localize results with user_location.