AI & Machine Learning

Three Agentic Patterns Every Python Developer Should Have in Their Back Pocket

lex@lexgaines.com · · 5 min read
"AI agent" has become one of those phrases that means everything and nothing. Marketing decks throw it at every LLM that calls a function. Vendor pages promise autonomous workforces. Meanwhile, the developers actually shipping this stuff have quietly converged on a handful of design patterns that work — and a much larger pile of patterns that don't. This is a working developer's tour of the three agentic patterns I reach for most often: **Planning**, **Tool Use**, and **Multi-Agent**. Each one solves a specific problem, each one has a sweet spot, and each one will burn you if you misapply it. I'll keep the code light and Python-flavored, because that's where most of this work actually lives.

Before we get into patterns, a quick framing note. An "agent," in the way most practitioners use the term in 2026, is just an LLM in a loop with some way to act on the world. That's it. Strip away the buzz and you have three real questions to answer when you build one:

  1. How does it decide what to do?
  2. How does it actually do things?
  3. When does one agent stop being enough?

The three patterns below map directly to those questions.


1. Planning

The problem it solves: The LLM is being asked to do something multi-step, and going one token at a time leads it into the weeds.

The Planning pattern says: before you act, write down the plan. Either the model produces the plan itself (sometimes called ReAct-style or plan-and-execute), or a dedicated planning step happens first and an executor takes it from there. The plan becomes a contract — you can inspect it, log it, and intervene before any side effects happen.

The reason this works isn't magical. LLMs are better at reasoning when they have to commit reasoning to text. A plan forces the model to externalize its thinking, which makes it dramatically easier to debug when things go sideways at 2 a.m.

def plan(goal: str) -> list[str]:
    """Ask the model to break the goal into ordered, atomic steps."""
    prompt = f"""Break this goal into a numbered list of atomic steps.
Each step should be doable on its own.

Goal: {goal}
"""
    response = llm.complete(prompt)
    return [line.strip() for line in response.splitlines() if line.strip()]

def execute(steps: list[str]) -> list[dict]:
    results = []
    for step in steps:
        result = llm.complete(f"Execute this step: {step}")
        results.append({"step": step, "result": result})
    return results

Where it shines: Anything with more than three discrete steps — research workflows, code refactors, multi-stage data pipelines, anything where "did the agent do the right thing" needs to be auditable.

Where it bites: Short tasks. If the work is one or two function calls, planning is overhead. You'll burn tokens producing a plan to do the thing you could've just done.


2. Tool Use

The problem it solves: The model needs information or capabilities it doesn't have. APIs, databases, the filesystem, code execution — whatever lives outside the context window.

This is the pattern that actually moved agents from demo to production. Once you give a model a structured way to call functions and read the results, you stop fighting hallucinations on factual lookups and you stop pretending the LLM knows what time it is. Function calling is now table-stakes in every major SDK, and frameworks like LangChain, LlamaIndex, and the Anthropic SDK all expose roughly the same shape: declare your tools, hand them to the model, run a loop, return results.

def call_with_tools(user_input: str, tools: list[dict]) -> str:
    messages = [{"role": "user", "content": user_input}]
    while True:
        response = llm.complete(messages=messages, tools=tools)
        if response.stop_reason != "tool_use":
            return response.text
        for tool_call in response.tool_calls:
            result = TOOL_REGISTRY[tool_call.name](**tool_call.input)
            messages.append({"role": "tool", "content": result, "tool_use_id": tool_call.id})

The pattern looks deceptively simple. The hard part is everything around it: schema design, error handling when a tool fails, deciding what to do when the model calls a tool with bad arguments, and rate-limiting the loop so a confused model doesn't burn your API budget in a runaway.

Where it shines: Anywhere the model needs fresh, specific, or side-effecting information. Querying your database, hitting a SaaS API, running a script, reading a file, fetching today's data.

Where it bites: Tool sprawl. The more tools you give a model, the worse it gets at picking the right one. I'd rather have eight well-named tools than thirty overlapping ones, and I'd rather scope tools to a task than dump a kitchen sink into every prompt.


3. Multi-Agent

The problem it solves: A single agent is trying to be a generalist when the work actually has clear specialist roles, or the work parallelizes naturally and one agent in a serial loop is a bottleneck.

Multi-agent is where things get interesting and dangerous in equal measure. The idea is straightforward: spin up multiple agents, each with its own role, prompt, and toolset, and have them collaborate. A "researcher" agent gathers information, a "writer" agent drafts, a "critic" agent reviews. Or you go orchestrator-and-workers, where a lead agent dispatches subtasks to specialized workers and merges the results.

def orchestrate(goal: str) -> str:
    subtasks = orchestrator.plan(goal)
    results = []
    for task in subtasks:
        worker = WORKERS[task.role]   # e.g. "researcher", "coder", "reviewer"
        results.append(worker.run(task))
    return orchestrator.synthesize(results)

When it works, it works really well. Specialization lets each agent have a tighter prompt, fewer tools, and a narrower failure surface. Parallel workers can chew through embarrassingly-parallel work much faster than a single sequential loop.

When it doesn't work, you've built a distributed system with non-deterministic nodes that occasionally lie to each other. Coordination overhead is real. Token costs multiply fast. And debugging a multi-agent run is genuinely harder than debugging a single agent — you're tracing conversations between models, not just one model's reasoning.

Where it shines: Research-heavy tasks, content workflows with distinct roles (draft → critique → revise), and parallelizable work like analyzing many documents at once.

Where it bites: Anything you could've done with one well-prompted agent and a handful of tools. If you don't have a clear reason for the second agent, you don't need it.

Conclusion

Most agentic systems I've seen in the wild are some combination of these three. A planner on the outside, tool use on the inside, and the occasional second agent when the work genuinely calls for it. The discipline isn't picking the most sophisticated pattern — it's picking the simplest one that solves the problem in front of you.

If you're starting from zero in Python, build a tool-using agent first. Add planning when the tasks get longer than a couple of steps. Reach for multi-agent only when you can articulate, out loud, why one agent isn't enough. That order will save you a lot of complexity and a lot of tokens.

The patterns aren't the point. Shipping is.


Lex Gaines is an AI Infrastructure Engineer and founder of LexG.ai and QUASR.ai, building tools and infrastructure that connect AI models to real-world data and systems. He writes from Denver, Colorado.

Python agentic AI multi-agent orchestration tool design MCP