LlamaIndex is built around the idea of indexing your own documents and querying them. That is exactly the right model when your knowledge lives in a fixed corpus: a folder of PDFs, a Notion export, a product manual. It is exactly the wrong model when the answer depends on something that changed this morning. A vector index built yesterday cannot tell you today's price, this week's release notes, or a competitor's new landing page.
The fix is not a bigger index. It is a tool. LlamaIndex lets you hand an agent a Python function and call it a tool, and the agent decides when to invoke it. So you can give an agent two capabilities, search the web and fetch a page, and let it reach for live data only when the question actually needs it. This post wires that up end to end with FunctionTool and the link.sc API.
Why not just index the web?
The instinct with LlamaIndex is to shove everything into a VectorStoreIndex and query it. For the open web, that breaks down fast. To keep a web index fresh you would have to re-crawl and re-embed the internet on a schedule, which is neither possible nor affordable. And most questions do not need the whole web, they need two or three pages that are relevant right now.
That is the difference between retrieval over a static corpus and live web search. Static RAG is a good fit for a known, slow-moving body of text. Live web access is a good fit for the long tail of "what is true at this moment." An agent with tools handles the second case cleanly: it runs a search when it decides one is warranted, reads the results, and answers from what it actually read. If you want the static-corpus version too, our RAG pipeline walkthrough covers the fetch, clean, chunk, embed loop in detail. Here we are doing the tool-calling version.
The two functions
Everything starts with two plain Python functions. One searches, one fetches. link.sc gives you a single endpoint for each: /v1/search takes a query and returns results with full-page markdown already attached, and /v1/fetch takes a URL and returns clean markdown. Because the output is markdown rather than raw HTML, it drops straight into an LLM context without eating your token budget on navigation and cookie banners.
import requests
HEADERS = {"x-api-key": "lsc_your_key_here"}
def web_search(query: str) -> str:
"""Search the web for current information.
Use this when you need up-to-date facts you do not already know.
Returns the top results as clean markdown, with titles and URLs.
"""
r = requests.post(
"https://api.link.sc/v1/search",
headers=HEADERS,
json={"q": query},
)
results = r.json()["results"][:3]
return "\n\n---\n\n".join(
f"# {x['title']}\n{x['url']}\n\n{x['content']}" for x in results
)
def fetch_url(url: str) -> str:
"""Fetch a single web page and return its content as clean markdown.
Use this to read a specific URL in full, for example a result the
search returned that looks most relevant.
"""
r = requests.post(
"https://api.link.sc/v1/fetch",
headers=HEADERS,
json={"url": url, "format": "markdown"},
)
return r.json()["content"]
The docstrings are not decoration. LlamaIndex turns them into the tool descriptions the model reads when it decides which tool to call, so write them for the model. Say what the tool does and when to use it. A vague docstring produces an agent that reaches for the wrong tool or never reaches at all.
Turning functions into tools
FunctionTool.from_defaults wraps each function, reading the name, signature, and docstring to build the schema the LLM sees.
from llama_index.core.tools import FunctionTool
search_tool = FunctionTool.from_defaults(fn=web_search)
fetch_tool = FunctionTool.from_defaults(fn=fetch_url)
That is the whole conversion. The type hints (query: str, url: str) become the input schema, so the model knows to pass a string, and LlamaIndex validates the call before your function ever runs.
Handing the tools to an agent
Now give the tools to an agent and pair it with an LLM. The example uses the Anthropic integration with Claude Opus 4.8, whose model id is claude-opus-4-8, but any tool-calling model works.
import asyncio
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.anthropic import Anthropic
llm = Anthropic(model="claude-opus-4-8")
agent = FunctionAgent(
tools=[search_tool, fetch_tool],
llm=llm,
system_prompt=(
"You answer questions using the search and fetch tools. "
"Search when you need current information, then fetch the most "
"relevant result to read it in full. Answer only from what the "
"tools return. If they do not contain the answer, say you could "
"not find it. Cite the source URL for every claim."
),
)
async def main():
response = await agent.run("What changed in the latest Python release?")
print(response)
asyncio.run(main())
The agent loop runs itself. The model reads the question, calls web_search, scans the returned markdown, often calls fetch_url on the most promising result to read the whole page, and then writes an answer grounded in that text. You did not script any of those steps. You defined the capabilities and the policy, and the agent chose the sequence.
The system prompt does the grounding
Tools alone do not stop a model from guessing. The instruction that does is the one telling it to answer only from what the tools return and to admit when the answer is not there. That single sentence flips the job from recall (which is where hallucinations live) to reading comprehension over text you handed it, which is what these models are genuinely good at. Requiring a source URL per claim makes wrong answers cheap to catch, because you can click the link and check. The same grounding discipline applies to any agent framework, as we cover in how to give an AI agent internet access.
What about the query engine?
If you prefer LlamaIndex's query-engine abstraction, you can still slot live search in. Fetch a handful of pages for a query, build a throwaway VectorStoreIndex over just those documents, and expose its .as_query_engine() through a QueryEngineTool. That gives you retrieval quality over the fresh pages while keeping the agent interface. For most live-web cases the plain FunctionTool route above is simpler and cheaper, because you skip embedding pages you will read once and discard. Reach for the ephemeral index only when a single question pulls in more text than fits comfortably in context.
Cost and the DIY alternative
Two meters run here: LLM tokens and web-data calls. Keep both low by returning markdown instead of HTML, capping the search to the top few results, and caching pages that rarely change. On link.sc a page or a search is roughly one credit, and there are 500 free credits every month, enough to build and test a real agent before you pay anything.
You can also build the fetch-and-clean layer yourself with a headless browser fleet, a proxy pool, and an HTML-to-markdown pipeline. That is real infrastructure to own forever, and none of it is your product. link.sc collapses it into the two functions above, and it is open core, so you can self-host the same thing if you would rather own the stack. Either way, your LlamaIndex agent stops being frozen at its training cutoff and starts reading the web on demand.
Ready to give your LlamaIndex agent live web access? Grab an API key at link.sc/register, 500 free credits a month to start.