●CLASSROOM — Starting today, August 10, Gemini in Classroom opens on the web to students of all ages. Mobile follows on August 17●STUDY — Students can pick course materials and have Gemini build flashcards or practice quizzes tailored to that specific class●PROMPT — The old starter prompts become contextualized experiences, so there is less bouncing between Classroom and Gemini to get relevant help●SUNSET — Image generation models shut down on August 17, seven days out. The Grok 4.1 family follows on the 20th and gemini-robotics-er-1.6-preview on the 31st●FLASH — gemini-3.6-flash, GA since July 21, improves token efficiency and agentic planning at a lower price point than 3.5 Flash●PARAMS — The sampling parameters temperature, top_p, and top_k are now deprecated. It is a good moment to revisit existing calls●CLASSROOM — Starting today, August 10, Gemini in Classroom opens on the web to students of all ages. Mobile follows on August 17●STUDY — Students can pick course materials and have Gemini build flashcards or practice quizzes tailored to that specific class●PROMPT — The old starter prompts become contextualized experiences, so there is less bouncing between Classroom and Gemini to get relevant help●SUNSET — Image generation models shut down on August 17, seven days out. The Grok 4.1 family follows on the 20th and gemini-robotics-er-1.6-preview on the 31st●FLASH — gemini-3.6-flash, GA since July 21, improves token efficiency and agentic planning at a lower price point than 3.5 Flash●PARAMS — The sampling parameters temperature, top_p, and top_k are now deprecated. It is a good moment to revisit existing calls
Finding the questions your help docs never answer, by asking Gemini to write the quiz
When support keeps asking something your help page already covers, generate questions from that page and check whether the page alone can answer them. A two-pass audit pipeline with call design and cost math.
On the days I clear App Store and Google Play review replies, I write 30 to 40 of them back to back. The App Store makes you wait about eight seconds between submissions, so forty replies means over five minutes of nothing but waiting. In those five minutes I read the same complaint two or three times.
For the wallpaper apps, the recurring one is some version of "I saved it and now I can't find it." The steps are in the help center. I wrote them. And the next month the same sentence arrives again.
It does not arrive because the answer is missing. It arrives because the answer is unreachable. I wanted a way to tell those two apart with something other than my gut.
"Is it documented" and "can it be found" are different questions
Documentation audits usually turn into coverage exercises: this feature has no page, that setting is never explained. You fill the gaps and move on.
What actually hurts an indie developer is not the gap, it is the dead end. Help headings get written in the developer's vocabulary. I labeled a section "Changing the download location." Users search for "can't save." When the words do not overlap, that page might as well not exist.
So the question worth auditing is not "did I write this down." It is "given how a user would phrase it, can this document alone answer them." That is far too many permutations to enumerate by hand, and exactly the kind of work a model is good at.
As of August 10, 2026, Gemini in Classroom widened its rollout so students can pick their course material and get practice quizzes and flashcards generated from it. That mechanic — hand over a document, get back the questions it supports — transfers directly. It looks like an education feature. Underneath, it is a device that enumerates the questions a document can reach.
Generate questions from the document, then answer them using only that document
The design is two passes.
Generation pass: feed the help text, get back the questions a user would plausibly send
Judgment pass: for each question, attempt an answer grounded only in that same help text, and return a three-way verdict
The verdicts are answered, partial, and missing. A partial means the evidence exists but is split across sections, so no single spot closes the question. Whether you keep that middle state changes what you do next more than anything else in the pipeline.
The critical constraint: do not merge generation and judgment into one call. Ask a model to "write questions and answer them" and it will only write questions it can already answer. The whole point is to find the ones it cannot, so the passes have to be separate, with the judging call kept ignorant of where the questions came from.
✦
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 surface the holes in your help center yourself, before they turn into repeat support tickets
✦You will be able to separate 'never written down' from 'written down but unreachable', and fix them in the right order
✦You will be able to size the call pattern that keeps a 40-document audit near $0.64 per run against your own doc volume
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.
Google's Python SDK plus structured output, with a fixed schema so the downstream stage cannot silently break.
# pip install google-genai pydanticfrom google import genaifrom pydantic import BaseModelfrom typing import Listclient = genai.Client(api_key="YOUR_API_KEY")MODEL = "gemini-3.6-flash"class Question(BaseModel): question_id: str # stable key for matching against the judgment pass text: str # phrased the way a user would phrase it user_words: List[str] # the user-side vocabulary it containsclass QuestionSet(BaseModel): questions: List[Question]GEN_PROMPT = """You are a first-time user of this app.Read the help text below and write 8 questions you would realistically send to support.Constraints:- Do not reuse the headings from the help text. Rephrase in everyday language- At least half should start from a symptom: "won't", "disappeared", "can't find"- Include questions the text probably does not answer, if any come to mind- Number question_id from q01 through q08Help text:---{doc}---"""def generate_questions(doc_text: str) -> QuestionSet: res = client.models.generate_content( model=MODEL, contents=GEN_PROMPT.format(doc=doc_text), config={ "response_mime_type": "application/json", "response_schema": QuestionSet, }, ) return QuestionSet.model_validate_json(res.text)# Expected output (excerpt):# questions=[Question(question_id='q01',# text="I saved a wallpaper but it isn't in my photos",# user_words=['saved', 'not there', 'photos']), ...]
The line that earns its keep is "do not reuse the headings." Drop it and the generated questions become paraphrased table-of-contents entries, the judgment pass returns answered across the board, and you learn nothing.
Seeding the prompt with a handful of real review texts helps too. The vocabulary users actually reach for is blunter than anything you would invent for them.
Implementing the judgment pass
The judge sees the questions and the document, nothing else. Outside knowledge is explicitly forbidden and a verbatim quote is mandatory for any positive verdict. Requiring the quote noticeably cuts down on the model drifting toward a comfortable answered.
class Verdict(BaseModel): question_id: str verdict: str # "answered" | "partial" | "missing" quote: str # verbatim span from the document, empty if none reason: str # under 20 wordsclass VerdictSet(BaseModel): verdicts: List[Verdict]JUDGE_PROMPT = """Using only the document below as evidence, decide whether each question can be answered.Verdicts:- answered: one passage in the document settles it- partial: evidence exists but requires combining separate passages- missing: the document contains no basis for an answerRules:- Never use knowledge from outside the document- For answered and partial, put a verbatim span from the document in quote- Always echo back the same question_idDocument:---{doc}---Questions:{questions}"""def judge(doc_text: str, qs: QuestionSet) -> VerdictSet: listed = "\n".join(f"{q.question_id}: {q.text}" for q in qs.questions) res = client.models.generate_content( model=MODEL, contents=JUDGE_PROMPT.format(doc=doc_text, questions=listed), config={ "response_mime_type": "application/json", "response_schema": VerdictSet, "system_instruction": "You are a document reviewer. Never fill in what the document does not say.", }, ) vs = VerdictSet.model_validate_json(res.text) # Detect dropped items. A short response goes back for a retry got = {v.question_id for v in vs.verdicts} missing_ids = [q.question_id for q in qs.questions if q.question_id not in got] if missing_ids: raise ValueError(f"verdict missing for: {missing_ids}") return vs
Writing the replies taught me the shortfall was vocabulary, not explanation
The idea came out of the reply work itself.
For the recurring question my first read was "the explanation must be too thin," so I added paragraphs to the help page. The tickets kept coming. The next month, the same phrasing landed again.
Writing replies one at a time, the reason surfaces slowly. The words a user reaches for and the words in my headings simply do not overlap. I wrote "save location." They write "where did it go." Adding explanation without adding a path to that explanation changes nothing.
Which is why the value of this device sits in the partial results more than the missing ones. A partial means the evidence is there but no single stop delivers it — a defect you fix by renaming a heading and consolidating two sections, not by writing more. Less work, faster payoff.
I prefer running the audit right after a batch of review replies rather than before a feature launch. The user vocabulary is still fresh, which makes the generated questions much easier to judge for realism.
Judging one question at a time costs 7x the input tokens
How you batch the calls moves the bill visibly. Assume 40 help documents, roughly 1,800 characters of body text each, 8 questions per document. Japanese runs close to one token per character, and the rates used here are Gemini 3.6 Flash at $1.50 per 1M input tokens and $7.50 per 1M output tokens.
Judgment layout
Input tokens
Output tokens
Approx. cost per run
Send the document once per question
716,800
51,200
~$1.46
Send 8 questions of one document together
170,800
51,200
~$0.64
The gap is about 7.0x, entirely on the input side. In the judgment pass the document dominates, and sending it per question means paying for the same body text eight times over. Batched, it goes across once.
Batching has a cost of its own. Later questions in a batch tend to drift toward whatever the earlier ones were judged, so verdicts clump. Two guards keep that in check:
Batch only within a single document. Never pack multiple documents into one call
Require question_id on every verdict and reconcile by ID, never by position
If your setup genuinely has to resend the same body text repeatedly, context caching is the cleaner answer. I laid out how to decide in cutting Gemini API costs with context caching.
Keep a ledger and only re-run what changed
A one-off audit is not worth much. The loop closes when you edit the page and confirm the same question has moved to answered.
import hashlibimport jsonfrom pathlib import PathLEDGER = Path("docs_answerability.jsonl")def doc_fingerprint(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]def load_ledger() -> dict: if not LEDGER.exists(): return {} rows = {} for line in LEDGER.read_text(encoding="utf-8").splitlines(): if line.strip(): row = json.loads(line) rows[row["doc"]] = row return rowsdef run(doc_paths): ledger = load_ledger() out = [] for path in doc_paths: text = Path(path).read_text(encoding="utf-8") fp = doc_fingerprint(text) prev = ledger.get(path) if prev and prev["fingerprint"] == fp: continue # unchanged documents stay off the bill qs = generate_questions(text) vs = judge(text, qs) gaps = [v for v in vs.verdicts if v.verdict != "answered"] out.append({ "doc": path, "fingerprint": fp, "total": len(vs.verdicts), "gaps": [ {"id": v.question_id, "verdict": v.verdict, "reason": v.reason} for v in gaps ], "prev_gap_count": len(prev["gaps"]) if prev else None, }) with LEDGER.open("a", encoding="utf-8") as f: for row in out: f.write(json.dumps(row, ensure_ascii=False) + "\n") return out# Expected output (excerpt):# [{'doc': 'help/en/save-wallpaper.md', 'fingerprint': '9f1c...', 'total': 8,# 'gaps': [{'id': 'q03', 'verdict': 'partial', 'reason': 'save path lives in another section'}],# 'prev_gap_count': 3}]
Carrying prev_gap_count makes it obvious when a page you thought you fixed still has the same number of holes. In my own rotation about five documents change in a month, which under the batched layout works out to roughly $0.08 monthly. Cost is not the reason to skip this.
Drop the fingerprint check and you pay full price on unchanged documents every run. That is not a line worth cutting.
Where to stop handing work to the model
All this device returns is the fact that a question went unanswered. Deciding what to rewrite stays with you. My split:
Model's job: generating the questions, judging reachability, aggregating gaps per document, diffing against last time
My job: renaming headings, merging sections, and deciding whether the product itself is what needs fixing
A cluster of missing verdicts is also worth pausing on before you start writing. An operation that only the help page can rescue usually points at a rough edge in the app. For an indie developer, prose looks like the cheap fix, and then you pay for it monthly in review replies.
Start with a single help page
There is no need to point this at everything at once. Take the page behind your most frequent ticket, generate the eight questions, and read them yourself. If they do not read like sentences a real user would type, the constraints in your generation prompt are too loose — and tightening those is the first real piece of work.
I am still undecided about the right granularity for the audit. If you run something similar, I would genuinely like to hear how you handle it. Thank you for reading this far.
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.