AI Learning Hub
medium

Agents

An LLM in a while loop with tools. Everything you've heard about "agentic AI" is built on this three-line idea.

The agent is a contractor with a directory of specialists.loopdecide → call →observe → repeatAgent(LLM)search_web()send_email()calc()db.query()calendar()user goalfinal answerEach loop: pick a tool, call it, read the result, decide whether to call another. Stop when done.
An agent doesn't do the work itself — it decides which specialist tool to call, then reacts to the result.

What an agent actually does

An agent is a model that doesn't just answer once — it decides what to do, does it (via tools), looks at what happened, and decides what to do next. That cycle continues until the task is finished or you stop it. The model is steering. Your code provides the hands.

That single rhythm — think, act, observe, repeat — is what people mean when they say "agentic AI." It's also why agents can do open-ended work (debug a build, file an expense, navigate a website) that a single prompt-and-response can't.

That's the whole concept. Below: the loop in code, when not to use an agent, and the failure modes that hit production hard.

When you'd reach for it

When the steps can't be planned in advance and the model genuinely has to react to what it finds:

  • Coding assistants that edit files, run tests, see failures, iterate (Cursor, Claude Code).
  • Research assistants that browse, read, follow links, summarise.
  • Customer-support agents that look up account state, try a fix, check the result.
  • Anything resembling exploration where a fixed pipeline would short-circuit too early.

You wouldn't reach for an agent when the workflow is known. A scripted three-step pipeline is simpler, cheaper, and easier to debug than a wandering loop. Anthropic's Building Effective Agents makes this explicit: most "agent" use cases shouldn't be agents at all.

How it's actually built

The core loop, in pseudo-code:

while not done:
    response = model.generate(messages, tools)
    if response.tool_calls:
        results = run_tools(response.tool_calls)
        messages.append(model_request(response))
        messages.append(tool_results(results))
    else:
        return response.text
        done = True

The model decides which tool to call. You run it. You hand back the result. The model decides what's next. The loop ends when the model produces a text answer (no more tool calls), hits a max-step limit, or the user interrupts.

That's the entire architectural innovation. Every "agent framework" — LangGraph, AutoGen, CrewAI, OpenAI Assistants, Claude Code, Cursor's agent mode — is some refinement of that loop.

Where it bites in real life

Patterns that work

  • Plan-then-execute — model writes a plan in step 1, executes it in subsequent steps.
  • Orchestrator + workers — a "lead" agent dispatches sub-tasks to specialists, synthesises their structured outputs. (Advanced track.)
  • Reflection loops — after a candidate answer, run a "critic" pass, then a revise pass. Cheap quality boost.
  • Bounded recursion — caps on steps, output, and per-tool budget.

Common failure modes

FailureCauseMitigation
Infinite loopsNo progress checkMax steps; detect repeated identical tool calls
Cost explosionLong contexts × many turnsSummarise history; use prompt caching; cap tool budget
Wrong tool chosenVague descriptionsImprove descriptions; add few-shot in system prompt
Brittle to errorsTool throws → agent gives upReturn structured errors with hints; allow N retries
Drifts from goal on long tasksLost in conversation noisePeriodic re-grounding: "Your goal is still X"

Under the hood (optional)

A real-but-minimal agent harness in Python: a for loop bounded by MAX_STEPS, asking the model, running its tool calls, feeding results back. About 25 lines. Skip if you don't code — that one sentence covers the entire agent framework concept.

Show example code (Python, ~25 lines)click to expand
MAX_STEPS = 10
 
def agent(user_goal: str, tools, model="claude-sonnet-4-6"):
    messages = [{"role": "user", "content": user_goal}]
    for step in range(MAX_STEPS):
        resp = client.messages.create(
            model=model, tools=tools, messages=messages, max_tokens=2048
        )
        messages.append({"role": "assistant", "content": resp.content})
 
        tool_uses = [b for b in resp.content if b.type == "tool_use"]
        if not tool_uses:
            return final_text(resp)
 
        results = []
        for use in tool_uses:
            try:
                output = registry[use.name](**use.input)
            except Exception as e:
                output = {"error": str(e)}
            results.append({
                "type": "tool_result",
                "tool_use_id": use.id,
                "content": str(output),
            })
        messages.append({"role": "user", "content": results})
 
    raise RuntimeError(f"Agent didn't finish in {MAX_STEPS} steps")

That harness, plus a thoughtful set of tools, is most of what an "agent framework" gives you. Frameworks add traces, retries, parallel tool execution, persistence, and ergonomics — useful, but optional.

Check your understanding

  1. 1. Mechanically, what is an agent?
  2. 2. Anthropic's 'Building Effective Agents' main point about when NOT to use agents:
  3. 3. Which guardrail should every agent harness have?

Found this useful? Share it with someone learning AI.

Further reading

Related lessons in this track