The Self-Critique Architecture for AI Agents — Four Reflection Patterns That Make Antigravity Outputs Trustworthy
Four production-tested patterns for adding self-critique loops to Antigravity agents, with implementation code, stopping conditions, confidence calibration, and cost-control strategies.
One night, I ran my release-notes agent three times in a row and threw all three drafts away. Nothing was wrong with them. I just couldn't quite trust them. The first paragraph in each draft read like something scraped from an unrelated marketing site — technically accurate, structurally correct, and somehow not mine. The bullet points were precise. The headline was clean. But every time I read it back, my hand stopped before clicking "publish."
What I realized that night was that I didn't want a "regenerate" button. I wanted an agent that would read its own draft once, doubt itself, and then submit. A skilled writer almost never ships their first draft. They re-read, the tone shifts, weak claims get evidence, paragraphs collapse. An agent without that re-reading step seems to hit a quality ceiling no model size can lift you past. Larger models produce more polished first drafts, but the gap between "polished" and "trustworthy" never closes by scale alone — only by structure.
This article walks through four self-critique (reflection) patterns I've been running on Antigravity agents in production. The implementation, the stopping conditions, the cost controls — all included. It builds on the Reflexion line of research but is shaped to survive the realities of small, solo-built projects, where you cannot afford a separate evaluation team and every additional API call shows up on your own credit card.
Why "just regenerate" isn't enough
The most naive quality lever is to drop temperature and resample. Generate three candidates, pick the best, ship. This catches some classes of errors and is genuinely worth doing as a baseline. Resampling is cheap, requires no new architecture, and addresses the most basic kind of LLM failure: stochastic word-choice drift.
But resampling only handles variance. If the model started from a flawed assumption — misread a requirement, missed a constraint, used outdated API conventions — every sample will share that blind spot. "Three rolls and all three feel off" is rarely noise. It's almost always a structural blind spot. The samples differ in surface details but agree on the underlying mistake, and no aggregation strategy can rescue you from agreement on the wrong answer.
Self-critique loops attack this exact gap. Instead of evaluating the output as text, they evaluate the output against the requirements at one level of abstraction up, and feed that gap analysis back into the next sample. The critic isn't asking "is this paragraph well-written?" — that's the same question the generator already failed to answer correctly. The critic is asking "does this paragraph satisfy the constraints I was given?" That second question is harder to fake, harder to drift on, and crucially uses different reasoning paths than generation itself.
Public docs often summarize this as "spin up a separate sub-agent for verification." That phrasing hides the design decisions that actually decide whether your agent works. Should the critic be a separate model call or built into the same prompt? What counts as a stop condition? Which mistakes can a single critic catch and which need multiple perspectives? How do you keep the critic from becoming a perfectionist that never approves anything? Those questions are where this entire post lives.
In my experience, teams that adopt self-critique without thinking carefully about these questions end up worse off than where they started. Their pipelines become slower, more expensive, and produce outputs that are noticeably more cautious and less distinctive than what the unmodified generator would have produced. That's the failure mode to avoid.
Pattern 1: Generator-Critic split
The cleanest baseline. Two distinct prompts: a Generator that drafts, a Critic that judges. The Generator retries — informed by the Critic's findings — until the Critic returns "approve." This is the architecture most papers describe and the one most easily understood by collaborators who haven't seen the system before.
# self_critique_loop.pyfrom google import genaifrom pydantic import BaseModelfrom typing import Literalclient = genai.Client()class CriticVerdict(BaseModel): """Structured critic output. Separating verdict from issues makes the next-step action unambiguous for the Generator.""" verdict: Literal["approve", "revise"] confidence: float # 0.0 - 1.0 issues: list[str] # concrete revision instructions when verdict == "revise"GENERATOR_PROMPT = """You are a release-notes writer.Changes: {changes}Past feedback: {feedback}If past feedback exists, address it as the highest priority."""CRITIC_PROMPT = """You are a technical editor. Decide whether therelease notes draft below is shippable.Draft:{draft}Requirements:- Reader can tell what changes mean for them- No marketing-flavored adjectives- Known constraints and workarounds are not omittedIf verdict is revise, list 1-3 concrete rewrite instructions in issues."""def self_critique(changes: str, max_iterations: int = 3) -> tuple[str, list[str]]: feedback: list[str] = [] history: list[str] = [] for i in range(max_iterations): # 1. Generator drafts gen_resp = client.models.generate_content( model="gemini-2.5-pro", contents=GENERATOR_PROMPT.format( changes=changes, feedback="\n".join(feedback) if feedback else "(none)", ), ) draft = gen_resp.text history.append(draft) # 2. Critic judges critic_resp = client.models.generate_content( model="gemini-2.5-pro", contents=CRITIC_PROMPT.format(draft=draft), config={"response_mime_type": "application/json", "response_schema": CriticVerdict}, ) verdict: CriticVerdict = critic_resp.parsed if verdict.verdict == "approve" and verdict.confidence >= 0.8: return draft, history feedback = verdict.issues # Max iterations reached — return best draft for human review return history[-1], history
The max_iterations=3 cap is empirical but important. Beyond four iterations, quality saturates in most of my workloads while cost keeps climbing linearly. Stop early; you'll be glad you did. The intuition is straightforward: real improvements happen on the first or second pass when the critic catches something fundamental. By iteration four, the model is already chasing diminishing returns, and by iteration five it's often actively introducing regressions to satisfy a critic that has run out of legitimate complaints.
I started out assuming higher caps would help. The opposite turned out to be true: at five or seven iterations, the Critic itself starts to drift. It begins demanding "a slightly more careful tone" or "a touch more readability" without ever being satisfied, the Generator dutifully complies, and the prose loses every distinctive feature it had. The end result is generic, anodyne text that nobody would attach their name to. It looks a lot like what happens to me when I edit my own writing too long. There's such a thing as critic fatigue, and you have to design for it.
There's a second subtle issue with the split pattern: the critic can only see the final output, not the reasoning that produced it. If the generator made a wrong choice and then defensively justified that choice in the prose, the critic sees a coherent argument and approves. Solving this requires either Pattern 2 (where reflection sees the reasoning) or attaching the generator's chain-of-thought to the critic's input — which trades cost for visibility.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Move from 'I never trust the agent's first output and re-run it manually every time' to a system where self-critique loops keep quality consistent without you watching
✦Apply four concrete reflection patterns — with full implementation code — to your own agents today instead of just retrying with lower temperature
✦Avoid the cost blow-up that kills naive reflection loops by learning the stopping conditions, confidence thresholds, and budget controls that hold up in production
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Instead of splitting Generator and Critic across two calls, you fold the loop inside a single agent: draft, re-read, list gaps, finalize. This costs roughly 30-40% less than the split version and works surprisingly well for short, well-specified tasks. It also has the advantage that the model sees its own intermediate reasoning when reflecting, which catches a class of self-justification errors the split pattern misses.
SELF_REFLECT_PROMPT = """Answer in three steps.Step 1: Write a draft.Step 2: Re-read the draft and list 3 gaps versus the requirements.Step 3: Write the final version that closes those gaps.Wrap your final output in <final></final> tags.Task: {task}"""def self_reflection(task: str) -> str: resp = client.models.generate_content( model="gemini-2.5-pro", contents=SELF_REFLECT_PROMPT.format(task=task), config={"thinking_config": {"thinking_budget": 8192}}, ) text = resp.text import re m = re.search(r"<final>(.*?)</final>", text, re.DOTALL) return m.group(1).strip() if m else text
This pattern shines when requirements are easy to articulate and the task is short — commit messages, PR titles, API descriptions, error messages. It struggles on long documents and multi-step code generation, where self-contained reflection can't escape its own initial bias. The model can't fully step outside its own framing of the problem; it can polish but not reframe.
The thinking budget setting matters more than people expect. Without it, the model rushes through Step 2 in two or three sentences and produces shallow, generic gap analysis. With a budget of 4,096 to 8,192 tokens specifically allocated for the reflection step, the gap analysis becomes substantive and the final draft genuinely incorporates it. I treat the thinking budget as the lever that decides whether the reflection is real reflection or theater.
A practical pattern I use: ask for exactly three gaps. Not "any gaps" — three. The forced count prevents the model from declaring there are no gaps when there obviously are some. It also caps the over-reflection failure mode where the model lists eleven minor stylistic preferences and then doesn't know which to address. Three is small enough to stay actionable and large enough to surface the structural issues that matter.
That limitation around long documents is exactly what Pattern 3 exists to fix.
For high-stakes outputs, run several Critics in parallel, each from a different vantage point. On Antigravity's Manager Surface this falls naturally onto sub-agents. The cost is real — three to four times a single-critic run — but for outputs that will be read by hundreds or thousands of people, the math usually works out.
In my own documentation generation pipeline I run three critics:
Technical critic: working code, correct API usage, type integrity, version-specific syntax
Editorial critic: tone consistency, redundancy, readability, paragraph structure
User critic: simulates a developer with one year of experience reading this for the first time and flags exactly where they would get stuck
import asyncioasync def multi_perspective_review(draft: str) -> list[CriticVerdict]: perspectives = [ ("Technical critic", "Verify working code, types, and correct API usage rigorously."), ("Editorial critic", "Evaluate tone consistency, redundancy, and readability."), ("User critic", "Read as a developer with one year of programming experience and " "flag exactly where you would get stuck."), ] async def run_one(role: str, instr: str) -> CriticVerdict: prompt = f"You are the {role}. {instr}\n\nDraft:\n{draft}" return await call_critic_async(prompt) return await asyncio.gather(*[run_one(role, instr) for role, instr in perspectives])
Final output requires all three critics to approve. If any returns revise, you merge the issue lists and feed them back to the Generator.
The trap here is contradictory critics. Technical critic says "this paragraph is redundant," user critic says "no, please keep this part — beginners need it." That conflict happens often, perhaps every fifth or sixth draft in my pipelines. I added a fourth role I call the Resolver, which arbitrates with an explicit priority: when critics disagree, prefer the user critic.
Without an explicit priority, the Generator gets stuck in limbo. I learned this the hard way during a stretch where output quality actually fell after I added more critics — more perspectives meant more deadlock. The generator received contradictory issue lists, tried to satisfy all of them, and produced text that satisfied none. Whenever you add a viewpoint, write down who has the final say. Otherwise you've added noise, not nuance.
The choice of priority is itself a product decision. For documentation aimed at beginners, the user critic wins. For internal tooling read only by senior engineers, the technical critic wins. For marketing-adjacent content, the editorial critic wins. There is no universally correct priority order, and trying to design one will lead you to either freeze or to keep adjusting it forever. Pick one, ship it, observe the failure modes for two weeks, then revisit.
Pattern 4: Confidence-gated critic
Running a critic on every output is sometimes too expensive. For agents handling hundreds of tasks per day, let the Generator decide whether the output even needs critic review. This is the pattern I deploy whenever I am genuinely volume-constrained — when the marginal task no longer justifies the cost of a full critic loop.
class GeneratorOutput(BaseModel): answer: str self_confidence: float # 0.0-1.0 self-assessment risky_points: list[str] # places the agent itself flags as uncertaindef confidence_gated(task: str, threshold: float = 0.85) -> str: gen_resp = client.models.generate_content( model="gemini-2.5-pro", contents=f"Answer the task and return self-confidence and risky points: {task}", config={"response_mime_type": "application/json", "response_schema": GeneratorOutput}, ) output: GeneratorOutput = gen_resp.parsed if output.self_confidence >= threshold and not output.risky_points: return output.answer # Only invoke the critic when confidence is low or risk flags exist return run_critic_loop(output.answer, hints=output.risky_points)
The make-or-break detail is calibration. Raw self_confidence from an LLM is uncalibrated — typically too high (everything comes back at 0.95) or defensively low (everything at 0.6). I keep a rolling log of the last 100 (self_confidence, human_review_outcome) pairs and apply a learned correction factor before applying the threshold. This is essentially Platt scaling adapted to LLM self-assessment, and it makes a measurable difference once you have a few hundred samples to fit against.
I shipped my first version with threshold=0.85, which dropped critic invocation below 5%. That sounds great until you discover the slipped bugs in production. I now run between 0.7 and 0.8, which puts critic invocation in the 20-30% range — empirically the sweet spot for my workloads. The exact number is workload-dependent: for outputs with high blast radius (anything customer-facing) I lean toward 0.7. For internal-only outputs where the cost of a mistake is small, I push toward 0.85.
The risky_points field deserves separate attention. When the generator flags specific concerns, those flags often turn out to be genuinely informative. The model knows when it's uncertain about a particular API name, a version-specific behavior, a date, or a numeric claim. Surfacing those flags to the critic as targeted hints means the critic spends its budget on the parts that actually need scrutiny rather than re-evaluating the whole output. This makes confidence-gated critics significantly more efficient than blanket reviews.
Three stopping conditions to prevent runaway loops
Cost blow-up is the most dangerous failure mode of self-critique. A loop that won't terminate can spend ten times the normal token budget on a single task. Always combine these three stopping conditions:
Iteration cap: max_iterations = 3. Anything that fails three rounds goes to a human
Token budget: set a per-task ceiling at the start of the loop and bail when cumulative usage exceeds it
Stagnation detection: if the last two Critic verdicts return the same issues, the agent is stuck on the same blind spot — bail out
Stagnation detection helps quietly but reliably. Critics often paraphrase the same complaint, so set comparison misses real repetitions. Switching to embedding similarity (cosine ≥ 0.9 as "same issue") sharpened my stop accuracy considerably. The cost of running embedding similarity on 1-3 short issue strings is negligible compared to the LLM calls themselves, so there is no reason not to use it.
Token budget enforcement should be implemented at the loop level, not just at the model level. Per-call token caps don't help — you can still iterate fifty times. Track the cumulative spend against a budget assigned at task start. I default to four times the cost of a single Generator call as the per-task ceiling. That permits at most three full iterations of the split pattern, plus some margin, which matches the iteration cap and gives you a defensible upper bound on cost per task.
A subtle but important rule: log the stop reason. When a task fails to converge, you want to know whether it failed because of the iteration cap, the token cap, or stagnation detection. Each represents a different kind of failure and a different intervention. The iteration cap suggests the task is genuinely hard. The token cap suggests the prompts are too verbose or the model is rambling. Stagnation suggests a structural blind spot in the model's understanding of the task. Without that distinction in your logs, you cannot improve the loop over time.
A measurement harness for the loop
Adding a self-critique loop is not a "set it and forget it" change. You need to measure whether quality actually improved, otherwise you've only added cost. I track three numbers in an Antigravity-integrated evaluation harness:
Human approval rate: percentage of outputs merged without edits
Average iterations: how many rounds the loop takes to converge
Cost per approval: tokens spent per human-approved output
Approval rate alone hides cost. Cost alone hides quality. Cost-per-approval is the right north star — it forces both axes to improve together. In my case, approval rate moved from 62% to 87% after introducing the loop, while cost-per-approval rose by 1.3x. Whether that trade is acceptable depends on your business, but at least the impact becomes legible in numbers.
Beyond those three primary metrics, I also track two secondary signals: the distribution of stop reasons (broken down by iteration cap, token cap, and stagnation) and the gap between average iterations for approved outputs versus rejected outputs. If approved outputs converge faster than rejected ones — which is the typical pattern — that's good evidence the loop is doing real work. If they converge at the same rate, you might be running a critic that approves without actually evaluating, which is a known failure mode I've personally hit and which can take weeks to detect.
There is also a qualitative metric I run informally: I read 10 random rejected outputs each week and ask myself whether the rejection was correct. If the loop is rejecting outputs I would have approved, the critic's bar is too high. If the loop is approving outputs I would have rejected, the critic's bar is too low. This is more reliable than any automated metric for catching critic drift, but it requires discipline to actually do every week.
For deeper context on the evaluation harness, the production-quality evaluation framework guide and the completion verification design notes pair well with this article.
Common pitfalls I've hit in production
Beyond the design choices above, there are concrete failure modes that are easy to miss until they bite you. I'll list the ones that have cost me the most time, in rough order of pain.
The first is critic prompt drift over time. When you first write the critic prompt, it embodies a clear quality bar. Six months later, after a dozen small edits to handle edge cases, the critic has become a long, contradictory document that asks for things the generator cannot reasonably satisfy. I now version-control critic prompts the same way I version-control code, with a changelog entry for every modification and a quarterly review where I delete clauses that are no longer needed. Treating the critic as living infrastructure rather than a one-time artifact keeps it sharp.
The second is feedback contamination across tasks. If you cache previous critic feedback (which is tempting for cost reasons), you can accidentally apply feedback intended for one task to a completely unrelated one. I once had a critic that started rejecting marketing-tone outputs in a code-comment generator because cached feedback from a release-notes task had leaked into the prompt context. Always scope feedback strictly to the task that produced it, and never carry feedback across task boundaries unless you have an explicit cross-task lesson worth promoting to a system-level rule.
The third is silent critic agreement. The critic and generator might both share the same blind spot — they're often the same underlying model, after all — and reach a comfortable consensus that satisfies both prompts but fails to satisfy the actual requirement. The split pattern is supposed to mitigate this, but the mitigation is partial. The defense I use is to occasionally swap the critic to a different model family for a sample of outputs, comparing rejection rates. If the cross-model critic rejects significantly more often than your default critic, your default critic is likely too aligned with the generator's biases.
The fourth is iteration-induced regression. Sometimes iteration two is worse than iteration one. The critic asked for more detail, the generator added detail, but the addition broke the structure or introduced an inaccuracy. I now keep all iterations in memory and, at the end of the loop, optionally select the iteration the critic rated highest rather than always taking the latest. This protects against the case where the loop wandered into a worse state.
The fifth is over-fitting the critic to easy cases. If you tune the critic's quality bar by looking only at outputs that converged successfully, you'll miss the systematic ways your loop fails on hard cases. I deliberately review escalations — the outputs that hit the iteration cap or token cap — separately from the converged ones, because they reveal the failure modes the loop cannot fix on its own.
Combining patterns: when to layer
The four patterns are not mutually exclusive. In production I almost never run any single pattern in isolation; I layer them based on the cost and risk profile of the task.
For low-stakes, high-volume tasks (auto-generated commit messages, internal log summaries), I run Pattern 4 alone. The confidence gate keeps critic invocation rare, and the rare critic invocation runs Pattern 2's single-agent reflection because it's cheaper than splitting.
For medium-stakes outputs (release notes, status reports, customer-facing email drafts), I run Pattern 4 as the gate, and route the critic to Pattern 1 — split generator-critic, with the explicit feedback loop. This catches more failures than reflection alone but stays within an iteration cap of two or three.
For high-stakes outputs (public documentation, blog posts, anything that represents the brand), I bypass the gate entirely and run Pattern 3 from the start. Three critics, parallel, with explicit priority. The cost is significant — easily five to seven times the single-call baseline — but the failure cost is also significant.
The judgment call is not "which pattern is best" but "what is the cost of a slipped bug in this specific output type, and how much of the loop can I afford to skip on cheap outputs." Once you frame it that way, the layering becomes obvious.
Designing agents that doubt themselves wisely
Trusting an LLM's first answer is rarely the right move. The technical answer is a self-critique loop, but the deeper move is closer to "let an editor live inside the agent." Skilled writers all share the habit of distrusting their own first drafts. Agents need that same habit to break through the ceiling that one-shot generation imposes.
But reflection isn't infinitely good. Add too many critics and decision authority blurs. Add too many iterations and the prose thins out. Deciding where to stop doubting and start trusting is, in the end, the design choice that gives the agent its character. Agents that doubt too little are confidently wrong; agents that doubt too much are paralyzed and bland. Neither extreme is what users want — they want an agent that submits work it stands behind, having earned that confidence through structured re-examination.
The next concrete step: pull 50 outputs from your current agent's logs and manually classify each one as "a critic would have caught this" versus "a critic would have missed this." That single exercise will tell you which of these four patterns to start with — shaped by the specific failure modes of your project rather than a generic recommendation. The 50-output sample is small enough to do in an afternoon and large enough to expose the pattern you actually need.
I hope this helps you build agents that ship work you genuinely trust. Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.