●0918 — The Gemini API changelog has no entries after September 18. For new projects the recommendation is still 3.5 Flash-Lite or 3.8 Flash●09/30 — gemini-omni-flash-preview shuts down on September 30, six days away. Its successor is gemini-omni-1.1-flash●ENTCLI — An open question asks why Gemini 3.5 and 3.6 Flash have not reached Gemini CLI on Enterprise accounts, and the gap with personal accounts is still unexplained●NEW — The day I first removed a connected MCP server, and how I now pick the tools that stay on●CONFIG — Since Antigravity 2.17.0, repository settings load from .gemini/config.json, the same .gemini/ folder Gemini CLI already uses●PICKER — When a familiar model disappears, check what your own model picker actually lists first. It keeps an access limit from being mistaken for an outage●0918 — The Gemini API changelog has no entries after September 18. For new projects the recommendation is still 3.5 Flash-Lite or 3.8 Flash●09/30 — gemini-omni-flash-preview shuts down on September 30, six days away. Its successor is gemini-omni-1.1-flash●ENTCLI — An open question asks why Gemini 3.5 and 3.6 Flash have not reached Gemini CLI on Enterprise accounts, and the gap with personal accounts is still unexplained●NEW — The day I first removed a connected MCP server, and how I now pick the tools that stay on●CONFIG — Since Antigravity 2.17.0, repository settings load from .gemini/config.json, the same .gemini/ folder Gemini CLI already uses●PICKER — When a familiar model disappears, check what your own model picker actually lists first. It keeps an access limit from being mistaken for an outage
I Stopped Asking Gemini 'Is This Right?' and Started Hiding My Answer Instead
Ask Gemini to confirm a label you already chose and it tends to agree. Here is why I compared three ways of asking, chose blind re-classification, how my answer still leaked in, and the routing code that sends only disagreements to a human.
The night I finished tagging a new batch of ukiyo-e wallpapers by subject, I asked Gemini to double-check my work. I attached each image and asked, "Is this a landscape print?" Almost every reply came back as a polite yes with a plausible reason.
Then I noticed one print I had tagged by mistake: a close-up actor portrait still carrying the landscape tag. Gemini had agreed with that one too, pointing to the painted scenery behind the actor.
I thought I was getting a second opinion. I was getting my own opinion read back to me. This is the record of how I changed the question.
The confirmation question was handing over the answer
Looking back, the problem started in my prompt, not in the model. "Is this a landscape print?" already contains the answer. The model reads the image starting from that word and goes looking for evidence that fits.
I don't think this is unique to Gemini. The gemini-cli repository has a long-running issue (#4556) about responses leaning too far toward agreement, and the comments are split between "fix it with instructions" and "change how you ask."
The line I drew is simple:
When you ask for a judgment, don't let your own answer into the room.
Instead of showing my label and asking whether it's right, I ask the model to solve the task from scratch, and I compare the two answers myself.
Three ways to ask, side by side
Before rebuilding anything, I tried three prompt shapes on the same batch. As an indie developer running a small pipeline, the evaluation was my own eyes, nothing fancier.
Approach
Prompt shape
What happened
Where it fits
Confirm
"Is this X?"
Near-universal agreement, including on wrong tags
Reviewing prose where there is no single right answer (see below)
Object
"Find what's wrong with this tag"
Invents objections to correct tags; the bias just flips direction
Pure hunting for oversights
Blind
Give only the options, compare with my tag locally
Agreement and disagreement separate cleanly; slightly longer output
Classification with a fixed set of options and one right answer
The surprise was the "object" approach. Once finding a flaw becomes the assignment, the model finds one, even on correct tags ("if you weight the background, it could also be a landscape"). I had traded agreement bias for disagreement bias.
The blind approach costs a little more output per call because the model classifies from scratch. But the items I need to look at shrink to the disagreements, so the total effort went down. I kept blind classification for tagging and saved the confirmation style for something else.
✦
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
✦You will be able to have Gemini re-classify your data without ever seeing your labels, and review only the items where the two answers disagree
✦You will be able to close the paths through which your answer leaks back in (file names, option order, a helpful preamble) before they quietly inflate your agreement rate
✦You will be able to choose between asking for confirmation, asking for objections, and hiding your answer, based on the kind of task in front of you
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.
Here is the skeleton of what replaced the old code, using Python and the google-genai SDK. The goal is a single one: the model picks from the options without seeing a single character of my tag.
import hashlibimport ioimport jsonimport randomfrom pathlib import Pathfrom google import genaifrom google.genai import typesfrom PIL import Image# Which models a new project can use varies by account; check models.list firstMODEL = "gemini-3.8-flash"CATEGORIES = ["名所絵", "美人画", "役者絵", "花鳥画", "武者絵", "戯画", "その他"]client = genai.Client() # reads GEMINI_API_KEY from the environmentdef build_schema(categories: list[str]) -> types.Schema: """Observe -> choose -> runner-up -> confidence. The order is part of the design.""" return types.Schema( type=types.Type.OBJECT, properties={ "observations": types.Schema( type=types.Type.STRING, description="What is visible (figures, background, props, text). No category names.", ), "category": types.Schema(type=types.Type.STRING, enum=categories), "runner_up": types.Schema(type=types.Type.STRING, enum=categories), "confidence": types.Schema(type=types.Type.NUMBER), }, required=["observations", "category", "runner_up", "confidence"], property_ordering=["observations", "category", "runner_up", "confidence"], )def blind_bytes(path: Path) -> tuple[str, bytes]: """Re-encode to drop metadata, and track by hash instead of file name.""" img = Image.open(path).convert("RGB") buf = io.BytesIO() img.save(buf, format="JPEG", quality=90) # EXIF and IPTC captions are dropped here data = buf.getvalue() return hashlib.sha256(data).hexdigest()[:12], datadef classify_blind(path: Path, seed: int) -> dict: token, data = blind_bytes(path) cats = CATEGORIES[:] random.Random(seed).shuffle(cats) # spread out any pull toward the first option prompt = ( "この浮世絵の主題を、次の候補から一つだけ選んでください。" f"候補: {'、'.join(cats)}。" "先に画面に見えるものを書き、そのあとで選んでください。" ) resp = client.models.generate_content( model=MODEL, contents=[types.Part.from_bytes(data=data, mime_type="image/jpeg"), prompt], config=types.GenerateContentConfig( response_mime_type="application/json", response_schema=build_schema(cats), temperature=0.2, ), ) result = json.loads(resp.text) result["token"] = token result["model_version"] = resp.model_version # keep the model that actually answered return result
(The categories and prompt stay in Japanese because that's how my catalog is labeled; swap in your own.)
Three notes on why it's written this way.
First, property_ordering. Gemini's structured output writes properties in the order the schema specifies. Putting observations before category means the model describes what it sees before choosing, rather than choosing and then justifying.
Second, shuffling the options. If the order never changes, any tendency to pick near the top when unsure leaks straight into the results. A different seed per call spreads that tendency evenly across the options.
Third, blind_bytes, which leads into the next section.
My answer was still leaking in, three ways
The first night on the blind setup, the agreement rate looked suspiciously high. Before celebrating, I re-read the inputs and found my tags reaching the model through three routes:
File names. For traceability I had appended ファイル: meisho_0412.jpg to the prompt. The prefix is the answer.
Image metadata. During restoration work I had left subject notes in the caption field of some files. Until I re-encoded before sending, I had no idea they were going along for the ride.
A friendly preamble. One line saying "this batch is mostly landscapes" nudged every borderline print toward landscape.
None of these mattered while I was asking for confirmation. If you adopt the blind setup, I'd close these three gaps before anything else. Hiding the answer comes with a new responsibility: checking, on the input side, where the hidden answer might sneak back in.
I put that check in one function that runs right before sending:
import redef assert_no_leak(prompt: str, my_label: str, categories: list[str]) -> None: """Make sure neither my tag nor file-name hints appear outside the option list.""" head = prompt.split("候補:")[0] if my_label in head: raise ValueError(f"My own tag appears before the option list: {my_label}") if re.search(r"[A-Za-z_]+_\d+\.(jpe?g|png)", prompt): raise ValueError("Something that looks like a file name is in the prompt") for word in ("多め", "ほとんど", "大半"): # "mostly", "nearly all", "the majority" if word in head: raise ValueError(f"A batch-level hint is still in the preamble: {word}") if sorted(categories) != sorted(CATEGORIES): raise ValueError("The option set does not match the source of truth")
Call it once in classify_blind, right after building prompt. A loud exception is far easier to live with than a quietly inflated agreement rate.
Only disagreements go to a human
Now I compare the blind answer with my tag locally. One thing I learned: don't lean on the model's self-reported confidence. The numbers look reasonable, but wrong answers sometimes came with high values too.
I rely on two other signals instead: whether my tag shows up as the runner-up, and whether the answer changes when I ask twice with the options in a different order.
import csvfrom dataclasses import asdict, dataclass@dataclassclass Verdict: token: str mine: str blind: str runner_up: str stable: bool route: strdef decide_route(mine: str, a: dict, b: dict) -> Verdict: stable = a["category"] == b["category"] if a["category"] != mine and a["runner_up"] != mine: route = "human" # my tag isn't in the top two; suspect my side first elif a["category"] != mine: route = "boundary" # my tag is the runner-up; a print that spans two subjects elif not stable: route = "sample" # agreed, but the answer wobbles when the order changes else: route = "pass" return Verdict(a["token"], mine, a["category"], a["runner_up"], stable, route)def review_batch(items: list[tuple[Path, str]], out_tsv: Path) -> None: with out_tsv.open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=list(Verdict.__dataclass_fields__), delimiter="\t") writer.writeheader() for path, mine in items: first = classify_blind(path, seed=1) # if the first pass already disagrees, a human looks anyway; only re-ask on agreement second = classify_blind(path, seed=2) if first["category"] == mine else first writer.writerow(asdict(decide_route(mine, first, second)))
The second call only runs when the first one agrees with me. In production that keeps the number of calls from simply doubling: a disagreement goes to a human regardless, so checking for wobble only matters when the two answers match.
I no longer open rows marked pass. For human, I put the images side by side and look again. For boundary, I consider whether the print deserves two subject tags. For sample, I spot-check a few per batch.
Fewer images to open helped, but what helped more was that every row says why I'm opening it. Knowing the reason makes the decision faster.
One thing the documentation couldn't have told me: most of the rows that landed in human were my mistakes, not the model's. While I was asking for confirmation, those mistakes were being agreed with right along with everything else.
Where I still ask for confirmation
Blind classification suits tasks with a fixed set of options and one right answer. It doesn't help when the answer has no fixed shape. When I translate a store description into English, there is no single correct translation, so a blind re-translation gives me nothing to compare against.
For copy review, I kept the confirmation style, with two changes:
REVIEW_PROMPT = """Read the following English description as someone seeing this app for the first time.List at most three places a reader might misunderstand.If there are none, answer only "none".Do not list strengths or give overall impressions.---{text}"""def review_copy(text: str) -> str: resp = client.models.generate_content( model=MODEL, contents=REVIEW_PROMPT.format(text=text), config=types.GenerateContentConfig(temperature=0.2), ) return resp.text.strip()
I don't ask "is this right?" I ask for at most three likely misunderstandings, and I give explicit permission to say "none." Without that permission, the model tries to fill all three slots and we're back to invented objections. A ceiling plus an exit keeps it from leaning too far either way.
I don't see these as rivals. Hide the answer when there's one right answer; give a ceiling and an exit when there isn't. That's the split I work with now.
Run review_batch without passing your labels anywhere into the prompt, and confirm assert_no_leak never fires.
In the resulting TSV, open only the rows where route is human.
Whether those turn out to be the model's errors or your own, seeing the split once tends to change how you ask for a second opinion from then on. Twenty items is where I started, too.
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.