AI Agents and Orchestration
The pitch is irresistible: give the model a goal and some tools, and let it work out the steps. In a demo it books the meeting, files the ticket and drafts the reply. In week three it issues a refund twice, loops on a failing search for forty minutes, and runs up a bill nobody budgeted for.
Agents are real and useful. They are also the part of GenAI where the engineering discipline matters most, because the system now decides what to do next.
An agent is an LLM in a loop with tools, choosing its own next step. A workflow is the same ingredients on a path you defined. Use the simplest thing that works (a single call, then a workflow, then an agent) and wrap whatever you build in limits, approvals and an audit trail.
Workflow or agent?
| Workflow | Agent | |
|---|---|---|
| Who decides the next step | Your code | The model |
| Predictability | High; paths are known and testable | Lower; paths vary per run |
| Cost and latency | Bounded by design | Variable; depends on how long it wanders |
| Best for | Known sequences: classify, retrieve, answer, verify | Open-ended tasks where the steps can't be predicted |
Most useful systems are workflows with a model step or two inside. Reach for a true agent only when you can't write down the steps in advance, and even then keep its scope narrow.
The anatomy of an agent loop
The loop is simple: look at the goal and history, choose the next action, run it, record the result, repeat until done. What separates a toy from something you can run is the guard rails. Here is the skeleton, with a scripted stand-in for the planning model:
RISKY = {"issue_refund", "send_email"} # tools with side effects need a human
def plan(goal, history):
"""Stand-in planner: a real agent would ask an LLM for the next step."""
script = [("search_kb", {"q": goal}), ("issue_refund", {"order": "A-1001", "amount": 40}), ("finish", {})]
return script[len(history)]
def run_agent(goal, approve, max_steps=6, max_cost=1.00):
history, cost = [], 0.0
for _ in range(max_steps):
tool, args = plan(goal, history)
if tool == "finish":
return {"status": "done", "steps": history}
if tool in RISKY and not approve(tool, args): # human-in-the-loop gate
return {"status": "stopped: approval denied", "steps": history}
cost += 0.05 # track spend per step
if cost > max_cost:
return {"status": "stopped: budget exceeded", "steps": history}
history.append((tool, args)) # audit trail of every action
return {"status": "stopped: step limit", "steps": history}
print(run_agent("refund for late delivery", approve=lambda t, a: False)["status"])
print(run_agent("refund for late delivery", approve=lambda t, a: True)["status"])
Three controls to notice: a step limit and spend budget that stop runaway behaviour, an approval gate in front of any tool with side effects, and a history list that doubles as an audit trail. With approval denied, the refund never happens.
Why agents fail
- Compounding errors. A small mistake in step two shapes everything after it. Ten steps at 95% reliability each succeed only about 60% of the time.
- Loops and wandering. Repeating a failing action, or pursuing a tangent, burns time and money.
- Context bloat. The history grows each step, getting slower, costlier and less focused.
- Over-reach. Broad permissions plus a persuasive piece of untrusted text can lead to actions nobody intended.
What about multiple agents?
Splitting work across several agents (a researcher, a writer, a reviewer) can help when tasks are genuinely separable or one context can't hold everything. It also multiplies cost, latency and failure points. Pass structured work items between agents, not entire transcripts, and ask whether a single agent or a plain workflow would do the job first.
The solution architect's view
With agents, you are no longer designing behaviour. You're designing the boundaries inside which behaviour can vary.
Autonomy is a dial
Decide, per action type, how much the agent may do alone: suggest only, act with approval, or act and report. Start low and raise the dial as evidence accumulates. Put reversible, low-impact actions at the high-autonomy end and money, data deletion and external communication at the other.
Budgets are requirements
Set maximum steps, tokens, tool calls, wall-clock time and cost per task, and decide what happens when they are hit (stop, summarise progress, hand over to a person). A budget that's only a hope isn't a control.
Design for traceability
You need to answer "what did it do, and why" after the fact. Record the goal, each step, tool arguments, results and the prompt and model versions. This is both your debugging tool and your audit evidence.
Contain the blast radius
Run with least-privilege credentials, in a sandbox where it executes code, with network and data access restricted to what the task needs. Make actions idempotent and reversible where possible.
Evaluate trajectories, not just answers
Test whole runs: did it reach the goal, in a sensible number of steps, without forbidden actions? Include adversarial inputs. The evaluation post covers building that suite.
Questions for the design review
- Could a workflow do this instead? What exactly needs the model to decide?
- What may the agent do alone, and what needs approval?
- What are the step, cost and time limits, and what happens at the limit?
- Can we reconstruct every run from the logs?
- What is the worst thing it could do with its current permissions?
Key Takeaways
- An agent chooses its own steps; a workflow follows yours. Prefer the workflow when you can.
- Errors compound across steps, so long autonomous runs are less reliable than they look.
- Bound every run with step, cost and time budgets.
- Put approvals in code in front of side-effecting actions.
- Keep a full audit trail of goals, steps, arguments and results.
- Evaluate whole trajectories and raise autonomy only as evidence builds.

Comments
Loading comments…