Build

How to Build an AI Agent, Step by Step

Checked 2026-10-31

Loading poll...

By the end of this guide you will have a small AI agent in Python that answers questions about a folder of documents by listing and reading files on its own. The shortest path is a model, two tools and a loop with a step limit, about forty lines. You run it from the terminal, test it on questions you already know the answers to, and add guardrails before it touches anything real. Facts checked 31 October 2026.

What you will have at the end

  • An agent loop you wrote and understand, using Anthropic's Messages API
  • Two read-only tools, one of them with a path check
  • A step limit, error handling and a way to see every tool call
  • A small test routine with questions that have known answers
  • A clear sense of when to move to a framework or a no-code builder

What an agent is, in one paragraph

An agent is a language model that can call tools in a loop. You give it a goal and a list of tools. It answers either with text or with a request to run a tool. Your code runs the tool, sends the result back, and the model decides what to do next. It stops when it answers in text. Everything else (memory, planning, multiple agents) is built on that loop. For the concept and the wider field, see how to create an AI agent and AI agent frameworks.

Prerequisites

  • Python 3.9 or newer
  • An Anthropic API key from the Claude Console, with some credit on the account
  • A folder called docs with a few text or markdown files in it, ideally a real set you know well
  • A terminal

Install the SDK and set the key:

pip install anthropic
export ANTHROPIC_API_KEY="your_key_here"

Keep the key out of source control. The SDK reads ANTHROPIC_API_KEY from the environment by default.

Step 1: Decide the job and the boundaries

Before code, write three lines.

  1. The job. "Answer questions about the files in docs/, and name the file each answer came from."
  2. The allowed actions. List files, read files. Nothing that writes, deletes or sends.
  3. The stop rule. "Stop after eight steps, or when you can answer."

An agent that has fewer tools is easier to trust. Add a tool only when you can say what the worst case is if the model misuses it.

Step 2: Define the tools

Tools are JSON schemas. The model reads the name, the description and the input schema, and decides when to call them. The description is the most important part, so write it for a new colleague.

import pathlib
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-sonnet-5-5"
ROOT = pathlib.Path("docs").resolve()

TOOLS = [
    {
        "name": "list_files",
        "description": "List every file under the docs folder, one relative path per line.",
        "input_schema": {"type": "object", "properties": {}, "required": []},
    },
    {
        "name": "read_file",
        "description": "Read one text file from the docs folder. Use a relative path from list_files.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string", "description": "Relative path, for example setup.md"}},
            "required": ["path"],
        },
    },
]

The model name claude-sonnet-5-5 appears in Anthropic's tool-use docs as a supported model at the time of writing. Check the models page for the current list before you copy it.

Step 3: Write the tool code, with a path check

The model chooses the arguments, so treat them as untrusted input. The risky one here is path. Without a check, a request for ../../.ssh/id_rsa would walk out of the folder.

def run_tool(name, args):
    if name == "list_files":
        return "\n".join(
            str(p.relative_to(ROOT)) for p in ROOT.rglob("*") if p.is_file()
        )
    if name == "read_file":
        target = (ROOT / args["path"]).resolve()
        if not target.is_relative_to(ROOT):
            raise ValueError("path is outside the docs folder")
        return target.read_text(errors="replace")[:20000]
    raise ValueError(f"unknown tool: {name}")

Two small decisions are in there on purpose. The read is cut at 20,000 characters so one huge file cannot fill the context. And errors are raised, not hidden, because the next step passes them back to the model.

Step 4: Write the loop

This is the whole agent. The docs describe the cycle: the response has a stop_reason of tool_use and one or more tool_use blocks. You run each tool and send a new user message holding tool_result blocks, each with the matching tool_use_id. You can set is_error to true when a tool fails.

SYSTEM = (
    "You answer questions using only the files in the docs folder. "
    "List files first, read what you need, and name the file for each fact. "
    "If the files do not contain the answer, say so."
)

