Lesson 4 - Reflection and Self-Correction

Welcome to Reflection and Self-Correction

Your agent can plan ahead with decomposition and reason as it goes with ReAct. But there’s still a gap: the moment it produces an answer, it’s done — even when that answer quietly missed something the task asked for. People don’t work that way. You write a first draft, then you read it back against what was wanted and fix what’s off. That second pass — checking your own work — is exactly what this lesson adds. Reflection is a step that runs after the agent produces a result: it critiques its own draft against the task, and if the critique finds problems, it revises and checks again. It’s the difference between an agent that hands you its first guess and one that catches “wait, the traveler is vegetarian” before it finishes. This is the third and final planning pattern, and once you have it, you’ve met all three.

By the end of this lesson, you will be able to:

  • Explain reflection as a critique-then-revise step that runs after producing a result
  • Build a reflect-and-revise loop that critiques a draft against the task and revises until it passes
  • Use an exact “OK” sentinel from the critique to decide when the loop should stop
  • Bound the loop with max_revisions so reflection can’t spin forever
  • Judge when reflection is worth its extra model calls and when it’s overkill

Let’s start with what reflection actually is.


Reflection Is a “Check Your Own Work” Step

The drafting pass and the checking pass are not the same job, and that’s the whole insight. When the model writes a first draft, it’s juggling everything at once — the request, the tone, the format, the constraints — and in that rush it’s easy to gloss over a requirement. A fresh pass that does nothing but compare the draft to the task, with no pressure to also produce content, catches misses the drafting pass slid past.

So reflection has a simple shape: draft, critique against the task, revise, loop until the critique is satisfied. The agent produces a result, then steps back and asks itself, “Does this actually satisfy every constraint?” If yes, it’s done. If no, it lists what’s wrong and rewrites — and then checks again, because a revision can introduce a new problem or only partly fix the old one. That loop continues until the critique comes back clean.

The reflect-and-revise loop. A Draft box flows into a Critique box ('Does this satisfy every constraint?'); if the critique finds problems it flows to a Revise box that produces a new draft and loops back to Critique; if the critique says OK it exits to the Final answer. A caption notes the agent checks its own work and fixes it before finishing.
Reflection: the agent drafts, critiques its own draft against the task, and revises — looping until the critique is satisfied, then returns the final answer.

Notice where this sits relative to the other two patterns. Decomposition plans before acting; ReAct reasons during the loop; reflection critiques after the result is in hand. They’re complementary, and we’ll see at the end how they layer.


Stage 1: The Draft

Everything starts with a first attempt. The agent takes the task and produces a draft the normal way — one model call, given the system prompt and the task as a user message. Atlas, our travel-planning agent, gets the task “Plan 3 dinners for a vegetarian traveler in Japan.”

draft_resp = client.messages.create(
    model=model, max_tokens=512, system=system,
    messages=[{"role": "user", "content": task}])
draft = "".join(b.text for b in draft_resp.content if b.type == "text")
print(f"Draft: {draft}")

This is just the agent answering — nothing reflective yet. We pull the text out of the response blocks the same way you have all module. The draft is the thing we’re about to scrutinize. Hold onto it; the next stage hands it back to the model for inspection.


Stage 2: The Critique — and the “OK” Sentinel

Now the second pass. We make another model call, but this one isn’t asked to plan dinners — it’s asked to judge the draft. We give it the task and the draft together, and we ask it to do one of exactly two things: reply with the literal string OK if the draft fully satisfies every constraint, or list what needs fixing if it doesn’t.

crit_resp = client.messages.create(
    model=model, max_tokens=256, system=system,
    messages=[{"role": "user",
               "content": f"Task:\n{task}\n\nDraft:\n{draft}\n\n"
                          f"Critique this draft against the task. If it fully "
                          f"satisfies every constraint, reply exactly OK. "
                          f"Otherwise list what to fix."}])
critique = "".join(b.text for b in crit_resp.content if b.type == "text").strip()
print(f"Critique {rnd}: {critique}")
if critique == "OK":
    print(f"Accepted after {rnd-1} revision(s).")
    return {"final": draft, "revisions": rnd - 1}

The phrase “reply exactly OK” is doing real work here. That exact string is a sentinel — a signal value the loop can test for. After stripping whitespace, the code asks a yes-or-no question: if critique == "OK". If it matches, the draft passed its own check and we return it. If it doesn’t match, the critique is the list of fixes, and we fall through to revising. The sentinel is what turns a free-form text response into a clean stop condition. Without it, you’d have to guess from prose whether the model was satisfied — brittle and unreliable. With it, the decision is a single string comparison.


Stage 3: Revise, Then Loop

When the critique isn’t OK, it’s a to-do list. We make a third kind of call: hand the model the task, its own draft, and the critique, and ask it to rewrite to fix what the critique flagged. The result becomes the new draft, and control loops back to the critique stage to check the revision.

rev_resp = client.messages.create(
    model=model, max_tokens=512, system=system,
    messages=[{"role": "user",
               "content": f"Task:\n{task}\n\nYour draft:\n{draft}\n\n"
                          f"Critique:\n{critique}\n\nRewrite to fix it."}])
