Browse 1000+ Public APIs

The Search Layer for AI Agents: Building Smarter LLM Workflows with Real-Time Data

7 days ago

A team I was reviewing code for recently shipped an internal agent that answers questions about upcoming developer conferences. On day one in production, someone asked it: "Which AI conferences are happening in San Francisco in Q1 2026?"

The agent returned a confident, well-formatted list of five conferences. Three of them didn't exist. One had happened in 2024. Only one was real.

The model wasn't broken. It was doing exactly what LLMs do — generating the most statistically likely next tokens given the prompt. The problem was structural: the model's training data ended sometime in 2024, and it had no way to know what it didn't know. So it filled the gap with plausible-sounding fiction.

This is the most expensive failure mode in modern AI engineering, and it gets worse as you give models more autonomy. A chatbot that hallucinates is embarrassing. An agent that hallucinates books a flight to a city the conference isn't actually in.

The fix isn't a better prompt or a bigger model. It's a search layer.

Why hallucinations are a retrieval problem

Every production LLM has a knowledge cutoff. The flagship models from OpenAI, Anthropic, and Google all freeze sometime in the past. Even when providers ship updates, the model is months behind real events, prices, schedules, and inventory.

The bigger issue is that models don't reliably know when they're past the edge of their knowledge. They don't return null or raise an exception. They generate. And because the loss function rewards fluency, the output is more convincing the further from training data you push it.

For chatbots, this is annoying. For AI agents — programs that take actions based on model output — this is operational debt that compounds:

  • An agent that researches competitors quotes pricing that changed six months ago.
  • A travel agent recommends a flight on an airline that stopped serving the route.
  • A shopping agent generates product specs that match no SKU in the real catalog.
  • A research agent cites papers that were never published.

You can't fix this with prompt engineering because the model literally doesn't have the data. The data has to come from somewhere else. Which brings us to the pattern that has quietly become the default for production agents.

Grounding LLMs with real-time data

The pattern is retrieval-augmented generation (RAG), but the retriever is a search engine instead of a vector database:

User query
   ↓
[Search Engine API] ← fetches live results
   ↓
Top N results + snippets
   ↓
[LLM prompt with results as context]
   ↓
Grounded answer

This is what people mean when they say "ground LLMs with real-time data." You're not fine-tuning. You're not retraining. You're injecting fresh evidence at inference time and asking the model to reason over it.

The interesting question is which retriever. Vector DBs are great when your corpus is fixed and you own it — docs, knowledge bases, internal Confluence. They fall apart the moment you need information about the open web: news from this morning, prices that updated an hour ago, a paper that dropped yesterday.

For that, you need a search engine API for AI agents — something that takes a query and returns structured JSON your LLM can reason over. Not HTML you have to parse. Not a brittle scraper. Real-time, typed, and built for programmatic access.

That's the role I use SerpApi for. It handles the parts of search that are easy to get wrong: rotating proxies, CAPTCHA, parsing the dozen flavors of Google's SERP into a consistent schema, and giving you back organic results, knowledge graph entries, answer boxes, news, and shopping panels as typed JSON.

A working implementation

Here's the smallest end-to-end version of search-grounded generation in Python. It's roughly 60 lines and it works.

Install the two dependencies:

pip install serpapi openai

Set both API keys before running:

export SERPAPI_API_KEY=...
export OPENAI_API_KEY=...

Then the agent itself:

import os
import serpapi
from openai import OpenAI

serpapi_client = serpapi.Client(api_key=os.environ["SERPAPI_API_KEY"])
client = OpenAI()  # reads OPENAI_API_KEY from the environment

def search(query: str, num_results: int = 5) -> dict:
    """Run a Google search and return the full parsed response.

    Returning the full response (not just organic_results) lets callers reach
    for typed SERP features — answer boxes, knowledge graphs, news boxes —
    when they're present. Raises RuntimeError on transport or API errors.
    """
    try:
        results = serpapi_client.search(
            q=query,
            engine="google",  # Easily swap for "bing", "baidu", or "yandex"
            num=num_results,
            hl="en",
        )
    except Exception as e:
        raise RuntimeError(f"Search request failed: {e}") from e

    if "error" in results:
        raise RuntimeError(f"Search returned error: {results['error']}")

    return results

def format_context(results: dict) -> str:
    """Turn organic results into a numbered snippet block the LLM can cite."""
    organic = results.get("organic_results", [])
    lines = []
    for i, r in enumerate(organic, start=1):
        title = r.get("title", "").strip()
        snippet = r.get("snippet", "").strip()
        link = r.get("link", "")
        lines.append(f"[{i}] {title}\n{snippet}\nSource: {link}\n")
    return "\n".join(lines)

def answer(question: str) -> str:
    results = search(question)
    if not results.get("organic_results"):
        return "I couldn't find current information for that question."

    context = format_context(results)

    prompt = f"""You are an assistant that answers questions using only the
search results below. If the results don't contain the answer, say so.
Cite sources by their bracketed number.

Search results:
{context}

Question: {question}

Answer:"""

    completion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.2,
    )
    return completion.choices[0].message.content

if __name__ == "__main__":
    print(answer("Which AI conferences are happening in San Francisco in Q1 2026?"))