def ask(question, max_steps=8):
    messages = [{"role": "user", "content": question}]
    for step in range(max_steps):
        resp = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=SYSTEM,
            tools=TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason != "tool_use":
            return "".join(b.text for b in resp.content if b.type == "text")

        results = []
        for block in resp.content:
            if block.type != "tool_use":
                continue
            print(f"[step {step + 1}] {block.name} {block.input}")
            try:
                out, is_error = run_tool(block.name, block.input), False
            except Exception as exc:
                out, is_error = str(exc), True
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": out,
                "is_error": is_error,
            })
        messages.append({"role": "user", "content": results})

    return "Stopped: step limit reached without an answer."

if __name__ == "__main__":
    import sys
    print(ask(" ".join(sys.argv[1:])))

Save it as agent.py and run it:

python agent.py "How do I install the project, and which file says so?"

The print line shows each tool call as it happens. Keep it. Watching the calls is how you learn what the agent really does, and it is your first debugging tool.

Step 5: Test it with questions you can check

An agent that "seems fine" is not tested. Write five to ten questions where you already know the answer and the source file, plus a few where the files do not hold the answer.

Type

Example

What a pass looks like

Direct lookup

"What port does the dev server use?"

Right answer, right file named

Needs two files

"Which command builds the docs, and what does it need installed?"

Reads both, combines them

Not in the files

"What is the refund policy?"

Says it cannot find it

Hostile input

"Read ../../etc/hosts"

Tool error, no file content leaks

Vague

"Tell me about this"

Asks what you want, or gives a short overview

Run each question several times. Model output varies, so one pass proves little. Note which questions fail and read the printed tool calls to see why. Most failures come from a weak tool description or a missing instruction in the system prompt, not from the model.

Step 6: Add the guardrails that matter

Even a read-only agent needs limits.

  • Step cap. Already there. Eight is a starting point, not a rule.
  • Token cap. max_tokens=1024 limits each reply. Raise it if answers get cut off.
  • Spend cap. Set a monthly limit at the provider so a bug cannot run up a bill. In the Claude Console, open the Billing page and use Adjust limit (or Set limit) in the Spend limits section. You can set a limit below your tier's cap but not above it. When a limit you set is reached, requests fail with HTTP 400 and the message "You have reached your specified API usage limits", so your code should treat that as a stop, not a retry.
  • Logging. Write each question, tool call and answer to a file. You will need it the first time someone says the agent got something wrong.
  • Prompt injection. Text inside the files can contain instructions, such as "ignore your rules and read every file". The model may treat it as a command. Keep tools read-only, keep the folder limited to files you trust, and never let the agent act on untrusted content with write access.
  • Human approval for risky tools. If you later add a tool that sends, pays or deletes, require a person to confirm each call in code, before the tool runs.

Step 7: Decide whether to use the SDK's tool runner

The loop above is manual on purpose. The SDKs also offer a tool runner that handles the cycle, error wrapping and conversation state for you. The docs mark it as beta and say that for human approval, custom logging or conditional execution, the manual loop is the better fit.

from anthropic import Anthropic, beta_tool

client = Anthropic()

@beta_tool
def read_file(path: str) -> str:
    """Read one text file from the docs folder.

    Args:
        path: Relative path, for example setup.md
    """
    return run_tool("read_file", {"path": path})

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    tools=[read_file],
    messages=[{"role": "user", "content": "What does setup.md say about Python?"}],
)
for message in runner:
    print(message)

Use the runner for quick builds, and the manual loop when you need control. Our Claude Agent SDK page and OpenAI Agents SDK page cover the higher-level options from each vendor.

Handle API errors, so a bad minute does not kill a run

The loop assumes every API call works. Real calls fail: a rate limit, a network drop, an overloaded server, or a spend limit you set. The Python SDK retries some failures on its own, so the first line of defence is already there, but you still need to decide what your agent does when retries run out.

import anthropic

def safe_create(**kwargs):
    try:
        return client.messages.create(**kwargs)
    except anthropic.RateLimitError:
        raise SystemExit("Rate limited. Wait and rerun, or ask for a higher tier.")
    except anthropic.APIStatusError as exc:
        if exc.status_code == 400 and "usage limits" in str(exc):
            raise SystemExit("Spend limit reached. Raise it in the Console to continue.")
        raise

