Reflection: draft, critique, revise
One pass drafts, a separate adversarial pass critiques it against a stated rubric, and a third rewrites using that critique. The critic is a distinct call with its own instruction, because a model asked to check its own work inside one completion tends to agree with itself.
Key Facts
- Level
- beginner
- Runtime
- Python • OpenAI API
- Pattern
- Inspectable flow with visible system boundaries
- Interaction
- Live sandbox • Script
- Updated
- 27 July 2026
Navigate this example
Pattern
Doctrine relationsWhich principles this pattern expresses in code, and which its default form breaks.Library
Browse examplesReopen the wider library to compare adjacent patterns and linked learning paths.Interaction
Run sandbox nowTry the interaction directly in this example’s guided sandbox surface.Source
Open full sourceRead the real implementation, highlighted checkpoints, and runtime requirements.MCP
Call via MCPUse the same resource inside agents, deterministic exports, and MCP setup flows.
Linked principles
Doctrine relations
These are reasoning shapes: how an agent decides, when it acts, what it remembers. For how to compose the interface around it, see the AI Pattern Blueprint. For triggers, scheduling, context loading and recovery, see Agent runtime architecture.
This pattern expresses these principles directly in code.
P7 · Establish trust through inspectability
The critique is a typed artifact rather than a hidden step: failing_rules and what_to_change are surfaced on every round, so a reader can see why a draft was rejected and judge whether the critic was right. A reflection loop that only prints the final text has thrown away the part worth inspecting.
reflection.py:Critique
The textbook form of this pattern violates these by default. Each one carries the fix.
P2 · Ensure that background work remains perceptible
Each round is two or three model calls, so a three-round loop can run for a long time behind a single silent call. Fix, applied here: emit the round number and phase before each call, so a loop that is still working is distinguishable from one that has stalled.
reflection.py:runP10 · Optimise for steering, not only initiating
The rubric is fixed at the start and the loop runs to acceptance or exhaustion. An operator watching a round fail for a reason they disagree with cannot amend the rubric or accept the draft as-is without restarting. Fix: check for an amended rubric or an operator override between rounds, and treat exhaustion as a decision point rather than a return.
reflection.py:run
Your implementation decides these, not the pattern. No verdict is asserted.
P5 · Replace implied magic with clear mental models
Whether the reader understands that the answer was revised, and how many times, is a wrapper decision. This implementation prints the arc; a version that returns only the accepted text presents a revised answer as if it were a first draft. The pattern does not decide which you get.
reflection.py:run
Doctrine relations are first-party analysis, not the output of a validation run. Run the validator on your own code to get a scored verdict.
reflection.py
"""
Reflection: draft, critique, revise
The cheapest reliability win available. One model drafts, a second pass
critiques that draft against a stated rubric, and a third rewrites using the
critique. Loop until the score clears the bar or the budget runs out.
Draft -> Critique (scored) -> Revise -> ... -> Accept or exhaust
The critic is a separate call with a separate instruction, not the same call
asked to "check your work". A model grading its own output inside one
completion tends to agree with itself.
Two things worth knowing before you reach for this:
- It costs you a multiple of the latency and tokens of a single call. Spend
it where quality matters more than speed, not everywhere.
- Scores from a model cluster hard around the middle of whatever range you
give it. A 1 to 10 scale mostly returns 6, 7 and 8. Ask for specific
boolean checks instead when you need a real gate.
Run: python reflection.py
"""
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
client = OpenAI()
MODEL = "gpt-5.4-mini"
MAX_ROUNDS = 3
#: What "good" means here, stated once so the critic and the reviser cannot
#: drift apart. Editing this changes the gate for both.
RUBRIC = """
1. Every claim is supported by something in the brief. No invented specifics.
2. States what is NOT covered, rather than implying full coverage.
3. Concrete nouns and verbs. No filler adjectives.
4. Under 120 words.
""".strip()
class Critique(BaseModel):
"""Boolean checks, not a 1-to-10 score.
A model asked for a score returns the middle of the range almost
regardless of quality. Specific checks force it to commit to something
falsifiable, and they give the reviser an actionable list.
"""
passes_rubric: bool = Field(description="True only if every numbered rule holds.")
failing_rules: list[int] = Field(
description="Rule numbers that fail. Empty when passes_rubric is true."
)
what_to_change: str = Field(
description="Concrete instruction for the rewrite. Empty if nothing to change."
)
def draft(brief: str) -> str:
completion = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": "Write a short internal summary for the given brief.",
},
{"role": "user", "content": brief},
],
)
return completion.choices[0].message.content or ""
def critique(brief: str, text: str) -> Critique:
"""A separate call with an adversarial instruction, not self-review."""
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"You are reviewing someone else's draft against a rubric. "
"You did not write it and you gain nothing by approving it. "
"Mark a rule as failing whenever you are unsure it holds.\n\n"
"Rubric:\n" + RUBRIC
),
},
{"role": "user", "content": f"Brief:\n{brief}\n\nDraft:\n{text}"},
],
response_format=Critique,
)
return completion.choices[0].message.parsed
def revise(brief: str, text: str, verdict: Critique) -> str:
completion = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Rewrite the draft to fix exactly the listed problems. Change "
"nothing else. Do not add new claims.\n\nRubric:\n" + RUBRIC
),
},
{
"role": "user",
"content": (
f"Brief:\n{brief}\n\nDraft:\n{text}\n\n"
f"Failing rules: {verdict.failing_rules}\n"
f"Required change: {verdict.what_to_change}"
),
},
],
)
return completion.choices[0].message.content or ""
def run(brief: str) -> str:
print(" drafting...")
text = draft(brief)
print(f" draft: {text[:90]}...")
for round_number in range(1, MAX_ROUNDS + 1):
print(f" [{round_number}/{MAX_ROUNDS}] critiquing...")
verdict = critique(brief, text)
if verdict.passes_rubric:
print(f" ACCEPTED after {round_number} round(s)")
print(f" final: {text}")
return text
print(f" failing rules: {verdict.failing_rules}")
print(f" change : {verdict.what_to_change}")
text = revise(brief, text, verdict)
print(f" revised: {text[:90]}...")
# Budget exhausted is a real outcome. Return the best text with the
# honest caveat rather than presenting it as if it passed.
print(f" NOT ACCEPTED after {MAX_ROUNDS} rounds, returning last revision")
return text
if __name__ == "__main__":
brief = (
"Our nightly sync job failed twice this week. Both times it was the "
"same downstream timeout. We have not yet found why the timeout "
"started happening. Write the update for the team channel."
)
print(f"\n> {brief[:70]}...")
run(brief)
Related principles
- P2visibilityEnsure that background work remains perceptibleWhen the system is operating asynchronously or outside the user’s immediate focus, it should provide persistent and proportionate signals that work is continuing.Open principle →
- P5delegationReplace implied magic with clear mental modelsThe product should help users understand what the system can do, what it is currently doing, what it cannot do, and what conditions govern its behaviour.Open principle →
- P7trustEstablish trust through inspectabilityUsers should be able to examine how a result was produced when confidence, accountability, or decision quality is important.Open principle →
- P10delegationOptimise for steering, not only initiatingThe system should support users not only in starting tasks, but also in guiding, refining, reprioritising, and correcting work while it is underway.Open principle →