Self Reflection Agents
A self-reflection agent reads its own draft like a reviewer, names what is wrong, and rewrites once before the answer ships.
Why Does This Exist?
A single model call ships its first draft. Ask for a mean function and it writes sum(xs) / len(xs), which is correct on every list except the empty one, where it divides by zero. In a demo you shrug. In a pipeline the crash pages someone at night.
A self-reflection agent adds a reviewer between the draft and the user. The generator writes, a critic checks the draft against a short list of requirements, and the generator rewrites once with the critique in front of it. The loop catches the errors a first draft almost always carries.
This page covers that draft-critique-revise loop and its stop rule. The step-by-step loop underneath is ReAct. Breaking the job into steps first lives in task decomposition. Splitting the work across people instead of drafts lives in supervisor subagent patterns.
Basics first: the critic never solves the task. It reads a draft plus the requirements and returns either PASS or a named miss. Generation and judgment stay in separate calls so the second can disagree with the first.
Think of It Like This
The cook who tastes before plating
A line cook finishes a sauce and does not send it out. They spoon a taste, find it under-salted, add a pinch, and taste again. The first taste is the draft. The wince is the critique. The pinch is the revision. The second taste that passes is the stop rule.
Map it point for point. The recipe is the task. The spoon is the test the critic runs. The salt gap is the named miss. The re-taste is the second draft checked against the same spoon.
Where it stops: a cook trusts one tongue. An agent should not trust one model twice on hard tasks, because the same blind spot can approve its own error. That is why the critic gets a checklist and a low temperature while the generator stays creative.
How It Actually Works
Two roles circle one draft. Call the draft , the critique , the revision , and the requirement list . Here has four checks: correct mean on [2, 4, 6], correct mean on [5], empty list returns instead of crashing, and no side effects. The critic returns PASS only when all four hold.
The three calls
- Generate. The generator sees the task plus and writes . No tools, one call, first attempt kept honest by the checklist in the prompt.
- Critique. The critic sees the task, , and , and runs each check. It writes : which checks passed, which failed, and the smallest fix. It never rewrites the code itself.
- Revise. The generator sees the task, , and , and writes . Then the critic reads once more. PASS ships it. Anything else stops at the turn cap.
Worked example
Task: "Write mean(xs) over [2, 4, 6], [5], and []."
Draft is return sum(xs) / len(xs). Score it by hand. On [2, 4, 6] the sum is , divided by gives . Correct. On [5] the sum is , divided by gives . Correct. On [] the sum is and the length is , so raises. That is of checks, and the critic writes: "Fail: empty list divides by zero. Fix: return when the list is empty."
Revision adds the guard first, then the division. Score it again: , , , no side effects. That is of , so the critic returns PASS. Two drafts plus one critique make model calls against for single-pass, which is the price of the catch.
Use this loop for code, math, and plans a reviewer can check. Skip it for chat, where a second call buys polish nobody asked for and doubles the bill.
Code
The point here is the checklist, not a model API. This stdlib-only script replays the same drafts and counts the same checks, so the numbers match the worked example:
def draft_v1(xs: list[float]) -> float: return sum(xs) / len(xs)
def draft_v2(xs: list[float]) -> float: if not xs: return 0.0 return sum(xs) / len(xs)
def checks(fn) -> int: score = 0 if fn([2.0, 4.0, 6.0]) == 4.0: score += 1 if fn([5.0]) == 5.0: score += 1 try: ok = fn([]) == 0.0 except ZeroDivisionError: ok = False if ok: score += 1 if fn([2.0, 4.0, 6.0]) == 4.0: # pure: same input, same output score += 1 return score
print(checks(draft_v1)) # -> 3print(checks(draft_v2)) # -> 4print((2.0 + 4.0 + 6.0) / 3) # -> 4.0Draft one scores , draft two scores , and the mean line reproduces . The guard is one if statement, and that statement is the whole difference between shipping and paging.
Watch Out For
The reviewer who approves its own error
Symptom: generator and critic share one model, one temperature, one prompt style, and PASS arrives on a draft that still crashes. The critic shares the blind spot it was asked to check.
Give the critic a written checklist, a low temperature, and no permission to rewrite. On important tasks use a stronger model for the critic than the generator. Agreement without a checklist is not a review.
A revision loop with no cap
Symptom: the trace shows draft three, critique three, draft four, each slightly reworded, the bill climbing with every call. The pair disagrees forever and nothing ships.
Cap the loop at two or three drafts in code, not in prose, and ship the best-scoring draft when the cap hits. Log every score so you can see whether revision three ever beats revision one.
The Quick Version
- Self-reflection splits one task into a generator that drafts and a critic that checks against a written list.
- A worked mean function scores 3 of 4 on draft one and 4 of 4 on draft two after adding an empty-list guard.
- The critic returns PASS or one named miss, and the loop stops on PASS or a cap near three drafts.
- Two drafts plus one critique cost 3 model calls against 1 for single-pass generation.
- A shared model reviewing itself needs a checklist and low temperature, or approvals mean nothing.