teachyou.ai academy
← All posts
AI AgentsLLM engineeringagent design patternstool useprompt engineering

Agent Reflection: Self-Critique and Retry Loops

Pramod Dutta · Jul 8, 2026 · 12 min read

Agent reflection is the pattern where an AI agent evaluates its own output before returning it to the user, then retries or revises the work if the evaluation finds a problem. Instead of trusting a single pass through the model, the agent runs an act-then-critique loop: produce an answer, judge that answer against explicit criteria, and either accept it or try again with the critique folded back into the next attempt. This article covers why single-pass agents fail in predictable ways, how to build a reflection loop from scratch, and how to keep that loop from becoming an expensive infinite retry machine.

Why single-pass agents fail

A plain agent call looks like this: the model reads a prompt, maybe calls a few tools, and returns an answer. There is no step where the model checks its own work against the original goal. That gap causes a specific, recurring set of failures.

The most common one is silent partial completion. Ask an agent to "refactor this function and update all call sites," and it will often refactor the function, update two of five call sites, and report success anyway. Nothing in a single forward pass forces the model to go back and verify the claim it just made. A second failure mode is confident hallucination in structured output: the model returns JSON that looks well-formed but references a field name that doesn't exist in the schema, or a code block that imports a package that was never installed. A third is scope drift on multi-step tasks, where step four of a plan quietly contradicts a constraint stated in step one, because nothing re-reads the constraint once the model has moved on.

None of these are reasoning failures in the sense of the model "not knowing" the right answer. In most cases, if you handed the same output back to the same model and asked "does this actually satisfy the request," it would catch the problem. That is the entire premise of agent reflection: verification is a different cognitive task than generation, and running it as a separate pass catches errors that generation alone misses.

The core reflection loop

The pattern has four stages that repeat until the output passes or a stopping condition is hit:

  1. Act: the agent produces an output (a code change, an answer, a plan, a tool call sequence).
  2. Observe: the agent (or a harness around it) gathers evidence about what actually happened, such as test results, a diff, or a rendered page.
  3. Critique: a model call, prompted specifically to judge, compares the output plus evidence against the original goal and lists concrete problems.
  4. Retry or accept: if the critique finds no blocking issues, the loop ends. Otherwise the critique is fed back into a new "act" step, and the loop repeats.

This is close to the Reflexion pattern described in agent research, where an agent maintains a running memory of its own past mistakes and uses that memory as extra context on the next attempt, rather than starting over from scratch each time. The important design decision is not "add a loop," it's making the critique step genuinely independent from the generation step, so it isn't just rubber-stamping its own prior work.

Building a minimal reflection loop

Here is a self-contained reflection loop using the Anthropic Python SDK. It separates the generator and critic into two distinct calls, each with its own system prompt, and it uses structured output from the critic to decide whether to keep going.

import json
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-sonnet-4-5"  # swap for whatever current model id you're standardized on

GENERATOR_SYSTEM = """You are a careful software engineer. You write code that
directly satisfies the task. If you are given feedback from a previous attempt,
treat it as a list of required fixes, not suggestions."""

CRITIC_SYSTEM = """You are a strict reviewer. You do not write code. You are given
a task and a candidate solution. Check the candidate against the task line by
line. Respond with JSON only, in this exact shape:
{"pass": true or false, "issues": ["specific issue 1", "specific issue 2"]}
If there are no issues, return {"pass": true, "issues": []}."""

