Self-Critique Loops for LLMs

AILLMAI AgentsMachine Learning
Share on LinkedIn

The first draft SQL query ran but scanned 40 million rows. A second pass — asking the model to review its own output against the requirements — caught the missing WHERE clause. Self-critique loops (generate → critique → revise) exploit the fact that LLMs are often better at finding flaws in existing text than producing perfect text initially. The technique works, but blind application triples latency and cost on tasks that didn't need a second opinion.

The Reflexion pattern

Draft → Critic evaluates → [pass? return draft] → Revise → [optional: re-critic] → Final
async def generate_with_reflection(
    task: str,
    max_iterations: int = 2,
) -> str:
    draft = await llm.complete(GENERATE_PROMPT, task)

    for i in range(max_iterations):
        critique = await llm.complete(CRITIC_PROMPT, f"Task: {task}\n\nDraft: {draft}")
        score = parse_score(critique)

        if score >= PASS_THRESHOLD:
            break

        draft = await llm.complete(REVISE_PROMPT, f"""
            Task: {task}
            Previous draft: {draft}
            Critique: {critique}
            Produce an improved version addressing all critique points.
        """)

    return draft

Three prompts, up to three LLM calls. Budget accordingly.

Writing effective critic prompts

Bad critic: "Review this and suggest improvements."

Good critic:

Evaluate the draft against these criteria:
1. Does the SQL query include a WHERE clause filtering by date?
2. Are all requested columns present?
3. Will this query use indexes (no full table scans)?
4. Is the output format valid JSON?

Score each criterion pass/fail. Overall score 1-5.
If any criterion fails, explain specifically what's wrong.
Do NOT rewrite — only critique.

Separate critique from revision. Combined "critique and fix" prompts produce shallow self-praise.

When reflection helps

Task type Reflection value
SQL/code generation High — catches syntax and logic errors
Multi-constraint writing High — checks requirements checklist
Factual Q&A with context Low — critique can't fix retrieval gaps
Classification Low — usually overthinking
Creative writing Mixed — can homogenize voice

Use a router:

if complexity_score(task) >= 4 or task_type in REFLECTION_TASKS:
    return await generate_with_reflection(task)
return await llm.complete(DIRECT_PROMPT, task)

Cost and latency control

Reflection multiplies cost. Mitigate:

REFLECTION_BUDGET = RunBudget(max_cost_usd=0.05, max_steps=3)

Self-critique in agents

Agents naturally reflect when tool results contradict their plan:

async def agent_step(state: AgentState) -> AgentState:
    action = await planner.decide(state)
    result = await execute_tool(action)

    if result contradicts state.plan:
        reflection = await llm.complete(
            "Why did this fail? What should we try next?",
            context=[state.plan, action, result],
        )
        state.add_reflection(reflection)

    return state

Store reflections in agent memory — they prevent repeating failed approaches (see Reflexion paper).

Evaluating reflection value

A/B test on your task set:

If quality improves 5% at 200% cost, route selectively.

Failure modes

Ground the critic in explicit criteria from the original task, not open-ended evaluation.

Reflexion pattern for agents

Store reflections in memory to prevent repeating failed approaches:

class ReflexionAgent:
    def __init__(self, llm, memory: list[str] = []):
        self.llm = llm
        self.memory = memory  # accumulated reflections

    async def run(self, task: str, max_attempts: int = 3) -> str:
        for attempt in range(max_attempts):
            context = "\n".join(self.memory) if self.memory else ""
            action = await self.llm.generate(f"Task: {task}\n\nPast reflections:\n{context}")
            result = await self.execute(action)

            if result.success:
                return result.output

            reflection = await self.llm.generate(
                f"Task: {task}\nAction taken: {action}\nResult: {result.error}\n"
                f"What went wrong and what should be tried differently?"
            )
            self.memory.append(reflection)

        raise MaxAttemptsExceeded(task)

Each failed attempt generates a reflection stored in memory. Subsequent attempts explicitly avoid repeated mistakes.

Self-critique with explicit rubric

Open-ended critique produces vague feedback — use structured rubrics:

CRITIQUE_RUBRIC = """
Evaluate the draft response against these criteria (score 1-5 each):
1. Addresses the original question completely
2. Uses only information from provided context (no hallucination)
3. Appropriate length and tone
4. Correct format (JSON/markdown as specified)

Draft: {draft}
Original task: {task}
Context: {context}

Return JSON: {{"scores": {{...}}, "must_fix": ["..."], "revised": "..."}}
"""

Critic returns specific fixes, not generic "could be better." Revised output included in same call — one round-trip for critique + revision.

Cost-benefit analysis for reflection

Reflection adds 2–3× token cost — route selectively:

REFLECTION_ROUTING = {
    "high_stakes": True,   # legal, medical, financial — always reflect
    "complex_task": True,  # multi-step reasoning — reflect on failure
    "simple_faq": False,   # single-turn FAQ — skip reflection
    "creative": False,     # creative writing — reflection reduces quality
}

def should_reflect(task_type: str, first_attempt_quality: float) -> bool:
    if REFLECTION_ROUTING.get(task_type, False):
        return True
    return first_attempt_quality < 0.7  # reflect only if first attempt weak

Measure quality delta vs cost increase per task type before enabling reflection globally.

Failure modes

Production checklist

Resources

Frequently asked questions

Do self-critique loops always improve output quality?

No. They help on complex reasoning, code generation, and multi-constraint tasks where first drafts often miss requirements. They often hurt simple tasks — the critique introduces errors or verbosity. Route to reflection only when a complexity classifier scores above threshold, not on every request.

How many critique iterations should I run?

One generate-critique-revise cycle catches most fixable errors. Two cycles show diminishing returns and 3x cost. Cap at 2 iterations with a quality gate — if the critic scores the draft above 4/5, skip revision. Never loop unbounded.

Should the critic be the same model as the generator?

Use the same or stronger model for critique. A weaker critic misses errors the generator made. Common pattern: strong model critiques, mid-tier model generates (or vice versa for cost — mid generates, strong critiques only when needed). Never use the exact same prompt for both roles.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →