GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/Advanced
Advanced/2026-04-25Advanced

Building Self-Critiquing Agents with Gemini API: A Production-Ready Guide to Reflection and Critic-Refiner Patterns

A production-grade walkthrough of Reflection and Critic-Refiner patterns with Gemini 3 Pro and 2.5 Flash. Covers implementation, cost guards, over-correction defenses, and monitoring signals from real deployments.

gemini102gemini-3-pro4reflection2agent10production140self-improving

Premium Article

If you've run a Gemini API workload long enough, you've probably hit the moment when single-shot quality plateaus. You tuned the prompt, dropped the temperature, added few-shot examples—and still about 10% of responses come back with hallucinated facts, broken formatting, or instruction violations. I ran into the same wall when I shipped a customer-email drafting feature in one of my apps.

The most practical way past that wall is the Reflection pattern, and its more refined cousin, the Critic-Refiner pattern. The idea is straightforward: have the LLM critique its own output, then rewrite it. Both patterns have been validated for years in research and in production. The catch is that naively bolting them onto a real workload introduces a new set of headaches—infinite loops, 3x cost spikes, and the dreaded "over-correction" failure mode where the model rewrites the parts that were already fine.

This guide walks through how I actually wire up self-critiquing agents in production using Gemini 3 Pro and 2.5 Flash. You'll get three runnable code examples, the cost guards I had to learn the hard way, and the monitoring signals that flag when your reflection setup has stopped earning its keep.

Where one-shot inference hits its ceiling

A one-shot LLM call—prompt in, response out—gets you 90% of the way for most tasks. The issue is the remaining 10%, where the failure modes are expensive enough that you can't ignore them.

Concrete examples I've seen:

  • A customer-document generator that occasionally misspells the recipient's name or omits a required field
  • A code-review agent that flags obvious vulnerabilities but misses dependency-breaking changes
  • A product description generator that lets a forbidden competitor brand slip through, despite explicit "do not mention" instructions

Trying to crush all of those with a bigger one-shot prompt usually backfires. I've watched Gemini 2.5 Pro and 3.1 Pro both start losing constraints once the prompt grows past about 30 stated rules—the model picks a subset to honor and quietly drops the rest.

The mental shift is to stop asking a single call to satisfy every constraint. Split the work into "draft → self-review → revise." That's where Reflection enters.

Reflection pattern: start with the minimum viable loop

The shape of it

The basic Reflection pattern is three steps:

  1. User request → Model A produces a first draft
  2. First draft → Model A (same model) produces a structured review listing instruction violations, factual errors, and formatting issues
  3. First draft + review → Model A produces a revised draft

Three calls instead of one. In my own A/B tests, this lifts perceived quality by roughly 20% without changing prompts much. The key detail is that the review must be a structured object, not free-form text. Vague feedback turns into vague edits, which turn into the over-correction failure mode where the revision step rewrites paragraphs that were already fine.

Code: a single-model Reflection loop with Gemini 3 Pro

What this code does: takes a task and a list of constraints, runs the three-step Reflection loop, and returns the final text.

# requirements: google-genai>=0.5.0, pydantic>=2.0
import os
from google import genai
from google.genai import types
from pydantic import BaseModel
from typing import List
 
client = genai.Client(api_key=os.environ["YOUR_GEMINI_API_KEY"])
MODEL = "gemini-3-pro"
 
class Issue(BaseModel):
    """A single problem found by the reviewer."""
    category: str  # e.g., "instruction_violation" / "factual_error" / "formatting" / "forbidden_term"
    snippet: str   # the offending span (max 80 chars)
    suggestion: str  # how to fix it
 
class Review(BaseModel):
    issues: List[Issue]
    needs_revision: bool
 
def generate_first_draft(task: str, constraints: List[str]) -> str:
    """Produce the initial draft."""
    prompt = (
        f"Write text that satisfies the following requirements.\n\nTask:\n{task}\n\n"
        f"Constraints:\n" + "\n".join(f"- {c}" for c in constraints)
    )
    resp = client.models.generate_content(
        model=MODEL,
        contents=prompt,
        config=types.GenerateContentConfig(temperature=0.4),
    )
    return resp.text
 
