Skip to main content

Web Search: Real-Time Knowledge for Agents

Aria can check Julie's inbox now. In the previous article, she read a message from Jane asking to grab coffee while Jane's in town. Naturally, Julie might follow up with something like: "Are there any big events happening in town next week that might make the coffee shop crowded?"

Try asking a plain language model that. It can't know — not because it's broken, but because of something fundamental about how these models work, which we're about to unpack. Then we'll fix it, using the exact same tool-calling pattern you already learned in Article 2.

🟢 Skill level: Beginner.

Quick Reference

When to use this: Whenever a question depends on information that could have changed since the model was trained — current events, today's data, anything time-sensitive.

Basic syntax:

from langchain.tools import tool
from tavily import TavilyClient

tavily_client = TavilyClient()

@tool
def web_search(query: str) -> dict:
"""Search the web for information."""
return tavily_client.search(query)

agent = create_agent(model="gpt-5-nano", tools=[web_search])

Common patterns:

  • Web search is just another tool — same @tool decorator, same decision-making process you learned in Article 2
  • The agent decides whether a question needs current information before calling it
  • A second API key (from the search provider) is required, separate from your model provider's key

Gotchas:

  • ⚠️ Without a web search tool, a model will sometimes confidently state outdated or made-up information rather than admitting it doesn't know — this is worth understanding, not just working around.
  • ⚠️ The agent only searches when it judges the question requires it — it's not "always on."

See also: Tool Calling: Giving Agents Abilities

What You Need to Know First

  • Everything from Article 2 — what a tool is, the @tool decorator, and how an agent decides to call one
  • An OpenAI API key already set up — see Article 1 if you haven't done this

What We'll Cover in This Article

  • Why language models can't know about current events on their own
  • How to set up a web search provider (Tavily) and get an API key
  • How to wrap web search in a tool, using the exact pattern from Article 2
  • How to watch Aria decide when a question actually needs a web search

What We'll Explain Along the Way

  • What a "training cutoff" is and why it exists
  • What an API provider is, for readers newer to working with external services

Why Can't a Language Model Just Know Current Events?

Quick recap from Article 1: a language model learns by being shown enormous amounts of text during a process called training. That training happens at a specific point in time, and then it stops — the model doesn't keep learning after that point, the same way a textbook doesn't update itself after it's printed. The point at which a model's training data ends is called its training cutoff.

Anything that happened after that cutoff — yesterday's news, this week's weather, a website that just launched — simply isn't in the model's training data, because it didn't exist yet when training happened. The model can't look it up internally because there's nothing internal to look up; it's not like the model is choosing not to check, there's just nothing there.

Here's the part that surprises a lot of beginners: a plain model without a search tool often won't say "I don't know." It will frequently generate something that sounds plausible and confident anyway — a behavior often called "hallucination." This isn't the model being dishonest; it's doing exactly what it was built to do, predicting likely-sounding text, even when the honest answer would be "I have no way to know that."

The fix isn't a smarter model. It's giving the model a way to actually go check — which, as you already know from Article 2, means giving it a tool.

Setting Up a Web Search Provider

We'll use Tavily, a search API built specifically for AI agents to use. Like getting your OpenAI key in Article 1, you'll need an account and an API key:

  1. Go to tavily.com and sign up for an account
  2. From your dashboard, copy your API key
  3. Add it to the same .env file you already have:
# .env
OPENAI_API_KEY=your-openai-key-here
TAVILY_API_KEY=your-tavily-key-here

⚠️ Same rule as before: keep this key out of your code and out of version control. Tavily's free tier includes a monthly allowance of searches, which is plenty for following along here.

Install the Tavily Python client alongside what you already have:

uv add tavily-python