A few details that matter more than they look:

Low temperature. The model's job here is to summarize evidence, not invent. temperature=0.2 keeps it tethered to the snippets.

Numbered citations. Forcing the model to cite [1], [2], etc. makes hallucinations much easier to catch in eval. If a citation points at a snippet that doesn't support the claim, you've found a bug.

Explicit "say so" instruction. Without it, the model will paper over gaps in the results. With it, you get an honest "results don't cover this" — which is often more useful than a confident wrong answer.

The raw response from the search call looks like this — useful to know when you start parsing more than just organic results:

{
  "search_metadata": { "status": "Success", "id": "..." },
  "search_parameters": { "engine": "google", "q": "..." },
  "organic_results": [
    {
      "position": 1,
      "title": "Example Conference 2026 — schedule and venue",
      "link": "https://example.com/conf",
      "snippet": "Annual gathering for engineers building AI systems ...",
      "displayed_link": "example.com › conf"
    }
  ],
  "knowledge_graph": { ... },
  "answer_box": { ... },
  "related_questions": [ ... ]
}

The fields beyond organic_results are where this approach gets interesting.

Advanced patterns

Multi-step retrieval

A single search is rarely enough for non-trivial questions. Real agents do something closer to:

Search → extract entities → search those entities → synthesize

For the conferences agent from earlier, the first search returns candidate conferences. A second pass searches each one for venue, dates, and ticket prices. The LLM coordinates the loop, deciding when it has enough evidence to answer.

The trick is bounding the loop. I cap retrieval at three rounds and 12 total search calls per user question. Anything beyond that is almost always a sign the agent is looping on an unanswerable question, and the right behavior is to surface that to the user.

Caching with query-shape-aware TTLs

Search isn't free, and most production agents repeat themselves more than you'd expect. A simple Redis cache keyed by hash(query) knocks 30-50% off your search bill in my experience.

The non-obvious part is the TTL. "iPhone 17 price" and "history of TCP" both come through the same code path but want very different caching policies:

TTL_BY_CATEGORY = {
    "news":        60 * 5,        # 5 minutes
    "price":       60 * 15,       # 15 minutes
    "events":      60 * 60,       # 1 hour
    "reference":   60 * 60 * 24,  # 1 day
    "evergreen":   60 * 60 * 24 * 7,
}

You can classify the query with a small prompt to a cheap model, or with a regex on temporal markers ("today", "latest", "this week" → short TTL). Both work fine.

It's also worth knowing that SerpApi caches on its end, which compounds nicely with your own layer: identical requests are served from its cache for up to an hour, with the cache resetting on the clock hour rather than as a rolling window. For genuinely real-time queries where even that is too stale, bypass it explicitly by passing no_cache="true" in your search call.

Structured SERP features

The single biggest accuracy lift I got after the basic implementation was switching from "summarize the top 5 organic results" to "extract from typed SERP features when present, fall back to organic otherwise."

Most queries about facts, prices, times, conversions, and definitions trigger Google's answer box or knowledge graph. Those are structured fields. Feeding them directly to the model — and telling the model these fields are higher-confidence than free-text snippets — drops the hallucination rate noticeably.

def extract_evidence(results: dict) -> dict:
    return {
        "answer_box": results.get("answer_box"),
        "knowledge_graph": results.get("knowledge_graph"),
        "organic": results.get("organic_results", [])[:5],
    }

For specialized domains, swap the engine. The same client surface works for Google News when you're answering "what happened today", Google Scholar for academic questions, Google Shopping for product agents, and YouTube for video-grounded retrieval. The full list and parameter docs live in the SerpApi search endpoint reference.

Rate limits and graceful degradation

Every search engine API has limits. The cheap mistake is letting a single failed search take down a user-facing response. The right behavior is exponential backoff on retryable errors, then a documented fallback:

import time
from random import uniform

def search_with_retry(query: str, max_attempts: int = 3) -> dict:
    for attempt in range(max_attempts):
        try:
            return search(query)
        except RuntimeError:
            if attempt == max_attempts - 1:
                raise
            backoff = (2 ** attempt) + uniform(0, 1)
            time.sleep(backoff)

This retries blindly on any RuntimeError. In production you'll want to distinguish transient failures (network, 429) from permanent ones (auth, malformed query) so you don't burn retries on errors that will never succeed.

For agents serving real users, pair this with a circuit breaker: after N consecutive failures, stop calling search for 60 seconds and route the LLM to a "I can't access search right now, here's what I knew at training time, please verify" response. Failing visibly beats failing silently.

What this gets you

Search-grounded generation is not a 10x architecture. It's a small structural change — a hundred lines of code in most stacks — and it eliminates the largest category of bugs in agent products: the confident wrong answer.

The shape of the pattern is now standard. The interesting work is upstream of it: choosing which retriever, designing prompts that cite cleanly, caching with the right TTLs, and deciding when an agent should refuse to answer instead of guessing.

If you're building agents and haven't put a search layer between your users and your LLM, that's probably the highest-leverage change you can make this week.

And if you reach for a hosted search layer rather than rolling your own, it's worth skimming the SerpApi integrations page — there are official SDKs across most languages, so wiring one into an existing stack is usually a few lines rather than an afternoon.