draft = "".join(b.text for b in rev_resp.content if b.type == "text")
print(f"Revision {rnd}: {draft}")

The revision is given the critique on purpose — it’s not rewriting blind, it’s rewriting to address specific complaints. That’s why reflection improves things rather than just shuffling them: each rewrite has a target. After the rewrite, we go back to the top of the loop and critique again, because the only way to know the fix worked is to re-check it.

Here’s the full orchestration with all three stages assembled:

def run_reflect_revise(client, task, *, system, model="claude-haiku-4-5", max_revisions=2):
    # Draft
    draft_resp = client.messages.create(
        model=model, max_tokens=512, system=system,
        messages=[{"role": "user", "content": task}])
    draft = "".join(b.text for b in draft_resp.content if b.type == "text")
    print(f"Draft: {draft}")

    for rnd in range(1, max_revisions + 1):
        # Critique
        crit_resp = client.messages.create(
            model=model, max_tokens=256, system=system,
            messages=[{"role": "user",
                       "content": f"Task:\n{task}\n\nDraft:\n{draft}\n\n"
                                  f"Critique this draft against the task. If it fully "
                                  f"satisfies every constraint, reply exactly OK. "
                                  f"Otherwise list what to fix."}])
        critique = "".join(b.text for b in crit_resp.content if b.type == "text").strip()
        print(f"Critique {rnd}: {critique}")
        if critique == "OK":
            print(f"Accepted after {rnd-1} revision(s).")
            return {"final": draft, "revisions": rnd - 1}

        # Revise using the critique
        rev_resp = client.messages.create(
            model=model, max_tokens=512, system=system,
            messages=[{"role": "user",
                       "content": f"Task:\n{task}\n\nYour draft:\n{draft}\n\n"
                                  f"Critique:\n{critique}\n\nRewrite to fix it."}])
        draft = "".join(b.text for b in rev_resp.content if b.type == "text")
        print(f"Revision {rnd}: {draft}")
    return {"final": draft, "revisions": max_revisions}

Run it on the vegetarian-dinners task, and you can watch reflection do its job:

Draft: Day 1: sushi tour. Day 2: wagyu beef dinner. Day 3: ramen.
Critique 1: The traveler is vegetarian but Day 1 sushi and Day 2 wagyu are not. Replace them with vegetarian options.
Revision 1: Day 1: vegetarian kaiseki. Day 2: tofu & vegetable hot pot. Day 3: vegetarian ramen.
Critique 2: OK
Accepted after 1 revision(s).

Look at what happened. The first draft missed the vegetarian constraint entirely — sushi and wagyu beef are not vegetarian. The drafting pass, focused on producing three plausible Japanese dinners, glossed right over the single most important requirement. The critique pass, doing nothing but comparing the draft to the task, caught it immediately and said exactly what to fix. The revision swapped in vegetarian options, the second critique returned OK, and the loop stopped. The agent corrected its own mistake before handing anything back — which is the entire point.


Bounding the Loop: Why max_revisions Matters

There’s a parameter we’ve quietly relied on: max_revisions, and the for rnd in range(1, max_revisions + 1) that uses it. This is not optional polish — it’s a safety rail, and reflection is dangerous without it.

A reflect-and-revise loop has no guaranteed stopping point on its own. If the critique never returns OK — because the task is genuinely impossible to fully satisfy, or because the model keeps finding new nitpicks, or because each revision introduces a fresh problem — an unbounded loop runs forever, burning model calls and money with nothing to show. Worse, reflection can sometimes make output worse: a model second-guessing a perfectly good draft may “fix” things that weren’t broken. Bounding the loop caps both the cost and the damage. After max_revisions rounds, the function returns the best draft it has, OK or not, and moves on.

Reflection needs a budget, not infinite patience

Treat max_revisions as a budget for self-correction, not a target. Most genuine constraint misses are caught and fixed in the first revision — as in the example above, where one round was enough. A small bound (two or three) catches the common case while guaranteeing the loop terminates. If your agent regularly exhausts its budget without reaching OK, that’s a signal worth heeding: the task may be ambiguous, the constraints may conflict, or the critique prompt may be too picky. The fix is rarely “allow more revisions” — it’s usually a clearer task or a more focused critique.


Why Reflection Works — and What It Costs

It’s worth being precise about why a second pass helps, because it’s not magic. The drafting call and the critique call are separated on purpose. When the model drafts, it’s generating content under many simultaneous pressures and can let a requirement slip. When it critiques, it has one job — compare draft to task — and a concrete artifact to examine. That focus is what surfaces the constraint the drafting pass missed. It’s the same reason a fresh reader catches typos the author can’t see: a different task, with the work already in front of you, exposes different errors.

But reflection is not free, and you shouldn’t reach for it on every turn. Each round is extra model calls and extra latency — one critique call always, plus one revision call for every round that isn’t clean. A task that needs two revisions costs five model calls instead of one. That’s a real tax on cost and speed. So reserve reflection for outputs where getting it right matters: high-stakes answers (a final plan the user will act on) and constraint-heavy tasks (multiple requirements that are easy to drop, like our vegetarian-in-Japan dinners). For a quick currency conversion or a one-line factual reply, reflection is pure overhead — there’s nothing to miss and nothing to fix.

