Let your AI agent search the web: tool calling with a free web search API for LLMs, the agent loop, prompt injection defenses, and the limits to plan for.
A language model knows only what was in its training data, and that data stops at a cutoff date. To answer questions about anything newer, or to cite where an answer came from, the model has to search the web while it works. You give it that ability with two tools: one that searches and returns ranked pages, and one that reads a page's text. The model decides when to call them, your code makes the call, and the results go back into the conversation.
This pattern is called tool calling, or function calling, and when the thing being retrieved is the web, it's retrieval-augmented generation (RAG) with a search engine as the retriever. The worked example here uses the Carpathian Search API, which answers both tools as JSON and is free up to a monthly allowance, but the pattern carries over to any search API that returns ranked results and page text.
Why can't a model search the web on its own?
A model generates text from its weights. It has no network connection, so it can't fetch anything by itself. When a chat app "browses the web," the app is running the searches and pasting the results back into the conversation.
In your own application, you build that piece. You describe a tool to the model, the model answers with a request to call it, your code runs the search, and you hand the model the results. That gives you control over which search engine is used, how many results the model sees, what it reads, and what it all costs.
What does a web search API for AI agents need to return?
A search API built for people returns a page of links. A model needs a little more structure so it can choose well and spend as few tokens as possible.
Ranked results with a title, URL and snippet, enough for the model to pick which pages to read.
A published date, so the model can tell an old answer from a new one.
Clean page text, without navigation, ads and footers. Raw HTML wastes most of a context window on markup.
Predictable errors and rate limits, so your loop can back off instead of failing.
Each Carpathian Search result carries a rank, title, URL, site, snippet, the character offsets of the matched words in the snippet, and the date the page says it was published. Reading a page returns its title, description, language, published date and main text, without navigation, ads or footers, up to 20,000 characters by default.
How do you give a model a search tool?
You describe each tool with a name, a description the model reads, and a JSON schema for its arguments. These two come from the API's own documentation and work with any model that supports tool calling:
[
{
"name": "search_web",
"description": "Search the web. Returns ranked pages with a title, URL and snippet. Use specific words; put exact phrases in double quotes; add site:example.com to stay on one site.",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "The search, in plain words." },
"limit": { "type": "integer", "description": "How many results, 1 to 10." }
},
"required": ["query"]
}
},
{
"name": "read_page",
"description": "Read the full text of a page returned by search_web.",
"parameters": {
"type": "object",
"properties": {
"url": { "type": "string", "description": "A url from a search_web result." }
},
"required": ["url"]
}
}
]
Then you write the two functions your code runs when the model asks. Keep the key in an environment variable on your server, never in a browser or a mobile app.
import os
import requests
SEARCH = "https://search.carpathian.ai/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["CARPATHIAN_SEARCH_KEY"]}
def search_web(query, limit=5):
r = requests.post(f"{SEARCH}/search", json={"query": query, "limit": limit}, headers=HEADERS, timeout=10)
r.raise_for_status()
return [
{"title": x["title"], "url": x["url"], "snippet": x["snippet"], "published": x["published"]}
for x in r.json()["results"]
]
def read_page(url):
r = requests.post(f"{SEARCH}/document", json={"url": url}, headers=HEADERS, timeout=10)
if r.status_code == 404:
return {"error": "That page is not in the index. Pick another result."}
r.raise_for_status()
doc = r.json()
return {"title": doc["title"], "url": doc["url"], "published": doc["published"], "text": doc["text"]}
Trimming each result to the fields the model needs keeps the conversation short, which makes every later turn cheaper and faster.
What does the agent loop look like?
The loop is the same whatever model you use. The model asks, your code answers, and the conversation grows until the model has what it needs.
Send the user's question with the two tool definitions.
If the model answers with a tool call, run the matching function with its arguments.
Append the result to the conversation as the tool's answer, and send it back.
Repeat until the model answers in text, and stop it after a fixed number of tool calls so a confused model can't loop forever.
A short system prompt does most of the steering: search, read the one to three most promising results, answer from what you read, and cite the URL. Models that read a couple of pages answer better than models that skim twenty snippets.
The search request takes a few options that help an agent in particular:
trail sends up to five earlier queries from the same session, so results lean toward where the conversation is heading.
exact: true turns off spelling correction, for part numbers, names and code.
lang weighs down pages that state another language.
Query operators work in the query itself: "exact phrase", -word to exclude a word, and site:example.com to stay on one site.
How do you keep web pages from steering your model?
Any page your agent reads can contain text written to give the model instructions. OWASP ranks prompt injection first in its Top 10 for LLM Applications 2025, and describes this case directly: "Indirect prompt injections occur when an LLM accepts input from external sources, such as websites or files." A search tool is exactly that kind of input.
You can't filter every attack out of page text, so design the agent so a successful one does little harm.
Tell the model page text is data. Say in the system prompt that tool results are content to read, never instructions to follow, and wrap each page so it's clearly marked as a tool result.
Keep reading and acting apart. An agent that reads the web shouldn't also hold tools that send email, move money or change records without a person confirming.
Show your sources. Cite the URLs the answer came from, so a person can check a claim that looks wrong.
Cap what goes in. Limit results per search and pages per question, which also caps cost.
What does it cost, and what are the limits?
Searches and page reads through the Carpathian Search API are free up to a monthly allowance for each organization, then billed per 1,000 at the rate on the service page. Only a request answered with a 200 counts, so errors and refusals cost nothing.
Per-key limits. Each key has a limit of requests a minute and a day, which you set. Past either, the API answers 429 with a Retry-After header, so a bug that loops can't run up a bill.
Keys shown once. Carpathian keeps only a hash of the key. If one leaks, rotate it, and the old key stops working within a minute.
Request size. A query is up to 512 characters, a search returns up to 50 results, and paging goes up to an offset of 200.
What doesn't it do?
The API reads only pages already in the index. It never fetches a URL because you asked for it, so a page published this morning, or a site the crawler hasn't reached, answers 404 not_indexed. If your agent needs to read any live URL on demand, you'll need your own fetcher alongside it, built with care, since fetching addresses a model chose is its own security risk.
Results come from Carpathian's own index, not a resold feed from a larger engine, so coverage differs from what you're used to. Try your own topics before you build on it. Page text is capped at 20,000 characters by default, and the API returns pages and text, not images or video.
Get a key and try it
Make a Search API key in Search Engine Console, drop it into the two functions above, and your model can search the web on its next turn. New to Carpathian? Create an account and enable Search Engine Console from Services first. The Search API reference covers every field, and https://search.carpathian.ai/llms.txt hands the same reference to your coding assistant. If you'd rather run the model on Carpathian too, the AI API speaks the OpenAI format.
How to Let AI Search the Web - FREE API | Carpathian