AI·Frontier
← Back to Home
AI Agents

Building Your First Agent

Building Your First Agent

Building Your First Agent

Most tutorials about AI agents start with a framework. You install a package, paste a prompt, and watch a demo do something impressive. Then you try to make it do your actual job, and it falls apart. The reason is rarely the model. It is that you never learned the mechanics — the loop of perceiving, deciding, and acting — that every agent, fancy framework or not, is built on. This article walks you through a from-scratch agent you can deploy this weekend, using nothing more than Python and a large language model API. By the end you will understand exactly where the intelligence lives, and just as importantly, where it does not.

Start With the Goal, Not the Tools

Every good agent begins with a sharply defined goal. Not "be helpful," but something you can test: monitor my inbox, triage emails by urgency, and draft a reply for anything marked urgent. When the goal is measurable, you can tell when the agent is working and when it has drifted off task. Write the goal down before you write any code. For our walkthrough, we will build a small but genuinely useful agent: a ticket-summarizer that reads support tickets from a file, classifies each one by category, assigns a priority, and writes a one-paragraph summary a human can act on. It is small enough to finish in an afternoon and real enough to replace a chore you actually do.

"An agent is not a chatbot that gets to press the enter key. It is a loop with a memory, a purpose, and a set of tools it can reach for. Build the loop first; the intelligence will follow."

The Agent Loop, In Three Steps

Strip away every abstraction and an agent is just a while-loop with three phases. You will write each one by hand:

  • Perceive. Read the current state: the tickets in your queue, the fields you care about, any results from your last action.
  • Decide. Send that state plus your goal to the model, and ask it to choose the next step — including the option to say "done."
  • Act. Execute the chosen tool, capture the result, and feed it back into the next perception step.

That is the whole skeleton. Everything fancy — memory, planning, multi-agent crews — is an extension of these three moves. Let us make the loop concrete. Here is the core of a minimal perception step:

def perceive(queue_dir):
    tickets = []
    for path in sorted(Path(queue_dir).glob("ticket_*.json")):
        with open(path) as fh:
            tickets.append(json.load(fh))
    return {
        "pending": len(tickets),
        "tickets": [
            {"id": t["id"], "text": t["body"][:1200], "unread": not t.get("read")}
            for t in tickets
        ],
    }

Notice what this function does: it converts messy files into a compact, model-friendly snapshot. Your perception step should always normalize the world before the LLM sees it — truncate long fields, drop sensitive data, and present only what the decision needs. Garbage perception is the single most common reason agents make bad decisions.

Diagram of the perceive, decide, act agent loop

The Decision Step: Ask for One of a Few Things

The model is fragile the moment you let it free-form. The trick is to constrain it. Give the model a small, fixed set of possible actions and ask it to pick one, returning structured output you can parse without guessing. For the ticket summarizer, those actions are classify, escalate, draft_reply, and done. Here is the decision call:

def decide(model, state):
    prompt = f"""You are a support triager. We have {state['pending']} pending tickets.

For the first unread ticket, choose exactly one action:
- classify  (set category and priority)
- escalate  (this needs a human right now)
- draft_reply (respond to the customer)
- done (no unread tickets left)

Return JSON only: {{"action": "...", "ticket_id": "...", "reason": "..."}}
Ticket: {state['tickets'][0]}"""
    return model.complete(prompt, response_format="json")

Two details matter. First, the prompt lists a closed set of actions and demands JSON, which keeps the model honest and your parser simple. Second, the model sees only one ticket at a time — that keeps the context small, the cost low, and the decisions independent. When you start an agent this way, later improvements become local: you add actions, not new architectures.

The Action Step: Real Tools, Real Consequences

An action is just a function that takes arguments and returns a result. The discipline is to make actions small, idempotent where possible, and wrapped so they never blow up the loop. Here is how we execute the classify action:

def act(action, args, model):
    if action == "classify":
        ticket = load_ticket(args["ticket_id"])
        parsed = model.complete(
            f"Classify as billing|bugs|sales|other. Priority 1-5. {ticket['text']}",
            response_format="json",
        )
        ticket.update(category=parsed["category"], priority=parsed["priority"])
        save_ticket(ticket)
        return {"ok": True, "ticket_id": ticket["id"], "next": "perceive"}
    # ... escalate, draft_reply, done ...

The full loop ties these together. Keep the loop's error handling brutal and visible: if a tool raises, log it and move on rather than letting the agent retry silently forever. A runaway loop that burns tokens in the dark is the failure mode every beginner hits.

Memory: The Part Everyone Forgets

A single pass through the loop has no memory. For the summarizer, that is fine — each ticket is independent. But the moment your agent must do something multi-step, you need to store what happened. The cheap, robust approach is a transcript: append every decision and its outcome to an ordered list, and feed the tail of that list back into the next prompt. Six steps of history is a sensible default; too little and the model forgets what it just did, too much and you bloat the prompt and raise your costs.

transcript = []  # [{step, action, args, result, at}]
def remember(entry):
    transcript.append(entry)
    return transcript[-6:]  # keep the last six steps in context
A running agent processing a queue of tickets with logging

Deploy This Weekend

Deployment is where most first agents die, so keep it boring. Put the loop in a small module, read your API key from an environment variable, and run it on a schedule with a cron job or a simple task queue. Give yourself a kill switch: an upper bound on tokens or steps per run, so a misbehaving agent cannot spend your budget overnight. And write a one-line log for every action it takes.

  • Pin your model version and dependencies; do not let today's working agent break on tomorrow's upgrade.
  • Run it against a copy of your real tickets first, then point it at production data with read-only access.
  • Add a human approval gate for any action that sends a message or changes a record.
  • Measure outcome quality, not just task completion: did its categories match yours on a held-out set?
The gap between a demo and a deployed agent is not intelligence; it is discipline. Normalize your inputs, constrain your actions, log everything, and bound your budget.

What You Just Built

What you built is not a toy. It is the same three-phase loop that powers much larger systems: perceive the world, decide with a model, act with tools, remember, repeat. The frameworks you will encounter later — the ones that handle orchestration, retries, and tool schemas for you — are all doing exactly what you did here, just with more polish and more moving parts you did not have to write yourself. That is a wonderful thing, but only after you can see the loop underneath. Now that you have built one from scratch, you will never look at an agent demo the same way again. You will ask the only question that matters: where does it perceive, how does it decide, and what happens when it acts?