Quick answer: To build an AI agent you need four things: a goal, a model that can call tools, a set of tools the model can use (a web fetch and search tool is the most common), and a loop that lets the model plan, act, observe the result, and decide what to do next. Everything else is refinement. This tutorial walks through each piece with runnable Python you can adapt.
An agent is not a magic abstraction. It is a while loop around a language model that can call functions. If you understand that sentence, you already understand the architecture. The rest is engineering discipline: clear tools, sane stop conditions, and guardrails that keep the loop from running forever or doing something you will regret.
If you want the conceptual grounding first, read what is an AI agent. This post is the build.
Step 1: Define the goal
Start with a task narrow enough to test but real enough to matter. "Answer any question" is not a goal. "Given a company name, find its current pricing and return it as a short summary with sources" is a goal. It has a clear input, a clear output, and a way to check whether the agent succeeded.
A good first goal has three properties:
- Checkable. You can look at the output and say whether it is right.
- Bounded. It finishes in a handful of steps, not a hundred.
- Tool-shaped. It needs an action the model cannot do alone, like reading a live web page.
The pricing-lookup example needs current web data, which a model cannot produce from its training weights. That makes it a genuine agent task rather than a plain prompt.
Step 2: Pick a model
You want a model that supports tool calling and reasons well enough to plan multiple steps. For this tutorial I use Claude Opus 4.8 (claude-opus-4-8), which handles long-horizon tool use and adaptive thinking. Any capable tool-calling model works; the loop structure is the same.
Install the SDK:
pip install anthropic requests
Set your keys as environment variables so they never live in code:
export ANTHROPIC_API_KEY=sk-ant-...
export LINKSC_API_KEY=lsc_...
Step 3: Give it tools
The model needs a way to reach the live web. Rather than build a fetcher and a search engine yourself (rendering, proxies, parsing, and anti-bot handling are their own full-time job), point the agent at one API. link.sc exposes a fetch endpoint that turns any URL into clean markdown and a search endpoint that returns structured results with titles, URLs, and descriptions you can feed straight into fetch.
Here is what a raw call looks like:
curl https://api.link.sc/v1/search \
-H "x-api-key: lsc_..." \
-H "Content-Type: application/json" \
-d '{"q": "Acme Corp pricing", "engine": "google"}'
curl https://api.link.sc/v1/fetch \
-H "x-api-key: lsc_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.example/pricing", "format": "markdown"}'
Now wrap those as tool functions and declare them to the model. The declarations tell the model when each tool applies:
import os
import json
import requests
import anthropic
LINKSC_KEY = os.environ["LINKSC_API_KEY"]
LINKSC_HEADERS = {"x-api-key": LINKSC_KEY}
def web_search(query: str) -> str:
r = requests.post(
"https://api.link.sc/v1/search",
headers=LINKSC_HEADERS,
json={"q": query, "engine": "google"},
timeout=60,
)
r.raise_for_status()
results = r.json()["serpData"]["results"][:5]
lines = [
f"{item['title']}\n{item['targetUrl']}\n{item['description']}"
for item in results
]
return "\n\n".join(lines)[:8000]
def web_fetch(url: str) -> str:
r = requests.post(
"https://api.link.sc/v1/fetch",
headers=LINKSC_HEADERS,
json={"url": url, "format": "markdown"},
timeout=60,
)
r.raise_for_status()
return r.json()["content"][:8000]
TOOLS = [
{
"name": "web_search",
"description": "Search the web and return the top results as titles, URLs, and descriptions. Call this when you need to find pages relevant to a query, then use web_fetch to read a result in full.",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
},
{
"name": "web_fetch",
"description": "Fetch one URL and return it as clean markdown. Call this when you already have a specific URL to read.",
"input_schema": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
]
DISPATCH = {"web_search": web_search, "web_fetch": web_fetch}
Truncating each result to 8000 characters is a small but important guardrail: it stops one enormous page from blowing your context window.
Step 4: Write the agent loop
This is the heart of the agent: plan, act, observe, repeat. The model plans by deciding which tool to call, acts when your code runs the tool, and observes when you feed the result back. It keeps going until it stops asking for tools.
client = anthropic.Anthropic()
def run_agent(goal: str, max_steps: int = 8) -> str:
messages = [{"role": "user", "content": goal}]
for step in range(max_steps):
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
thinking={"type": "adaptive"},
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if b.type == "text")
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
fn = DISPATCH[block.name]
try:
output = fn(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
except Exception as e:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
messages.append({"role": "user", "content": results})
return "Stopped: reached the step limit without finishing."
if __name__ == "__main__":
print(run_agent("Find Acme Corp's current pricing and summarize it with the source URL."))
A few things worth noticing. The full response.content gets appended, not just the text, because it carries the tool_use blocks the model needs to track. Every tool_result includes the matching tool_use_id. Adaptive thinking lets the model decide how much to reason between steps without you tuning a token budget.
Step 5: Add stop conditions and guardrails
The loop above already has two guardrails: a step limit and per-result truncation. Real agents need a few more. Here is how the common ones map to failure modes.
| Guardrail | What it prevents | How to add it |
|---|---|---|
| Max step count | Infinite loops, runaway cost | max_steps cap in the loop |
| Result truncation | Context window overflow | Slice tool output before returning |
| Tool error handling | One failed call killing the run | is_error: True result, let the model retry |
| Timeout per tool call | Hanging on a slow endpoint | timeout= on the request |
| Human approval for writes | Destructive or irreversible actions | Gate write tools behind a confirmation |
The last one matters most as agents gain power. Reading a web page is safe to run automatically. Sending an email, deleting a record, or spending money is not. Promote those actions to dedicated tools and require a human to approve them before your code executes the call. Reversibility is the test: if the action is hard to undo, gate it.
Step 6: Test and iterate
Run the agent on ten variations of your goal, not one. Watch where it wanders. Common early problems and their fixes:
- The model answers without searching. Tighten the tool description to say when to call it, or add a line to the goal instructing it to verify with the web first.
- It loops on the same failing fetch. Add the failed URL to the observation and tell it not to retry the same page.
- It stops too early. Raise the step limit, or make the goal spell out what a complete answer contains.
Once the single agent is reliable, you can chain search and fetch more deliberately. The pattern of combining both into one clean pass is covered in combine search and fetch in one pipeline.
What you built
You now have a working agent: a model with two web tools, a plan-act-observe loop, and guardrails that keep it bounded. The same skeleton scales up. Add more tools, swap the goal, raise the limits, and you have a research assistant, a monitoring bot, or a data-enrichment worker. The loop does not change; only the tools and the task do.
The single biggest lever is the quality of the web data flowing in. An agent reasoning over clean markdown from the live web beats one guessing from stale training data every time.
Ready to give your agent reliable web access? Get started free with link.sc and add fetch and search to your agent in minutes.