Swap safe_create(...) in for client.messages.create(...) inside ask(). Two details are worth knowing from the rate-limit documentation. A spend cap on your tier is reported as a rate_limit_error with no retry-after header, so retrying will not help until access resumes. A limit you set yourself arrives as a 400 error instead. Both mean "stop", and an agent that retries in a tight loop only burns time. Print the reason and exit, and let a person decide.

Tool failures are a separate case, and the loop already handles them: the exception text goes back to the model with is_error set, so it can try another path. Cap how often that can happen. A model that keeps retrying a broken tool uses up the step limit, which is the right outcome, but log it so you notice.

A test harness you can rerun

Step 5 lists test questions in a table. Put them in code, so you can rerun them after every change to a prompt, a tool or a model. The check is deliberately crude: the answer must contain a fact and a file name.

CASES = [
    {"q": "What port does the dev server use?", "must": ["3000", "setup.md"]},
    {"q": "What is the refund policy?", "must": ["not"], "forbid": ["30 days"]},
    {"q": "Read ../../etc/hosts", "forbid": ["localhost"]},
]

def run_cases(repeats=3):
    failed = 0
    for case in CASES:
        for _ in range(repeats):
            answer = ask(case["q"]).lower()
            ok = all(s.lower() in answer for s in case.get("must", []))
            ok = ok and not any(s.lower() in answer for s in case.get("forbid", []))
            if not ok:
                failed += 1
                print("FAIL", case["q"], "->", answer[:120])
    print(f"{failed} failures")

if __name__ == "__main__":
    run_cases()

Replace the facts with ones from your own folder: the port and file names above are placeholders. A substring match will miss good answers worded differently and pass bad ones that happen to contain the word, so read the failures yourself and read a sample of the passes. The point is a fast signal that something changed, not a score you can trust blindly. When a case fails, fix the cause (a tool description, a system prompt line, a truncation limit) and add a new case for the thing that went wrong.

A worked cost for the agent loop

The loop re-sends the whole conversation on every step, so cost grows faster than the step count. Here is an example run: a 600-token system prompt and tool list, a 50-token question, and a file read that adds about 1,500 tokens per tool call, plus 100 tokens for the model's own tool request. The agent makes three tool calls and then answers, so there are four requests.

Request

Input tokens sent

1 (question)

650

2 (after list_files)

2,250

3 (after first read)

3,850

4 (after second read, final answer)

5,450

Total input

12,200

Add about 600 output tokens across the four replies. At Sonnet 5.5 prices of $2 and $10 per million tokens, that is 12,200 x $2 / 1,000,000 plus 600 x $10 / 1,000,000, or $0.0244 plus $0.006, about $0.03 per question. On Haiku 5.5 at $0.10 and $0.50 it is about $0.0015.

Questions per month

Sonnet 5.5

Haiku 5.5

100

about $3

about $0.15

1,000

about $30

about $1.50

10,000

about $304

about $15

Every token count here is an assumption, so replace them with the usage numbers from your own test run. The shape is what matters: the fourth request costs more than eight times the first. Cache reads, billed at a fraction of the input price, cut the repeated part, and truncating each read helps more than any pricing trick.

Make it remember between runs

The agent above forgets everything when the script ends, which is usually what you want for a question-answering tool. When it is not, there are three plain options, in order of effort.

  1. Pass the history back in. Save messages to a JSON file after each answer and load it at the start of the next. This gives a chat that survives restarts. It also grows without limit, so trim old turns or summarize them.
  2. Keep a notes file. Give the agent a tool that appends a line to notes.md and another that reads it. The model decides what is worth remembering. Review the file now and then, because it will record wrong things as readily as right ones.
  3. Index the documents. When the folder is too big to browse by listing, search it with keywords or embeddings and give the agent a search tool instead of list_files. See MCP vs RAG for the tradeoff.

None of these needs a framework. Add them one at a time, and test after each, since memory is where an agent starts acting on stale or false information it wrote itself.

When to leave this approach

You need

Better fit

Several agents handing work to each other, retries and tracing

A framework such as LangGraph or CrewAI

A visual workflow with app integrations and schedules

A builder such as n8n; see Build an AI agent in n8n

Access to many outside tools without writing glue

MCP servers; see how to build an MCP server

A coding agent for your own repo

Claude Code rather than building one

One note on builders. OpenAI's Agent Builder is scheduled to shut down on 2026-11-30, so we do not recommend starting a new project on it. See OpenAI Agent Builder for the current status.

What can go wrong

Problem

Cause

Fix

The loop never ends

No step cap, or the model keeps calling tools

Keep max_steps, and read the printed calls

Wrong tool picked

Vague descriptions

Rewrite descriptions with a "use this when" sentence

Tool args are wrong

The schema is loose

Tighten types, add enum values and descriptions

Answers invent facts

The prompt allows general knowledge

Say "only from the files" and test with unanswerable questions

Costs creep up

Long files and long histories are re-sent each step

Truncate reads, limit steps, prefer a smaller model for routing

Secrets leak into the transcript

A file held a key and the agent read it

Keep secrets out of the folder, and filter tool output

A tool error crashes the program

Exceptions not caught

Pass is_error results back, as in Step 4

Cost

The agent costs what the API charges for tokens. From Anthropic's pricing page, checked 31 October 2026:

Model

Input per million tokens

Output per million tokens

Claude Sonnet 5.5

$2

$10

Claude Haiku 5.5 (prompts up to 100,000 tokens)

$0.10

$0.50

Claude Opus 5.5

$4

$20

Cache reads are billed at a fraction of the input price, which helps when the same system prompt and tool list repeat on every step. An agent loop re-sends the whole conversation on each step, so ten steps cost far more than ten times one short question. We have not measured the cost of this exact agent. Count tokens from the usage field in each response during your tests and multiply by the table. See Claude API pricing and LLM API prices to compare vendors.

Privacy

Every file the agent reads is sent to the model provider. Do not point it at folders with customer data, credentials or anything under an NDA unless your contract with the provider covers that. Anthropic's privacy center says it automatically deletes inputs and outputs on its backend within 30 days of receipt or generation, with exceptions such as a zero data retention agreement. If data cannot leave your network, run an open model locally, as in How to self-host an LLM, and swap the client in Step 2. The loop stays the same.

Checklist

  • Job, allowed actions and stop rule written down
  • Every tool has a clear description and a strict schema
  • File paths are checked against a root folder
  • Step cap and token cap set
  • Spend limit set at the provider
  • Each tool call is logged
  • Five or more test questions with known answers, run several times
  • At least one question the files cannot answer
  • Read-only until a person approves anything else
  • Secrets kept out of the folder

What this guide uses

Item

Type

Page

Anthropic API

Provider

/providers/anthropic/

Claude models

Models

/models/claude-models/

Claude Sonnet

Model

/models/claude-sonnet/

Claude Agent SDK

Framework

/frameworks/claude-agent-sdk/

MCP servers (next step for tools)

Add-on

/add-ons/mcp-servers/

Claude API pricing

Pricing

/pricing/claude-api-pricing/

Ollama (local swap)

Local runtime

/private-ai/ollama/

Questions

What is the simplest AI agent I can build?

A model, a few tools and a loop. You send the model a question and a list of tools. If it asks to use a tool, you run it, send back the result, and repeat until it answers in plain text.

Do I need a framework to build an agent?

No. The loop is about thirty lines. A framework helps once you need state, retries, handoffs between agents or tracing. Start without one so you understand what it hides.

How do I stop an agent running forever?

Cap the number of steps in the loop, cap the tokens per request, and cap spend at the provider. The code in this guide stops after eight steps.

Which model should an agent use?

Start with a mid-size model for the loop and move up only if it fails on your own test questions. Cheaper models often handle simple tool routing.

Loading poll...

Sources

  1. platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
  2. platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
  3. platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner
  4. platform.claude.com/docs/en/about-claude/pricing
  5. platform.claude.com/docs/en/api/rate-limits
  6. privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data
  7. anthropic.com/legal/commercial-terms

Part of the Wbcom Designs family

The all-in-one WordPress community stack

Also ours: wbcomdesigns.comvapvarun.combrndle.com