def review_draft(draft: str, constraints: List[str]) -> Review:
    """Review the draft and return a structured list of issues."""
    prompt = (
        "Review the following text against the listed constraints.\n"
        "Only list real problems—ignore stylistic preferences.\n"
        "If you find no issues, set needs_revision to false.\n\n"
        f"Constraints:\n" + "\n".join(f"- {c}" for c in constraints) +
        f"\n\nText:\n{draft}"
    )
    try:
        resp = client.models.generate_content(
            model=MODEL,
            contents=prompt,
            config=types.GenerateContentConfig(
                temperature=0.0,
                response_mime_type="application/json",
                response_schema=Review,
            ),
        )
        return Review.model_validate_json(resp.text)
    except Exception as e:
        # Fail safe: if JSON parsing breaks, treat as "no revision needed"
        print(f"[review_draft] fallback: {e}")
        return Review(issues=[], needs_revision=False)
 
def revise_draft(draft: str, review: Review) -> str:
    """Apply only the listed fixes and leave everything else alone."""
    issues_text = "\n".join(
        f"- [{i.category}] {i.snippet} -> {i.suggestion}" for i in review.issues
    )
    prompt = (
        "Rewrite the following text by addressing ONLY the listed issues.\n"
        "Do not change any part that was not flagged.\n\n"
        f"Fixes to apply:\n{issues_text}\n\nOriginal text:\n{draft}"
    )
    resp = client.models.generate_content(
        model=MODEL,
        contents=prompt,
        config=types.GenerateContentConfig(temperature=0.2),
    )
    return resp.text
 
def reflect_once(task: str, constraints: List[str]) -> str:
    """Run a single Reflection cycle and return the final text."""
    draft = generate_first_draft(task, constraints)
    review = review_draft(draft, constraints)
    if not review.needs_revision:
        return draft
    return revise_draft(draft, review)
 
# Expected output: a revised draft that respects the constraints more strictly than the first draft.
if __name__ == "__main__":
    final = reflect_once(
        task="Draft a 200-character invitation email for a new SaaS product.",
        constraints=[
            "Stay strictly under 200 characters",
            "Address the recipient as 'Mr. Hirokawa'",
            "Never mention the competitor 'Acme Corp'",
            "Use a warm, professional tone throughout",
        ],
    )
    print(final)

Why this shape: the review is a Pydantic model, not a paragraph of prose, and the revise step is told to touch only the flagged spans. If you let the revise step "rewrite the whole thing in light of the review," it will. That's where over-correction comes from. Constraining the revision target is the silent safety belt of the entire pattern.

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
If your one-shot prompts have plateaued, you'll walk away with working Reflection loops and a clear rule for when to split into Critic and Refiner
You'll learn how to combine Gemini 3 Pro and 2.5 Flash to lift quality while keeping the cost increase inside 1.5–2x, plus the monitoring metrics that prove it's still working
You'll get the guard rails that stop self-critique loops from runaway costs, over-correction, and Critic bias—and the production signals that reveal when reflection has stopped paying off
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.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini 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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Advanced2026-07-11
A Risk-Tiered Approval Gate for Gemini Function Calling
Handing full autonomy to an agent is unnerving. This walks through a Gemini function-calling loop that routes tool calls into auto-run and hold-for-approval by risk tier, then feeds the result back to the model after a human signs off.
Advanced2026-06-27
Your Gemini Completion Event Will Arrive Twice — An Idempotent Sink That Makes Webhook + Reconciliation Effectively Once-Only
Once you receive Gemini long-running operations over a Webhook and back it up with a reconciliation poller, the same completion arrives twice and publishing or billing runs twice. Build an idempotent sink with a normalized key and a claim-run-commit pattern that keeps side effects effectively once-only.
Advanced2026-06-18
Restarting a Long Agent Run From Where It Broke — A Step-Ledger Design for Gemini 3.5 Flash Long-Horizon Tasks
Gemini 3.5 Flash is good at long-horizon tasks, but when a 40-step run dies on step 29, you usually start over. An append-only step ledger gives you resume, idempotency, and audit in one place. Here is the design with working Python and measured results.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →