What an Agent Actually Is
TL;DR — An agent is a control loop around a language model: the model proposes an action, your code executes it, the result is appended to the context, and the loop runs again. The loop has four moving parts you must design — policy (what to do next), tools (what is possible), memory (what is still in context), and control (when to stop) — plus observability, which nobody designs until their first production failure. The model is usually not the weak link; the stopping condition, the tool error path, and the context budget are. Anything whose steps you can write down in advance is a workflow, not an agent, and a workflow is cheaper, testable, and faster.
1. Why this exists
Start with the failure, not the definition. You have a question that needs two lookups: “Which of the two universities that employ the author of Attention Is All You Need has a larger endowment?” A single chat completion answers from its weights. It will produce a fluent, confident, wrong answer — the model cannot know the endowment figures, and nothing in a single forward pass lets it fetch them.
The naive fix is to hand it tools and let it decide:
while True:
response = model(messages, tools)
if not response.tool_calls:
break
messages += [response] + [execute(tc) for tc in response.tool_calls]
That loop is four lines and it is also how agents delete production data. It has no stopping condition other than “the model stops asking,” no bound on tokens, no protection against two identical calls, and no place to see why it did what it did. Everything this chapter covers is a consequence of taking those four lines seriously.
Two framings are worth separating before anything else, because interviews probe the distinction:
- A workflow is a system “where LLMs and tools are orchestrated through predefined code paths” — you know the sequence, the model fills in the blanks (Anthropic, Building effective agents, 2024-12-19).
- An agent is a system “where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks” — the sequence itself is the model’s output.
Simon Willison’s one-sentence definition, which Anthropic adopted for its own context-engineering post, is the one to memorise: “An LLM agent runs tools in a loop to achieve a goal.” (Willison, 2025-09-18).
That sentence is doing a lot of work. “Runs tools” means the model’s output can change the world, not just produce text. “In a loop” means the output of one step becomes the input of the next, so errors compound and costs accumulate. “To achieve a goal” means there is a stopping condition somewhere, and if you did not design one, the model is improvising it.
The framing that survives contact with production. An agent is not a smarter model; it is a control system with a probabilistic component in the loop. Design it the way you would design any control system: bound the state, bound the time, make the actuators idempotent, and log everything.
2. How it works
2.1 The loop, formally
The ReAct paper gives the cleanest notation for the loop, and it is worth reading symbol by symbol (Yao et al., arXiv:2210.03629, §2).
At step the agent receives an observation from the environment and takes an action according to a policy , where
- — the observation: everything the environment returns after the previous action. In a text-only agent this is literally the tool result as a string. Remove it and the loop is no longer closed through the world; the model is reasoning about what it imagined happened.
- — the action: a call into the environment. In a ReAct-style
agent this is text; in a modern tool-calling agent it is a structured
tool_callsobject (see Tool / Function Calling). Remove the action channel and you have a chatbot. - — the policy: a distribution over actions given the context. With a frozen LLM, is whatever the system prompt, the tool schemas, and the sampling parameters induce. This is the thing you actually tune, and it is the only part of the loop that is a model rather than code.
- — the context: the whole trajectory, not the last observation. It is the agent’s entire working state, and because it is the only channel back into the model, “what is in ” and “what the agent knows” are the same question.
- — the step index. It exists because the loop is iterative; it is also what you count against a budget.
The paper’s motivating sentence is the reason this is hard: learning is difficult when the map is “highly implicit and requires extensive computation.” The agent bet is that a frozen, instruction-tuned LLM is a usable once you write the right context manipulation around it.
2.2 The four components (and the fifth one nobody designs)
| Component | Question it answers | Failure if you skip it | Canonical fix |
|---|---|---|---|
| Policy | what next? | Model answers from priors, never acts | System prompt + tool schema; fine-tune if too weak |
| Tools | what can it do? | Agent can only talk | Function calling, MCP, code execution |
| Memory | what does it still know? | Context overflow; amnesia across turns | Compaction, notes, retrieval-backed stores |
| Control | when does it stop? | Loops, budget blowout, duplicate side effects | Step caps, finish, recursion limits, idempotency |
| Observability | why did it do that? | Non-deterministic failures with no trace | Full trajectory tracing |
The first four are the standard decomposition; the fifth is the one teams add after their first incident. Anthropic’s production write-up puts it plainly: “Adding full production tracing let us diagnose why agents failed” (Anthropic, How we built our multi-agent research system, 2025-06-13). The unit of debugging is the triple plus its token cost and wall-clock time — not the final answer.
The survey framings, for completeness. Wang et al. decompose agents into a profile, memory, planning, and action module (arXiv:2308.11432); Xi et al. into brain, perception, and action (arXiv:2309.07864). Those are taxonomies of what people built. The policy/tools/memory/control decomposition is about what your runtime must contain. Say which one you are using; conflating them is how candidates get caught.
2.3 The loop is a cycle, and every edge is a place to lose
flowchart TD
G["goal + system prompt"] --> CTX["context c_t"]
CTX --> P["policy: model(c_t, tools)"]
P --> D{"action type?"}
D -->|"tool call"| EX["execute tool"]
EX -->|"result string"| APP["append to c_t"]
APP --> CTX
D -->|"finish / no tool call"| OUT["final answer"]
D -->|"budget exceeded"| KILL["abort with partial state"]
CTX -.->|"token count"| BUD{"budget check"}
BUD -->|"over"| KILL
Three things follow from the shape of that diagram, and they source most agent bugs:
- The loop is closed through text. The only way a tool result influences the model is by being appended to . This is why context engineering is a discipline rather than a prompt trick: if a result is too long, truncated, or badly formatted, the agent’s “knowledge” is too.
- The loop is unbounded unless you bound it. Nothing in the architecture says stop. Stopping is a component you build, and it must live in your code, not in the model’s good intentions.
- The loop accumulates. In the naive implementation the context grows monotonically, so any multi-step task is also a context-budget problem, and cost is superlinear because every step re-sends the entire prefix.
2.4 The augmented action space: why thoughts are free
ReAct’s central move is to add language to the action space:
- — the environment’s action space: real calls with real effects.
- — the space of language: free-form text the model can emit.
- — the augmented space. An action (a “thought”) does not affect the external environment, so it produces no observation; it only updates the context, .
This is the whole trick, and it is why reasoning is cheaper than acting. A thought costs output tokens and buys the model a scratchpad it can condition on next step; it cannot delete a file. An action costs a round trip, may be irreversible, and must be validated. Remove and you have a tool-calling robot that cannot plan; remove and you have chain-of-thought, which cannot check anything. ReAct is the chapter-length version of that trade.
2.5 Stopping conditions, ranked by reliability
This is the component most often left to the model, and the model is the worst available choice.
| Mechanism | How it works | Reliability |
|---|---|---|
| Model declares completion | emits finish[...] or a message with no tool calls | lowest — the model can forget |
| Step cap | harness aborts after steps | high — deterministic |
| Token / cost budget | harness checks cumulative usage | high — deterministic |
| Recursion limit | framework guard (e.g. LangGraph’s) | high — deterministic |
| Code-defined termination | graph reaches an END node via a typed edge | strongest — provable from the graph |
The numbers behind the step cap are concrete. ReAct used 7 steps for HotpotQA and 5 for FEVER as its backoff threshold, and reported that “Of all trajectories with correct final answers, those with 7 steps on HotpotQA and 5 on FEVER only take up 0.84% and 1.33% respectively” (ReAct §3.2). In other words: almost every long trajectory is a failing one. A step cap is not just a safety net; it is a cost optimisation that very rarely discards a success.
Idempotency is the control problem people miss. Every durable-execution engine — and every retry loop you write yourself — will re-run a step after a failure or a human approval. LangGraph’s docs are explicit: on resume, “the affected node runs again from the start of its function. Code and side effects before the pause run again,” and the prescription is “Use idempotency keys, upserts, or read-before-write checks” (LangGraph graph API, [as of 2026-10]). An agent with a non-idempotent “send email” tool and an interrupt will eventually send twice.
2.6 The failure taxonomy
Two levels of taxonomy are useful. At the mechanism level, the failures that recur across ReAct, Anthropic’s write-ups, and MCP’s security docs are:
| Failure | Symptom you actually observe |
|---|---|
| Loop / repetition | identical Thought/Action lines repeating |
| Reasoning error | trajectory reads coherently, answer is wrong (47% of ReAct’s HotpotQA failures vs 16% for CoT) |
| Hallucination | facts asserted with no source in (56% of CoT failures, 0% for ReAct on HotpotQA) |
| Non-informative retrieval | empty or irrelevant tool result; model cannot reformulate (23% of ReAct error cases) |
| Tool misuse | 4xx from the API, wrong-typed arguments |
| Context exhaustion | truncation mid-task; the model “forgets” the goal |
| Error compounding | one bad step sends the trajectory somewhere unrecoverable |
| Prompt injection | the agent takes an action no human requested |
| Non-termination | budget exhausted, no answer, no finish |
Rows 1-4 are quoted from ReAct’s human-labelled analysis (Table 2, 200 sampled HotpotQA trajectories). At the system level, the best-evidenced taxonomy is MAST, derived from 1,600+ annotated traces across 7 multi-agent frameworks and 150 traces with expert annotators (), yielding 14 failure modes in three categories — system design, inter-agent misalignment, and task verification (Cemri et al., arXiv:2503.13657). Its headline is the honest one: “Despite enthusiasm for Multi-Agent LLM Systems (MAS), their performance gains on popular benchmarks are often minimal.”
A diagnostic order that saves hours (cheapest discriminator first):
- Trace it. With no trajectory trace you cannot diagnose anything.
- Replay with a gold next step. Inject the correct thought or action at the failure point. Succeeds ⇒ the policy failed there; still fails ⇒ the failure is downstream (tool, context, state).
- Check the tool result. Empty, malformed, or stale? That is the non-informative-retrieval failure.
- Check the token count. Context exhaustion is invisible in a truncated trace.
- Only then tune the prompt.
3. Implementation
A minimal but production-shaped agent loop. This is real code against
openai 3.29.0 [as of 2026-10]; the parts worth copying are the bounds and the
error path, not the prompt.
"""A bounded tool-calling agent loop. Requires: openai==3.29.0 [as of 2026-10]."""
import json
import time
from dataclasses import dataclass, field
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.5" # [as of 2026-10] pin whatever flagship the account has access to
# ---- 1. Tool registry: schema + implementation + policy in one place ---------
@dataclass
class Tool:
schema: dict # what the model sees
fn: callable # what actually runs
timeout_s: float = 10.0
idempotent: bool = True # governs whether retry is legal
TOOLS: dict[str, Tool] = {}
def tool(schema: dict, timeout_s: float = 10.0, idempotent: bool = True):
def deco(fn):
TOOLS[schema["function"]["name"]] = Tool(schema, fn, timeout_s, idempotent)
return fn
return deco
@tool(
schema={
"type": "function",
"function": {
"name": "lookup_endowment",
"description": (
"Return the latest reported endowment in USD for a named "
"university. Use the university's full official name."
),
"parameters": {
"type": "object",
"properties": {
"university": {"type": "string"},
"fiscal_year": {
"anyOf": [{"type": "integer"}, {"type": "null"}]
},
},
"required": ["university", "fiscal_year"],
"additionalProperties": False,
},
"strict": True,
},
}
)
def lookup_endowment(university: str, fiscal_year: int | None) -> str:
rows = db_query( # your real data access; parameterised, not string-formatted
"SELECT year, usd FROM endowments WHERE university = %s ORDER BY year DESC",
(university,),
)
if not rows:
return f"no data for university={university!r}"
if fiscal_year is not None:
rows = [r for r in rows if r[0] == fiscal_year]
return json.dumps(rows[:3])
# ---- 2. Execution: errors are observations, never exceptions ----------------
@dataclass
class Budget:
max_steps: int = 12 # ReAct used 5-7; 12 is a research-agent range
max_input_tokens: int = 120_000 # checked by the harness, not by the model
max_seconds: float = 120.0
started: float = field(default_factory=time.monotonic)
steps: int = 0
input_tokens: int = 0
def execute(call, budget: Budget) -> str:
"""Run one tool call and return a string for the context. Never raises."""
name = call.function.name
entry = TOOLS.get(name)
if entry is None:
return f"error: unknown tool {name!r}; available: {sorted(TOOLS)}"
try:
args = json.loads(call.function.arguments) # SDK note: may not be valid JSON
except json.JSONDecodeError as e:
return f"error: arguments were not valid JSON ({e}); re-emit the call"
# Business-logic validation, separate from the schema check.
missing = [k for k in ("university",) if k not in args]
if missing:
return f"error: missing required argument(s) {missing}"
if not entry.idempotent:
# The server deduplicates on this key. It is derived from the call id,
# so the SAME call retried is a no-op, while a NEW call is not.
args["_idempotency_key"] = call.id
try:
return str(entry.fn(**{k: v for k, v in args.items()
if not k.startswith("_")}))
except Exception as e: # noqa: BLE001 - deliberate
return f"error: {type(e).__name__}: {e}"
# ---- 3. The loop -------------------------------------------------------------
def run_agent(goal: str, budget: Budget | None = None) -> dict:
budget = budget or Budget()
messages = [
{"role": "system", "content": (
"You are a research assistant. Call tools to gather facts; never "
"guess a number. When you have the answer, reply in plain text "
"with no tool calls. Cite the tool calls you relied on."
)},
{"role": "user", "content": goal},
]
seen: set[tuple[str, str]] = set() # loop detector
while budget.steps < budget.max_steps:
if time.monotonic() - budget.started > budget.max_seconds:
return {"status": "timeout", "messages": messages}
if budget.input_tokens > budget.max_input_tokens:
return {"status": "token_budget_exhausted", "messages": messages}
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=[t.schema for t in TOOLS.values()],
tool_choice="auto",
parallel_tool_calls=False, # serialise: side-effect ordering matters
temperature=0.0,
)
budget.input_tokens += response.usage.prompt_tokens
msg = response.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return {"status": "ok", "answer": msg.content, "messages": messages}
for call in msg.tool_calls:
key = (call.function.name, call.function.arguments)
if key in seen:
return {"status": "loop_detected", "call": key, "messages": messages}
seen.add(key)
result = execute(call, budget)
messages.append({
"role": "tool",
"tool_call_id": call.id, # MUST match the assistant's call id
"content": result,
})
budget.steps += 1
return {"status": "step_cap", "messages": messages}
The knobs that matter, each with a realistic range:
| Knob | Range | What moving it does |
|---|---|---|
max_steps | 5-25 (ReAct used 5-7) | More steps = more chances to recover, and a proportional cost multiplier on a failing run |
max_input_tokens | 30k-200k | The hard stop for context growth; too low truncates mid-task |
temperature | 0.0-0.3 | 0 makes replay/debugging tractable; higher buys diversity at the cost of reproducibility |
parallel_tool_calls | on/off | Off when tools have order-dependent side effects; on for independent reads only |
timeout_s per tool | 5-30 s | Must be shorter than the harness timeout, or the whole loop dies on one hung tool |
idempotent per tool | bool | The flag that decides whether a retry is legal at all |
Two structural notes on the code. First, errors return strings, not
exceptions — a traceback the model never sees is a dead end, while
"query failed with error: no such column: TrackId" is a retryable
observation. Second, the loop detector keys on (name, arguments) because
ReAct’s single most common failure is repeating an identical action; catching
it in the harness costs nothing and does not depend on the model noticing.
4. Numbers and trade-offs
4.1 Token arithmetic of an agent loop
Let
- — the tokens in the stable prefix: system prompt + tool schemas + the user’s task. Re-sent on every step.
- — the tokens appended per step: one assistant message plus one tool result.
- — the number of steps.
Because each step re-sends the entire context, total input tokens are
Read the two terms separately, because they have different fixes. The term is the prefix paid times — it is linear in only because is constant. The term is quadratic: every new token is re-read by every subsequent step. This is the arithmetic reason a long agent run costs far more than its final context size suggests.
With , , (example):
The naive estimate undercounts by 2.1×. Double the steps to 20 and becomes about 135,000 — 3.2× more, for 2× the work.
Two levers change the shape:
- Compaction resets by replacing the accumulated tail with a summary, at the cost of a summarisation call. It attacks the quadratic term.
- Prompt caching attacks the term: a stable prefix is billed at the cache-read rate. Anthropic’s documented multipliers are “Cache writes cost 1.25× base input price. Cache reads cost 0.1× base input price,” with a 1,024-token minimum for Sonnet [as of 2026-10] (prompt-caching cookbook). So a long, frozen system prompt is cheaper per turn than a short one that changes — anything that mutates the prefix (a timestamp, re-ordered tools) invalidates everything after it.
Note the tension: caching wants the prefix stable, compaction rewrites it. Which one wins depends on whether you are step-bound (compact) or turn-bound with a long stable preamble (cache).
4.2 Success-rate compounding
If a task requires independent steps, each succeeding with probability , the whole task succeeds with probability
- — per-step success probability, including the step where the model correctly decides it is done.
- — number of steps on the critical path.
- The product assumes independence, which is an optimistic assumption: model errors are correlated, so real success rates are usually worse than , not better.
| 1 | 0.99 | 0.95 | 0.90 |
| 5 | 0.951 | 0.774 | 0.590 |
| 10 | 0.904 | 0.599 | 0.349 |
| 20 | 0.818 | 0.358 | 0.122 |
| 50 | 0.605 | 0.077 | 0.005 |
At a very good 95% per-step reliability, a 20-step task succeeds 36% of the time. The engineering consequence is not “make the model better” — it is “shorten .” Decompose the task so the agent does 5 steps, not 20, and you recover most of the loss. This is also the entire argument for giving a single agent better tools instead of splitting the work across more agents: every handoff is another multiplicative factor.
Redundancy buys reliability back, but only if errors are independent. Running a task twice and accepting if either succeeds gives . At that is 0.84 (example) — but if both attempts hit the same systematic failure (the same missing tool, the same misleading document), the gain is zero. That is why verification with a different signal (a compiler, a test, a second model with a different prompt) works and “ask it again” usually does not.
4.3 Cost, latency, and which axis you are buying
Anthropic’s production numbers frame the budget honestly: “agents typically use about 4× more tokens than chat interactions, and multi-agent systems use about 15× more tokens than chats” (vendor-internal, not a controlled benchmark). If, say, input and output are billed at rates and per token, expected task cost is
- — the per-token prices; on most providers , so generated reasoning tokens dominate earlier than people expect.
- — average generated tokens per step (thinking + action + any scratchpad).
- — from §4.1.
Latency decomposes differently: model tokens are sequential, tool calls can be parallel. Serial steps each costing gives wall-clock ; fanning the tools out gives per step, which is why “the slowest subagent becomes the critical path” is the standard multi-agent bottleneck.
| Decision | Buys you | Costs you |
|---|---|---|
| More steps allowed | Recovery from a bad step | Quadratic input tokens, more compounded error |
| More tools exposed | Capability | Selection accuracy — “bloated tool sets” is Anthropic’s named failure mode |
| Stable long prefix | Cache-read pricing (0.1×) | Rigidity; any edit invalidates the cache |
| Parallel tool calls | Wall-clock latency | Ordering hazards, harder tracing |
| Compaction | Bounds context growth | A summarisation call, lost detail, cache invalidation |
4.4 Workflow vs agent, in numbers
For a task with a known 4-step sequence, a workflow executes exactly 4 model calls with a fixed context per call and is testable step by step. An agent solving the same task will average more calls (it must decide the sequence) and has a variance around that average. The agent’s advantage is only on tasks whose sequence you cannot write down in advance. Where you can write it down, the workflow dominates on cost, latency, reproducibility, and debuggability — four axes out of four.
5. Pitfalls
Loops that never terminate.
Symptom: identical (tool_name, arguments) pairs repeating, or the same
thought re-generated with cosmetic differences. This is ReAct’s most frequent
single failure. The model is usually either missing information it cannot
obtain or stuck in a local pattern from greedy decoding. Fix at the harness
level — repeat detection plus a step cap — because you cannot rely on the model
to notice.
Context growth until truncation. Symptom: cost per step climbing, latency climbing, then a sudden quality cliff where the agent “forgets” the goal or repeats an earlier step. A truncated context looks like a reasoning failure and is not one. Check the token counter before touching the prompt.
Tool results treated as trusted instructions. Symptom: the agent takes a consequential action nobody asked for — a request to an external URL, an email sent, a record deleted — immediately after reading a web page, an issue title, or a file. This is indirect prompt injection (Greshake et al., arXiv:2302.12173): the model has no notion of provenance, so “data” and “instructions” are the same token stream. Treat every tool result as attacker-controllable input; see Guardrails.
Silent tool failure.
Symptom: the agent proceeds confidently after a tool returned an empty list,
null, or a generic {}. The model reads “no results” as “no such thing
exists” and answers from priors. Distinguish “no data” from “lookup failed” in
the string you return, and make the empty case explicit.
Non-idempotent side effects on retry or resume. Symptom: duplicate rows, duplicate emails, double-charged customers — and only after a timeout, an interrupt, or a deployment, never in the happy-path test. Every durable execution engine re-runs the interrupted step from the top. Use idempotency keys, upserts, or read-before-write.
Error compounding. Symptom: a single wrong intermediate assumption propagates into every later step, and the final answer is confidently wrong but internally consistent. “One step failing can cause agents to explore entirely different trajectories.” Mitigations that work: verify intermediate state with a non-LLM check (a test, a schema, a row count) and re-anchor the original goal in the context.
Budget blowout with no answer.
Symptom: the run ends on a spent budget rather than a finish, and the
partial state is useless because nothing was checkpointed. If you allow the
budget to run out, make sure you also have a partial-result path.
6. Using it in a project
When NOT to use an agent. This is the more valuable half of the section.
- The sequence is known. If you can write the steps, write them. Prompt chaining (call A, validate, call B, validate, call C) is cheaper, faster, testable, and has no non-termination risk. Most “agent” features in production are workflows wearing an agent costume.
- One well-specified call suffices. Classification, extraction, summarising, translation, single-hop QA with the context already in the prompt: these are one call plus a schema, not an agent. Adding a loop adds failure modes and cost and buys nothing.
- The task is a single retrieval plus a generation. That is RAG, and a retrieval-then-answer pipeline beats an agent on both latency and reliability because the retrieval step cannot go off-script.
- Latency is the product. A voice assistant or an autocomplete does not have 15 seconds for three tool round trips. Agents are for tasks where the user is not watching the clock (a report, a background job, a code change).
- The risk of a wrong action is unbounded and the value is low. If the agent can drop a table and the task is “make my dashboard nicer,” you have the worst ratio of capability to consequence. Poka-yoke the interface instead: fewer, narrower tools and a human gate before anything irreversible.
- You have not measured the single-call baseline. Half of “we need an agent” evaporates once someone measures what one careful prompt achieves.
When an agent is the right design. The task needs a variable number of steps that depends on what the earlier steps return; the steps cross a real boundary (the web, a database, a filesystem, a browser); the value of completion justifies 4-15× the tokens of a chat; and you can bound the damage of a wrong action. Anthropic’s effort-scaling guidance is a good starting policy: “Simple fact-finding requires just 1 agent with 3-10 tool calls, direct comparisons might need 2-4 subagents with 10-15 calls each, and complex research might use more than 10 subagents.”
Decide the four components explicitly before writing code.
| Decision | Default for a first agent |
|---|---|
| Policy | Prompted, temperature=0, one flagship model |
| Tools | <= 8 narrow tools; read and write split; no shell |
| Memory | Full transcript, plus a token budget and compaction at 70% |
| Control | Step cap 12, token budget, wall-clock, loop detector, idempotency keys |
| Observability | Log every (o, a, o') triple with tokens and latency from day one |
7. How you would prove it worked
Agents have several metrics and they trade off; pick the primary one before you start, or you will end up reporting whichever moved.
Metrics.
- End-state task success rate — graded on the final state of the world or the final answer, not on whether the trajectory looked right. Anthropic’s point is exactly this: “Even with identical starting points, agents might take completely different valid paths,” so grade the outcome.
- Steps and tool calls to success — the efficiency axis, and a proxy for cost.
- Total tokens and cost per task — from the harness’s own accounting, not the model’s.
- p50 / p95 wall-clock — the p95 is what users complain about.
- Safety counters — duplicate side effects, injection-triggered actions, budget kills.
Baselines, in ascending order of sophistication. (1) A single well-crafted prompt. (2) Chain-of-thought. (3) A fixed workflow that hard-codes the sequence the agent is supposed to discover. (4) The agent. If the agent does not beat (3) on the tasks where the sequence is genuinely variable, you have bought nothing.
Ablations that isolate the loop rather than the model. Remove the thoughts (the Act arm). Remove the observations (the CoT arm). Remove the step cap and watch cost — not quality — move. Swap the model and hold the harness fixed: if quality does not move, your harness is the bottleneck.
Reporting template (example - replace with your own numbers). “On 200 tasks from our internal ops queue, the agent reached a correct end state 71% of the time, versus 48% for a single prompt and 63% for the fixed workflow, at 6.4× the tokens and 3.1 s p50 / 19 s p95 wall-clock (example). The step cap fired on 9% of runs; of those, 4% were partial successes we salvaged. Removing the loop detector increased duplicate tool executions from 0 to 0.8 per run (example).”
8. Interview drill
“What is an agent, precisely?” Strong answer: a control loop around a model: the model proposes an action from a policy , the harness executes it, the result is appended to the context, and the loop repeats until a designed stopping condition. The model is one component; the tools, the context management, and the stopping condition are the others. Common wrong answer: “a model that can use tools.” Tool use without a loop is a single function call; the loop is what makes the failure modes compounding and the cost superlinear.
“Agent or workflow?” Strong answer: if the sequence of steps is knowable in advance, it is a workflow and you should write the code path; use an agent only when the sequence itself is the model’s output. Anthropic draws exactly this line. Common wrong answer: “agents are the general case, so build the agent and simplify later.” You will ship the agent, and the workflow was a week’s work.
“Why is the context window the binding constraint?” Strong answer: the only channel back into the model is tokens appended to , so context is simultaneously memory, working state, and instruction surface. Cost is superlinear in steps because every step re-sends the prefix, and quality degrades as it fills — context rot and lost-in-the-middle are measurable, not folklore. Follow-up: “so what do you evict first?” — raw tool results no longer referenced, then duplicated outputs, then completed subgoal traces; never the user’s original constraints.
“How do you guarantee an agent terminates?”
Strong answer: you do not rely on the model. A deterministic step cap, a token
budget checked by the harness, a wall-clock timeout, and a loop detector on
repeated (tool, arguments) pairs. ReAct’s own data says long trajectories are
almost never the productive ones — only 0.84% of correct HotpotQA trajectories
needed more than 7 steps.
Common wrong answer: “it stops when it emits finish.” That is a hope, not a
guarantee; unknown tools and repeated calls both bypass it.
“Where do agents actually fail in production?” Strong answer: rank by frequency — repetition loops, reasoning errors that read coherently, non-informative tool results the model cannot recover from, context exhaustion, and error compounding. Then the security category: indirect prompt injection through tool results. Common wrong answer: “hallucination.” Hallucination is the reasoning-only failure mode (56% of CoT’s HotpotQA failures); a well-tooled agent almost never hallucinates, and hallucinating agents are the ones with no tools at all.
“What makes a tool safe to put in an agent?”
Strong answer: it is narrow, its arguments are poka-yoked so the obvious
mistake is impossible (absolute paths, enums instead of free strings), its
credentials are scoped to the tool rather than the agent, it is either read-only
or idempotent, and its result is treated as untrusted data.
Follow-up: “you have a send_email tool and a retry loop — what breaks?” —
duplicate emails, because the retry re-runs a non-idempotent side effect. Fix
with an idempotency key the server deduplicates on.
Next: ReAct — the pattern that made the loop explicit: interleaving reasoning traces with actions, and why that specific structure beats chain-of-thought on tool tasks and loses to it elsewhere.