(Or pip install tavily-python if you're using pip.)

Seeing the Problem First

Before fixing anything, let's actually see a plain agent struggle with a current-information question — it's a lot more convincing than just being told it'll happen.

# Purpose: Demonstrate that a plain agent can't reliably answer
# current-information questions
# Context: Establishes the problem before we introduce web search
# Input: A question that depends on real-time information
# Output: A response that's likely vague, outdated, or simply wrong

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain.messages import HumanMessage

agent = create_agent(model="gpt-5-nano")

question = HumanMessage(content="How up to date is your training knowledge?")

response = agent.invoke({"messages": [question]})

print(response["messages"][-1].content)

The model will tell you something about its training cutoff in general terms — but notice it genuinely cannot tell you anything that's happened since then. Ask it something concrete and current, like who currently holds a specific public office or what today's date is, and you'll see the gap directly.

Defining the Web Search Tool

Now let's fix it, using exactly the pattern you already know from Article 2: a Python function, wrapped with @tool, with a clear docstring.

# Purpose: Wrap Tavily's search API as a tool the agent can call
# Context: Same @tool pattern from Article 2, applied to a real external service
# Input: A search query string
# Output: Search results from the live web, as a dictionary

from langchain.tools import tool
from typing import Dict, Any
from tavily import TavilyClient

# TavilyClient automatically reads TAVILY_API_KEY from your environment
tavily_client = TavilyClient()

@tool
def web_search(query: str) -> Dict[str, Any]:
"""Search the web for information."""
return tavily_client.search(query)

This should look familiar — it's the exact same shape as check_inbox from Article 2: a decorated function with a docstring and a type-hinted return value. The only thing that's different is what happens inside the function: instead of returning fixed text, it makes a real network call to Tavily and returns live results.

Let's test it directly first, the same way we tested check_inbox:

# Purpose: Confirm the tool works before wiring it into Aria
# Context: Same direct-testing pattern from Article 2
# Input: A query dictionary
# Output: Real search results from Tavily

result = web_search.invoke({"query": "current weather events France this week"})
print(result)

You should see real, current results — not something the model made up, but actual data fetched from the live web right now.

Let's wire it into Aria and ask the kind of follow-up question Julie might actually have, building on Jane's coffee invite from Article 2:

# Purpose: Give Aria access to live web search and watch her use it
# Context: Continues the running scenario from Article 2 — Jane's coffee invite
# Input: A question that depends on current, real information
# Output: An answer grounded in real search results, not a guess

from langchain.agents import create_agent
from langchain.messages import HumanMessage

aria_system_prompt = """
You are Aria, a personal email assistant for Julie.
You are warm, concise, and a little formal — like an excellent
executive assistant. You never ramble, and you get straight to
the point while staying friendly.

You can check Julie's inbox and search the web for current information.
"""

agent = create_agent(
model="gpt-5-nano",
system_prompt=aria_system_prompt,
tools=[web_search],
)

question = HumanMessage(
content="Jane wants to grab coffee in town next week. Are there any "
"major events happening that might make things busier than usual?"
)

response = agent.invoke({"messages": [question]})

print(response["messages"][-1].content)

Aria should call web_search, pull back real current results, and answer based on what's actually happening — not a guess dressed up as an answer. You can confirm this the same way you did in Article 2:

# Confirm the search actually happened, and see exactly what was searched for
print(response["messages"][1].tool_calls)

Now ask Aria something that doesn't need current information, like "What's a good subject line for a reply to Jane?" — she'll answer directly, without searching, for the same reason explained in Article 2: the agent judges relevance per question, every time.

Common Misconceptions

❌ Misconception: Adding a web search tool gives the model "internet access" in general

Reality: The model still only knows what's in its training data, plus whatever a specific tool call returns for that specific question. It's not browsing the internet freely or maintaining ongoing access — it's making one targeted search when it decides one is needed.

Why this matters: If you expect the agent to somehow "know" about something it never explicitly searched for, you'll be surprised when it doesn't. Each piece of real-time knowledge has to come through an actual tool call.

Example:

# ❌ Wrong assumption: "the agent can browse the web whenever it wants to learn things"
# ✅ Correct: the agent calls web_search for a specific query, gets back specific
# results, and only knows what those results contained

❌ Misconception: A model "lying" about current events means something is broken

Reality: A model confidently stating outdated or incorrect information about current events isn't a malfunction — it's the predictable result of asking a model to answer something its training data simply doesn't contain.

Why this matters: Understanding why this happens (training cutoff, not a bug) is what makes you reach for a web search tool instead of just hoping a "better" model will magically know things it was never shown.

Troubleshooting Common Issues

Problem: Tavily authentication error

Symptoms: An error when calling web_search, mentioning an invalid or missing API key.

Common Causes:

  1. TAVILY_API_KEY is missing from your .env file or misspelled (most common)
  2. load_dotenv() wasn't called before TavilyClient() was instantiated

Diagnostic Steps:

# Step 1: Confirm both keys are present (without printing the actual values)
import os
print("OpenAI key loaded:", "OPENAI_API_KEY" in os.environ)
print("Tavily key loaded:", "TAVILY_API_KEY" in os.environ)

Solution: Double check TAVILY_API_KEY is spelled exactly right in your .env file, and that load_dotenv() runs before you create TavilyClient().

Prevention: Keep all your API keys in the same .env file and load them all at the very top of your script, before any client objects get created.

Problem: Aria doesn't search even though the question seems to need it

Symptoms: Aria answers a current-events question directly, without calling web_search, and the answer is vague or wrong.

Common Causes:

  1. The system prompt doesn't mention web search is available (most common)
  2. The question is ambiguous about whether current information is actually needed
  3. The web_search docstring doesn't clearly signal what kind of questions it's for

Diagnostic Steps:

# Step 1: Check tool_calls to see what (if anything) happened
print(response["messages"][1].tool_calls)

# Step 2: Try a more explicit, clearly time-sensitive question
question = HumanMessage(content="Search the web for the weather forecast in Paris next week.")

Solution: Make sure your system prompt explicitly mentions the agent can search the web, and keep the tool's docstring clear about its purpose.

Prevention: When testing a new tool, start with a question that obviously and unambiguously requires it, then gradually test more ambiguous phrasing.

Check Your Understanding

Quick Quiz

  1. Why can't a language model answer questions about events after its training cutoff?

    Show Answer

    Because that information simply never existed in the data the model was trained on — there's nothing to "look up" internally. The model isn't withholding information; it genuinely has no access to anything that happened after training stopped, unless given a tool that can fetch it.

  2. What's structurally different between check_inbox from Article 2 and web_search from this article?

    Show Answer

    Almost nothing at the code-pattern level — both use @tool, a docstring, and a type-hinted return value. The difference is entirely inside the function body: check_inbox returned fixed text, while web_search makes a real network call to an external API and returns live data.

  3. If you ask Aria "What's a polite way to say no?" with web_search available, will she search the web?

    Show Answer

    No — this question doesn't depend on current information, so the agent should judge it as not requiring a search and answer directly from its own knowledge, the same relevance judgment from Article 2.

Hands-On Exercise

Challenge: Modify web_search's docstring to specifically mention it's useful for "current events, weather, and recent news," then test whether that more specific docstring changes how reliably Aria decides to use it for borderline questions.

Show Solution
@tool
def web_search(query: str) -> Dict[str, Any]:
"""Search the web for current events, weather, recent news, or any
other information that may have changed recently."""
return tavily_client.search(query)

Explanation: A more specific docstring gives the model clearer signal about exactly when this tool applies, which often improves how consistently it's used for borderline or ambiguous questions — this is the same lesson from Article 2's troubleshooting section, just applied to a real external tool.

Summary: Key Takeaways

  • Language models have a training cutoff — they cannot know about anything that happened after that point on their own
  • Without a way to check, models will often answer current-event questions with confident-sounding but unreliable guesses
  • Web search is just another tool — same @tool pattern, same agent decision-making process from Article 2
  • A web search tool requires its own API key, separate from your model provider's key
  • The agent still decides, per question, whether a search is actually needed
  • Aria can now answer questions grounded in real, current information — not just guesses

Version Information

Tested with:

  • Python: >=3.10, <4.0
  • langchain: >=1.1.3 (latest stable as of writing: 1.3.4)
  • tavily-python: >=0.7.13

Known issues:

  • ⚠️ Tavily's free tier has a monthly search limit — if web_search starts failing unexpectedly after working fine, check your Tavily dashboard for usage limits before assuming it's a code problem.

What's Next?

You now understand why models need real-time tools at all, and how to give one web search using the exact pattern you already knew.

The natural next step is Memory and Threads: Agents That Remember — right now, every time you call agent.invoke(...), Aria starts completely fresh with no memory of anything said before. That's the next gap to close.

References