This is the third and last of the module’s three patterns, and they layer rather than compete: decompose before acting, reason (ReAct) during the loop, and reflect after producing the result. A serious agent uses all three on a hard task — plan the steps, reason through each one, then check the whole result before finishing. The guided project up next combines exactly these three into one Atlas that plans, reasons, and self-corrects end to end.


Practice Exercises

Exercise 1: Why bound the loop?

The loop runs for rnd in range(1, max_revisions + 1). Suppose you removed that bound and looped while critique != "OK" instead. Describe two distinct ways that could go wrong on a real task.

Hint

First failure: the critique might never return OK — the task could be impossible to fully satisfy, or the model keeps finding new nitpicks each pass — so the loop runs forever, burning model calls and money with no output. Second failure: each revision can introduce a new problem or “fix” something that was already fine, so the output can drift sideways or even get worse with more rounds. max_revisions caps both the runaway cost and the quality drift by guaranteeing the loop terminates and returns the best draft it has.

Exercise 2: Why an exact “OK” sentinel?

The critique prompt asks the model to “reply exactly OK” when the draft passes, and the code checks if critique == "OK". What would break if the model were chatty instead — say it replied “Looks great, this fully satisfies the task!”?

Hint

The string comparison critique == "OK" would be False even though the draft was actually fine, so the loop would treat a passing critique as a list of fixes and revise a draft that didn’t need revising — wasting a call and risking making it worse. The exact sentinel turns a free-form text answer into a clean, testable stop condition: one string comparison decides the loop. A chatty model defeats that, which is why the prompt insists on the literal OK and the code .strip()s before comparing. (In production you’d also guard against minor variations — lowercasing, or checking the response starts with “OK” — but the principle is the same: you need a reliable signal, not prose to interpret.)

Exercise 3: When is reflection worth it?

For each task, decide whether reflection earns its extra model calls or is overkill: (a) “Convert 50 USD to euros.” (b) “Plan a 5-day trip that stays under budget, avoids cold weather, and includes vegetarian food.” (c) “What’s the capital of Japan?”

Hint

(a) and (c) are overkill — single-fact, single-step answers with nothing to miss and nothing to fix; a critique pass just adds cost and latency for no gain. (b) is exactly where reflection earns its keep: it’s high-stakes (a plan the user will act on) and constraint-heavy (budget, weather, food, dates all at once), so the drafting pass can easily drop one constraint — and a critique pass catches it before the agent finishes. The rule of thumb: reserve reflection for high-stakes or constraint-heavy outputs, not every turn.


Summary

Reflection adds a “check your own work” step that runs after the agent produces a result: draft, critique the draft against the task, revise if needed, and loop until the critique is satisfied. The critique is its own model call — given the task and the draft, asked to reply with the literal OK when every constraint is met or to list fixes otherwise. That exact-OK sentinel is what lets the loop decide when to stop with a single string comparison. The loop is bounded by max_revisions so it can’t spin forever or drift worse — a budget for self-correction, not infinite patience. Reflection works because a fresh critique pass, separate from drafting, catches constraint misses the drafting pass glossed over — in the verified run, the first draft missed the vegetarian constraint and reflection corrected it in one revision. The cost is extra model calls and latency, so you reserve it for high-stakes or constraint-heavy outputs rather than every turn. This is the third of the module’s three patterns — decompose before, ReAct during, reflect after — and they layer.

Key Concepts

  • Reflection — critique-then-revise after producing a result; loop until the critique passes.
  • The “OK” sentinel — an exact signal value the critique returns when satisfied, giving the loop a clean stop condition.
  • max_revisions — bounds the loop so it terminates; without it, reflection can run forever or make things worse.
  • Cost vs. benefit — every round is extra calls and latency; reserve reflection for high-stakes or constraint-heavy outputs.

Why This Matters

The difference between an agent that hands you its first guess and one you’d trust with a real plan is often a single reflection pass. Constraint misses — the vegetarian traveler, the budget cap, the avoided date — are exactly the errors a confident first draft makes and a focused second pass catches. Knowing how to add that pass and how to bound it keeps your agent from both classes of failure: shipping a flawed answer, and looping forever trying to perfect it. With reflection in hand you’ve now met all three planning patterns — decomposition, ReAct, and reflection — and the next lesson brings them together on one agent.


Next Steps

Continue to Lesson 5 - Guided Project: A Planning, Reasoning Atlas

Combine decomposition, ReAct, and reflection into one agent that plans, reasons, and self-corrects.

Back to Module Overview

Return to the Planning and Reasoning module overview


Continue Building Your Skills

You can now give Atlas a “check your own work” step: it drafts, critiques its draft against the task, and revises until the critique returns OK — bounded by max_revisions so it always finishes. That completes the three planning patterns — decompose before, reason during, reflect after. Next, the guided project brings all three together into a single Atlas that plans a hard request into steps, reasons through each one, and reflects on the result before handing it back.