def generate(task, feedback=None):
    prompt = task
    if feedback:
        prompt += "\n\nFix these issues from the last attempt:\n" + "\n".join(f"- {f}" for f in feedback)
    response = client.messages.create(
        model=MODEL,
        max_tokens=2000,
        system=GENERATOR_SYSTEM,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.content[0].text

def critique(task, candidate):
    prompt = f"Task:\n{task}\n\nCandidate solution:\n{candidate}"
    response = client.messages.create(
        model=MODEL,
        max_tokens=500,
        system=CRITIC_SYSTEM,
        messages=[{"role": "user", "content": prompt}],
    )
    return json.loads(response.content[0].text)

def reflect_and_retry(task, max_attempts=3):
    feedback = None
    for attempt in range(1, max_attempts + 1):
        candidate = generate(task, feedback)
        result = critique(task, candidate)
        if result["pass"]:
            return candidate, attempt
        feedback = result["issues"]
    # exhausted attempts, return the last candidate with a warning
    return candidate, attempt

A few details matter here. The critic gets its own system prompt and is explicitly told not to write code, only to evaluate. That separation is what keeps the critic from just agreeing with whatever the generator produced. The critic also returns structured JSON rather than free text, which makes the "pass or fail" decision something your code can branch on instead of something you have to re-parse with another model call.

Reflection with tool use and real evidence

The example above critiques text against text, which catches logical and completeness problems but not runtime problems. A stronger version of reflection pulls in real evidence: run the code, run the tests, take a screenshot, check an API response. The critique step then judges the output against that evidence instead of just re-reading the same text it already saw.

import subprocess

def run_tests():
    result = subprocess.run(
        ["pytest", "-q", "--tb=short"],
        capture_output=True,
        text=True,
        timeout=60,
    )
    return result.returncode == 0, result.stdout + result.stderr

def reflect_with_evidence(task, max_attempts=3):
    feedback = None
    for attempt in range(1, max_attempts + 1):
        candidate = generate(task, feedback)
        write_candidate_to_disk(candidate)  # your own function
        tests_passed, test_output = run_tests()
        if tests_passed:
            return candidate, attempt
        # feed real failure output back in, not a model's guess about failure
        feedback = [f"Tests failed with output:\n{test_output[-1500:]}"]
    return candidate, attempt

This is a meaningfully different loop from the pure text-critique version, because the "critic" here is not a model call at all, it's the test suite. Whenever you have a ground-truth check available (tests, a linter, a schema validator, an HTTP status code), prefer it over an LLM critique. Model-based critique is for the cases where there is no automatic check: is this explanation clear, does this plan address the stated constraints, is this summary faithful to the source document. Reserve the LLM-as-critic pattern for exactly those judgment calls, and use deterministic checks everywhere a deterministic check exists.

Reflection inside an agent's tool loop

If you're building an agent that calls tools in a loop (the standard messages.create -> tool_use -> tool_result -> messages.create cycle), reflection can be inserted as one more tool the agent can call on itself, rather than an external wrapper. Define a self_check tool that the agent is instructed to call before it declares the task done.

tools = [
    {
        "name": "self_check",
        "description": "Call this before finishing. Re-reads the original task and your work so far, and returns a list of gaps, or an empty list if none.",
        "input_schema": {
            "type": "object",
            "properties": {
                "summary_of_work": {"type": "string"},
            },
            "required": ["summary_of_work"],
        },
    },
    # ... your other tools: read_file, edit_file, run_command, etc.
]

When the agent calls self_check, your harness code (not the model) runs a fresh, separate messages.create call with a critic system prompt, feeding it the original task and the summary_of_work argument. The result comes back as the tool_result, and the agent's own instructions tell it to treat a non-empty gap list as required follow-up work before it can stop. This keeps reflection inside the same conversation and tool-calling loop the agent already uses, instead of bolting on a separate orchestration layer.

The instruction that makes this reliable is blunt and explicit in the system prompt: "Do not report the task complete until self_check returns an empty issue list, or until you have called it twice and addressed every issue it raised." Vague instructions like "double check your work" get ignored under time pressure the same way a rushed human skips a step; a hard gate tied to a tool call does not.

Stopping conditions

An unbounded reflection loop is a cost and latency problem waiting to happen. Every retry is a full extra generation, and if the critic and generator get into a disagreement loop, cost climbs with no guarantee of eventual convergence. Set explicit stops:

  • Max attempts. Three is a reasonable default for most tasks. If a task hasn't converged in three critique-retry cycles, more retries rarely help. Escalate to a human or a different strategy instead.
  • Max wall-clock time or token budget. Especially important if a single "attempt" involves running a test suite or hitting external APIs, since attempt cost isn't uniform.
  • Diminishing-returns check. Track the issue count returned by the critic across attempts. If attempt 2 has more issues than attempt 1, the generator is regressing, not converging, and you should stop and surface the failure rather than retry again.
  • Explicit "good enough" threshold for subjective critiques. If the critic is scoring on a rubric rather than pass/fail, decide up front what score is acceptable so the loop doesn't chase a perfect 10/10 that never arrives.

On timeout or max-attempts exhaustion, return the best attempt along with the critic's last issue list, rather than silently returning a failing result as if it succeeded. The whole point of reflection is not to hide failures better, it's to catch them; a loop that swallows its own failure signal at the end defeats the purpose.

Common pitfalls

Same model, same blind spots. If the generator and critic are the same model with the same system prompt style, they can share the same misconceptions, so the critic approves work that has a systemic flaw neither call ever considered. Using a different model for the critic, or at minimum a critic prompt that forces line-by-line comparison against explicit requirements rather than a vague "does this look right," reduces this.

Critique without evidence. A model critiquing its own text output, with no access to actual execution results, is prone to agreeing that code "looks correct" without ever running it. Wire in real evidence (test runs, linter output, rendered screenshots) wherever it's available, and only fall back to pure text critique for genuinely subjective judgments.

Feedback that isn't actionable. A critique like "the code has some issues" gives the generator nothing to work with, and the next attempt often looks identical to the last one. Force the critic to output specific, line-referenced or quote-referenced issues, the way the JSON schema in the first example does.

Reflection theater. Adding a reflection step that always returns pass: true regardless of input is worse than no reflection at all, because it creates false confidence. Test your critic against known-bad outputs during development to confirm it actually catches problems, not just that it runs without erroring.

Cost blowup on cheap tasks. Not every agent call needs reflection. A one-line factual lookup doesn't benefit from a critique pass; a multi-file refactor does. Gate reflection behind task complexity, either a fixed list of task types that get the reflection treatment, or a cheap classifier call that decides whether the extra pass is worth it.

FAQ

Is agent reflection the same thing as chain-of-thought reasoning? No. Chain-of-thought is the model reasoning step by step within a single generation before producing a final answer. Reflection is a separate, subsequent pass that evaluates a completed output against the original goal, potentially using a different prompt, different evidence, or a different model call entirely. You can use both together: a model can think step by step while generating, and still benefit from a separate critique pass afterward.

Does reflection require a second model, or can the same model critique its own work? The same model can do both roles, and in practice this is the common setup because it's cheaper and simpler to operate. What matters more than using a different model is using a genuinely different prompt and, ideally, different evidence for the critique step. A critic prompt that just asks "is this good?" in the same conversational context as the generation tends to agree with itself; a critic prompt that restates the original requirements and checks each one explicitly, ideally in a fresh context window without the generator's own reasoning biasing it, catches more.

How many retry attempts should a reflection loop allow? Two to three is typical for most coding and writing tasks. Track the trend in issue count across attempts: if the number of issues found is dropping, keep going up to your max; if it's flat or rising, stop and escalate rather than burning more attempts on a loop that isn't converging.

When should I skip reflection entirely? Skip it for low-stakes, cheap, easily-reversible outputs, like a single factual lookup or a short summary the user can quickly eyeball themselves. Use it for anything with a real cost of being wrong: code that ships, multi-step plans that other steps depend on, structured output that downstream systems parse automatically, or any task where "looks plausible" and "is correct" commonly diverge.

Can reflection loops get stuck disagreeing with themselves forever? Yes, if you don't bound them. A generator and critic can ping-pong on a task with genuinely ambiguous requirements, each producing a "fix" that trades one issue for another. Always set a max-attempt limit and a clear behavior for what happens when that limit is hit (surface the failure with the critic's notes, don't silently return the last attempt as if it passed).

Does adding a self_check tool call slow down every agent run? It adds one extra model call per check, so yes, some latency and cost. The mitigation is to gate it: require the check only before the agent reports a task as fully complete, not after every intermediate tool call, and skip it entirely for task types where a wrong answer has low cost.