Agents From First Principles 03: AI Agent Keeps Making the Same Mistake? Add a Critique-and-Revision Loop
An AI agent can fail in a particularly frustrating way: it produces an answer that is almost right, you ask it to improve the answer, and it produces another answer with the same underlying defect.
Sometimes the wording changes. Sometimes it adds more explanation. Sometimes it becomes longer and more confident. But the important mistake survives.
That usually means the system is doing this:
prompt
↓
model
↓
answer
or this:
prompt
↓
model
↓
answer₀
↓
"improve this"
↓
answer₁
The second version looks iterative, but it is still weakly specified. The model has not been forced to identify what is wrong, preserve what is already correct, or demonstrate that the revision actually fixes anything.
A more useful pattern is:
draft
↓
critique
↓
revision plan
↓
revised draft
↓
verify improvement
This is the critique-and-revision loop.
It is one of the smallest agent techniques that can turn extra inference into useful work rather than just extra tokens.
But it is also easy to implement badly.
A critique loop can:
- agree with its own draft,
- generate vague criticism,
- fix one problem while introducing another,
- oscillate between two versions,
- keep revising after quality has plateaued,
- become more verbose without becoming more correct,
- or simply repeat the same failure forever.
So in this post we will build the technique from first principles and make the failure modes measurable.
The Core Problem: Revision Without Diagnosis
Suppose an agent generates this answer:
The bug is caused by the API returning null.
Add a null check before parsing the response.
But the real bug is a race condition.
A naive revision prompt might be:
Improve the answer above.
The model might return:
The most likely cause is that the API occasionally returns null.
Add robust null handling and defensive parsing before accessing fields.
The answer became more polished.
It did not become more correct.
The system needs an intermediate representation of the failure:
Draft defect:
The answer assumes null data without explaining evidence from the stack trace.
The observed failure occurs after two concurrent writes and is timing-dependent.
The revision must consider race conditions before recommending validation changes.
Now the revision is constrained by an explicit diagnosis.
That is the central idea of this article:
Do not ask the model merely to rewrite. Ask it to identify a defect, revise against that defect, and then verify whether the defect disappeared.
From Best-of-N to Critique-and-Revision
In the previous post we used Best-of-N:
prompt
↓
A B C D
↓
score
↓
best
That is horizontal search.
We generate several independent candidates and choose among them.
Critique-and-revision is different:
draft₀
↓
critique₀
↓
draft₁
↓
critique₁
↓
draft₂
That is vertical refinement.
Instead of exploring several independent solutions, we follow one trajectory and try to improve it.
These techniques solve different problems.
Use Best-of-N when:
- generation is highly variable,
- multiple strategies may work,
- independent candidates are valuable,
- a strong selector exists.
Use critique-and-revision when:
- the first answer is usually close,
- defects can be described explicitly,
- revisions can preserve strong parts of the draft,
- improvement can be checked.
You can also combine them later:
Best-of-N
↓
choose strongest draft
↓
critique
↓
revise
But first we need the basic loop.
The Smallest Useful Critique Loop
Start with three functions:
def generate(task: str) -> str:
...
def critique(task: str, draft: str) -> str:
...
def revise(task: str, draft: str, critique_text: str) -> str:
...
Then:
def improve_once(task: str) -> str:
draft = generate(task)
feedback = critique(task, draft)
return revise(task, draft, feedback)
That is already better than:
return llm(f"Improve this:\n{draft}")
because critique and revision are now separate computational roles.
But free-form critique is still fragile.
We should structure it.
Critique Should Be Data, Not Prose
A useful critique contains specific fields.
For example:
from dataclasses import dataclass
@dataclass
class Critique:
problem: str
evidence: str
severity: int
proposed_fix: str
Now the critic must answer four questions:
What is wrong?
What evidence supports that claim?
How serious is it?
What should change?
That is much stronger than:
Please critique the answer.
A structured critique might look like:
{
"problem": "The answer assumes a null-response failure without using the timing evidence.",
"evidence": "The failure appears only when two writes overlap and disappears when execution is serialized.",
"severity": 5,
"proposed_fix": "Reframe the diagnosis around concurrency and recommend inspecting locking or transaction boundaries."
}
This gives the reviser something operational.
A Critique Is Not Automatically Correct
This is one of the most important boundaries in reflection systems.
The critic is usually another model call.
Therefore:
critic output
≠
truth
The critic can hallucinate too.
So we should distinguish:
self-critique
from:
external evidence
A strong loop uses evidence where possible:
draft
↓
critic
↓
claims about defects
↓
compare against tests / logs / rubric / constraints
For coding:
critic says tests fail
is weaker than:
pytest actually fails
For factual questions:
critic says citation is wrong
is weaker than:
retrieved source contradicts claim
For structured outputs:
critic says field is missing
can often be replaced entirely by schema validation.
So the first design question is:
Can this defect be measured directly?
If yes, use the measurement.
Use model critique for failures that are difficult to encode deterministically.
Build a Critique Contract
Let us define a more useful critique object.
from dataclasses import dataclass, field
from typing import List
@dataclass
class Critique:
defects: List[str] = field(default_factory=list)
evidence: List[str] = field(default_factory=list)
preserve: List[str] = field(default_factory=list)
revision_actions: List[str] = field(default_factory=list)
severity: int = 0
The addition of preserve matters.
Without it, revisions often destroy working parts of the draft.
A good critique might say:
defects:
- diagnosis ignores concurrency evidence
- proposed null check does not explain intermittent timing
preserve:
- explanation is concise
- recommendation to add logging is useful
revision_actions:
- inspect race-condition explanation
- retain logging recommendation
- remove unsupported null-response claim
Now revision becomes constrained editing instead of unconditional regeneration.
The First Common Failure: The Critic Agrees With Everything
A weak critic often produces feedback like:
The answer is clear and well structured.
It could perhaps include slightly more detail.
This is nearly useless.
If your agent’s self-critique always says the draft is good, the critic may not have enough pressure to find defects.
One fix is to change the contract.
Instead of:
Review this answer.
ask:
Identify the single highest-impact defect that could cause this answer to fail.
If no material defect exists, return NO_MATERIAL_DEFECT.
Do not praise the answer.
Provide evidence for the defect.
This turns critique from an essay into a decision.
The system can then branch:
critique
├─ NO_MATERIAL_DEFECT → stop
└─ defect found → revise
Require Material Criticism
Define a threshold.
MIN_SEVERITY = 3
Then:
if critique.severity < MIN_SEVERITY:
return draft
This matters because revision itself has risk.
Every rewrite can introduce regressions.
So the question should not be:
Can we think of anything to change?
It should be:
Is there a defect large enough to justify another rewrite?
That is a much better agent policy.
The Second Common Failure: Vague Criticism
Suppose the critic says:
The answer could be clearer and more complete.
What exactly should the reviser do?
Probably guess.
That means the critique has not reduced uncertainty.
A useful critique should point to a concrete transformation.
Weak:
Improve clarity.
Better:
The second paragraph introduces three implementation concepts before defining the API boundary. Move the API boundary definition before the implementation details.
Weak:
Be more accurate.
Better:
The answer states the cache is thread-safe, but the supplied implementation has no lock around writes. Remove that claim or add evidence.
The general rule is:
A critique should be actionable enough that a deterministic editor could understand what must change.
The editor may still be an LLM, but the instruction should not require it to rediscover the problem.
Critique Quality Metrics
We can measure critic behaviour.
For each critique, record:
material_defect_found
severity
number_of_defects
number_of_evidence_items
number_of_revision_actions
Across a benchmark:
critique rate
false-positive critique rate
false-negative critique rate
revision success rate
revision regression rate
These are much more useful than saying:
reflection seemed helpful
The Third Common Failure: Revision Makes the Answer Worse
This is extremely common.
Suppose:
draft₀ score = 0.84
The critic identifies a real flaw.
The revision fixes that flaw but damages two other parts.
Now:
draft₁ score = 0.76
If your system always accepts the newest version, you have built a regression machine.
The fix is simple:
old draft
↓
revision
↓
compare old vs new
↓
keep better
This is a crucial pattern.
def accept_revision(old: str, new: str, score_fn) -> str:
old_score = score_fn(old)
new_score = score_fn(new)
return new if new_score > old_score else old
Now the loop becomes:
draft
↓
critique
↓
revise
↓
score old + new
↓
accept or reject
This is much safer.
Revision Acceptance Should Not Be Based on the Reviser’s Confidence
Avoid:
Did you improve the answer?
That asks the system that performed the change to grade itself.
Prefer:
independent scorer
objective test
schema validator
external judge
For code:
run tests
For ranked outputs:
pairwise scorer
For factual answers:
citation verification
For structured actions:
schema + semantic validation
When objective evidence exists, use it.
A Minimal Revision Gate
from dataclasses import dataclass
@dataclass
class RevisionDecision:
accepted: bool
old_score: float
new_score: float
reason: str
def choose_revision(old, new, evaluator) -> tuple[str, RevisionDecision]:
old_score = evaluator(old)
new_score = evaluator(new)
if new_score > old_score:
return new, RevisionDecision(
accepted=True,
old_score=old_score,
new_score=new_score,
reason="revision_improved_score",
)
return old, RevisionDecision(
accepted=False,
old_score=old_score,
new_score=new_score,
reason="revision_regressed_or_tied",
)
Now regression is observable.
The Fourth Common Failure: Endless Revision Loops
A system that can revise indefinitely eventually will.
Never use:
while not perfect:
revise()
There is no reliable definition of perfect here.
Every iterative agent needs explicit stopping conditions.
For critique-and-revision, useful stopping rules include:
maximum revisions reached
no material defect found
no score improvement
score improvement below threshold
same defect repeated
same draft repeated
cost budget reached
latency budget reached
For example:
MAX_REVISIONS = 3
MIN_IMPROVEMENT = 0.01
Then:
if new_score - old_score < MIN_IMPROVEMENT:
stop = True
This stops a loop from spending another model call to gain 0.001 on an unreliable scorer.
Plateau Detection
A simple plateau rule:
def plateau(scores, patience=2, epsilon=0.01):
if len(scores) < patience + 1:
return False
recent = scores[-(patience + 1):]
improvements = [
recent[i + 1] - recent[i]
for i in range(len(recent) - 1)
]
return all(delta < epsilon for delta in improvements)
Then:
0.70 → 0.82 → 0.84 → 0.841
may justify stopping.
The exact threshold depends on evaluator noise.
Evaluator Noise Matters
Suppose your scorer varies by ±0.02 between repeated evaluations.
Then treating:
0.840
as definitely better than:
0.835
is not justified.
This means acceptance rules should account for noise.
For example:
MIN_IMPROVEMENT = 0.03
or repeated scoring:
score each candidate three times
compare means
or use a deterministic evaluator whenever possible.
The broader rule is:
Do not build a precise revision policy on top of an imprecise evaluator without measuring evaluator variance.
The Fifth Common Failure: The Same Defect Comes Back
Imagine:
revision 0:
- too verbose
revision 1:
- unsupported claim
revision 2:
- too verbose
revision 3:
- unsupported claim
The agent is oscillating.
Track defects.
seen_defects = []
Normalize critique labels:
verbosity
unsupported_claim
missing_evidence
constraint_violation
Then detect repetition.
if critique.problem in seen_defects[-2:]:
terminate("repeated_defect")
Better still, pass previous critique history into the critic:
Previously fixed defects:
- verbosity
- unsupported claim
Do not reintroduce these defects.
This turns history into an active constraint.
Revision Memory Is Not Long-Term Memory
This is important for the series progression.
Inside a revision loop, we may track:
previous drafts
previous critiques
accepted changes
rejected changes
scores
That is working state.
It is not yet the memory architecture we will introduce later.
We do not need a vector database to remember the previous two revisions.
A Python list is enough.
history = []
Use the smallest memory mechanism that matches the problem.
A Useful Revision State Object
from dataclasses import dataclass, field
from typing import List
@dataclass
class RevisionStep:
draft: str
critique: Critique | None
score: float
accepted: bool
@dataclass
class RevisionState:
task: str
current: str
steps: List[RevisionStep] = field(default_factory=list)
termination_reason: str | None = None
This lets us inspect the whole trajectory later.
The Agent Loop
The complete control flow now looks like:
generate initial draft
↓
score baseline
↓
critique
↓
material defect?
├─ no → stop
└─ yes
↓
revise
↓
evaluate old vs new
↓
improved enough?
├─ no → reject / stop
└─ yes
↓
accept
↓
max revisions?
├─ yes → stop
└─ no → critique again
That is a genuine agentic loop because the result of each iteration changes what happens next.
A Complete Standalone Example
Below is a deliberately small implementation.
The LLM functions are injected so the loop is independent of any particular provider.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, List
@dataclass
class Critique:
problem: str
evidence: str
severity: int
proposed_fix: str
preserve: List[str] = field(default_factory=list)
@dataclass
class RevisionRecord:
version: int
draft: str
score: float
critique: Critique | None
accepted: bool
reason: str
@dataclass
class RevisionResult:
final: str
history: List[RevisionRecord]
termination_reason: str
def improve_with_critique(
task: str,
generate_fn: Callable[[str], str],
critique_fn: Callable[[str, str], Critique],
revise_fn: Callable[[str, str, Critique], str],
score_fn: Callable[[str, str], float],
*,
max_revisions: int = 3,
min_severity: int = 3,
min_improvement: float = 0.01,
) -> RevisionResult:
current = generate_fn(task)
current_score = score_fn(task, current)
history = [
RevisionRecord(
version=0,
draft=current,
score=current_score,
critique=None,
accepted=True,
reason="initial",
)
]
seen_problems: list[str] = []
for revision_idx in range(1, max_revisions + 1):
critique = critique_fn(task, current)
if critique.severity < min_severity:
return RevisionResult(
final=current,
history=history,
termination_reason="no_material_defect",
)
normalized_problem = critique.problem.strip().lower()
if normalized_problem in seen_problems[-2:]:
return RevisionResult(
final=current,
history=history,
termination_reason="repeated_defect",
)
seen_problems.append(normalized_problem)
candidate = revise_fn(task, current, critique)
candidate_score = score_fn(task, candidate)
improvement = candidate_score - current_score
accepted = improvement >= min_improvement
history.append(
RevisionRecord(
version=revision_idx,
draft=candidate,
score=candidate_score,
critique=critique,
accepted=accepted,
reason=(
"improved"
if accepted
else "insufficient_improvement"
),
)
)
if not accepted:
return RevisionResult(
final=current,
history=history,
termination_reason="revision_not_better",
)
current = candidate
current_score = candidate_score
return RevisionResult(
final=current,
history=history,
termination_reason="max_revisions",
)
This loop has several useful properties:
- revisions are bounded,
- weak critiques do not trigger rewriting,
- repeated defects stop the loop,
- regressions are rejected,
- every decision is logged,
- termination has an explicit reason.
That is far more robust than:
for _ in range(5):
answer = llm("Improve this: " + answer)
If You Found This Because Your Agent Keeps Making the Same Mistake
Debug the loop in this order.
1. Save every draft
You need:
draft₀
draft₁
draft₂
Do not only inspect the final answer.
If you cannot see the trajectory, you cannot diagnose the loop.
2. Save every critique
Record:
problem
evidence
severity
proposed fix
Then ask:
Did the critique correctly identify the defect?
3. Check whether the revision followed the critique
A good critique with a bad revision is a different failure from a bad critique.
Measure them separately.
4. Compare old and new drafts independently
Do not automatically accept the latest output.
5. Check whether the same defect returns
If it does, you have oscillation or incomplete constraint preservation.
6. Check whether quality actually improves
If scores stay flat:
0.82
0.82
0.81
0.82
reflection is probably adding cost rather than value.
Split Critic and Reviser Roles
You do not necessarily need different models.
But you should separate the calls.
Bad:
Review your answer and rewrite it better.
Better:
Call 1:
Identify the highest-impact defect.
Call 2:
Revise only to fix the identified defect while preserving listed strengths.
Why?
Because it creates observable intermediate state.
You can inspect:
critique
independently from:
revision
That gives you a debugging boundary.
Same Model or Different Models?
There are several configurations.
Same model for everything
model → draft
model → critique
model → revise
Advantages:
- simple,
- cheap operationally,
- no model-routing infrastructure.
Disadvantages:
- correlated blind spots,
- critic may inherit generator assumptions,
- self-preference may be stronger.
Separate critic model
generator → draft
critic → critique
generator → revision
Advantages:
- potentially different failure modes,
- critic can be optimized for evaluation.
Disadvantages:
- more infrastructure,
- more latency,
- more model mismatch.
Learned scorer + LLM critic
LLM critic → explanation of defect
learned scorer → accept/reject revision
This is often a useful split.
The critic proposes why something is wrong.
The scorer decides whether the new output appears better according to a stable learned objective.
Again, the Models From First Principles series gives us several possible scorer families.
Pairwise Evaluation Is Often Better Than Absolute Scores
Instead of asking:
Score answer A from 0 to 1.
Score answer B from 0 to 1.
ask:
Which answer better satisfies the task and constraints?
That directly matches revision acceptance:
old vs new
A pairwise gate:
def accept_pairwise(task, old, new, judge_fn):
winner = judge_fn(task, old, new)
return new if winner == "new" else old
But remember the judge failure modes from Best-of-N:
- position bias,
- order sensitivity,
- verbosity bias,
- style preference,
- self-preference.
So randomize order or evaluate both orders when the decision matters.
Objective Verification Should Override Reflection
Imagine a coding task.
The critic says:
The revised implementation is now correct.
But:
pytest: 3 failed
The tests win.
A useful priority order is:
objective environment signal
↓
structured validator
↓
learned scorer
↓
LLM judge
↓
self-assessment
Not every problem has objective verification.
But whenever it exists, use it.
Critique Can Be Specialized
A general critic may miss specific problems.
Instead of one giant prompt, define critique dimensions.
For example:
correctness
constraint compliance
evidence support
completeness
clarity
safety
Then run only the dimensions that matter.
For code:
correctness
API contract
error handling
complexity
For research:
claim support
source quality
contradictions
missing controls
This can be implemented as a rubric.
A Rubric Critic
RUBRIC = {
"correctness": "Does the answer contain a material factual or logical error?",
"constraints": "Does it violate an explicit user requirement?",
"evidence": "Are important claims unsupported by available evidence?",
}
Each dimension can return:
{
"dimension": "evidence",
"failed": true,
"severity": 4,
"problem": "The answer claims the retry policy is idempotent without evidence.",
"fix": "Either remove the claim or demonstrate idempotency."
}
Now the critic becomes easier to evaluate.
Do Not Fix Everything at Once
Suppose a critic finds eight defects.
Asking the reviser to fix all eight simultaneously may produce a completely new answer.
That creates a large mutation.
Large mutations are difficult to attribute.
A stronger strategy is:
find defects
↓
rank by severity
↓
fix highest-impact defect
↓
re-evaluate
That gives us a more controlled process.
It also produces cleaner experimental evidence.
If the score improves, we know which intervention was responsible.
Revision Size Is a Useful Metric
Measure how much changed.
At the simplest level:
import difflib
def text_change_ratio(old: str, new: str) -> float:
return 1.0 - difflib.SequenceMatcher(None, old, new).ratio()
Then compare:
small targeted revision
against:
complete rewrite
If every critique produces an 80% rewrite, your revision prompt probably does not preserve enough structure.
Preserve Constraints Explicitly
A revision prompt should include:
Task
Original draft
Critique
Things that must be preserved
Constraints
For example:
Do not change:
- the API signature
- the recommendation to log request IDs
- the answer length limit
Fix:
- unsupported null-response diagnosis
This is much safer than:
Rewrite the answer based on this feedback.
Revision Is an Optimization Process
We can describe the loop abstractly.
Let:
xₜ = current answer
cₜ = critique of xₜ
R(xₜ, cₜ) = proposed revision
E(x) = evaluator score
Then:
xₜ₊₁ = R(xₜ, cₜ)
but only if:
E(xₜ₊₁) > E(xₜ) + threshold
Otherwise:
xₜ₊₁ = xₜ
That is a simple hill-climbing process.
This makes the limitations obvious.
Hill climbing can:
- get stuck in local optima,
- depend heavily on the evaluator,
- regress if the evaluator is wrong,
- miss alternatives that require a large conceptual jump.
That is why later we will introduce planning and broader search.
Critique-and-Revision vs Best-of-N
The distinction is important enough to make explicit.
Best-of-N
A
/
prompt ─ B
\
C
Strength:
exploration
Weakness:
cost grows with candidate count
Critique-and-revision
A₀ → A₁ → A₂ → A₃
Strength:
incremental exploitation
Weakness:
can become trapped in one trajectory
You can think of it as:
Best-of-N = search breadth
Critique loop = search depth
This distinction will matter later when we move into explicit search trees.
A Hybrid Strategy
A practical agent can do:
generate 3 candidates
↓
choose strongest
↓
critique strongest
↓
revise once
↓
verify
This often gives a useful compromise.
But benchmark it.
Possible configurations:
1 candidate, 0 revisions
3 candidates, 0 revisions
1 candidate, 2 revisions
3 candidates, 1 revision
Measure quality and cost.
Do not assume the most complicated configuration wins.
Revision Budgeting
Every revision consumes resources.
Track:
model calls
input tokens
output tokens
latency
money
Suppose:
one-shot success: 72%
2 revisions: 79%
4 revisions: 80%
If four revisions triple latency for one percentage point, they may not be worth it.
The useful metric is often:
cost per successful task
rather than:
maximum possible score
When Critique Loops Hurt
Do not add reflection automatically.
It can hurt when:
- the evaluator is weaker than the generator,
- the first answer is already usually correct,
- revisions destroy concise answers,
- latency is strict,
- objective verification is available and cheaper,
- tasks need exploration rather than incremental editing,
- the critic and generator share the same blind spot.
In these cases, another mechanism may be better.
Do You Actually Need a Critique Loop?
Ask:
Is the first answer usually close?
│
├─ no → use broader generation/search
│
└─ yes
↓
Can defects be identified explicitly?
│
├─ no → critique may be unreliable
│
└─ yes
↓
Can revisions be evaluated?
│
├─ no → high regression risk
│
└─ yes
↓
Try bounded critique + revision
That is a better decision rule than:
agents should reflect
A Debugging Checklist
If your critique-and-revision agent is not improving results, measure these separately.
Generator
initial success rate
initial quality score
Critic
material defect detection rate
false-positive critique rate
false-negative critique rate
critique severity distribution
Reviser
critique-following rate
change ratio
revision success rate
Evaluator
agreement with ground truth
test-retest variance
position bias
Whole loop
final success rate
revision count
regression rate
termination reasons
latency
cost per successful task
Now you know which component is failing.
Useful Termination Telemetry
Count why the loop stopped.
For example:
no_material_defect: 48%
revision_not_better: 22%
max_revisions: 12%
repeated_defect: 10%
verified_success: 8%
This tells you much more than average iteration count.
If most runs stop because of revision_not_better, the critic may be identifying defects that the reviser cannot fix.
If most hit max_revisions, the stopping policy may be weak.
If most stop at no_material_defect but benchmark quality is poor, the critic may be complacent.
Test the Technique With Controlled Experiments
Do not compare:
simple agent
against:
critique agent with a different model, different prompt and different sampling settings
Change one mechanism at a time.
A useful experiment matrix:
| System | Candidates | Revisions | Critic | Acceptance gate |
|---|---|---|---|---|
| baseline | 1 | 0 | none | none |
| naive rewrite | 1 | 1 | none | latest wins |
| critique | 1 | 1 | structured | latest wins |
| gated critique | 1 | 1 | structured | evaluator |
| bounded loop | 1 | 3 | structured | evaluator |
Measure:
verified success
quality score
regression rate
model calls
latency
cost
Then you can answer:
Did the critique loop actually improve the system?
The Key Evidence Boundary
A model generating criticism does not mean the system is reflecting in any deep psychological sense.
What we can observe is simpler:
one model call produces a candidate
another model call produces an error hypothesis
another model call proposes a changed candidate
an evaluator decides whether to keep it
That is enough.
We do not need stronger claims.
The interesting engineering question is whether this extra computation improves outcomes.
The Pattern We Have Built So Far
Our series now has three progressively stronger mechanisms.
Step 00
observe → decide → act
Step 01
model output
↓
structured action
↓
validate
↓
execute
Step 02
generate many
↓
score
↓
select
Step 03
generate
↓
critique
↓
revise
↓
verify
Each technique changes the computation around the model.
The underlying foundation model may remain exactly the same.
That is a recurring theme in agent engineering:
A large amount of capability comes from control flow, state, search and verification around the model—not only from changing the model itself.
What Comes Next
Critique-and-revision works well when the agent already has a plausible draft.
But some tasks fail much earlier.
The model starts acting before it has decomposed the task.
It edits files in the wrong order.
It calls tools before gathering required information.
It solves step four before understanding step one.
That is the next common search problem:
Why does my AI agent fail on multi-step tasks?
The next post introduces another separation:
goal
↓
plan
↓
execute
Agents From First Principles 04: AI Agent Fails on Multi-Step Tasks? Separate Planning From Execution.