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:
- How does it decide what to do?
- How does it actually do things?
- 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.