"Gemini 2.5 Pro has a massive context window, so you can just drop the whole document in." That is half right. Once you are working with contracts or technical specifications over 100 pages, dumping everything into a single prompt surfaces problems — missed clauses, hallucinations, citations that do not match the text. The model needs more design than that for real business work.
This article collects the five techniques I found most valuable from extensive trial-and-error on long-document analysis, along with the code and accuracy observations that made each one earn its keep.
The real problem: a 200-page document is not a "summarize" problem
Long-document use cases in business almost always fall into these three categories:
- Condition extraction: pull termination clauses, pricing clauses, penalty rules out of a contract
- Consistency checking: detect contradictions inside a single document
- Change detection: identify substantive changes between an old and new version
None of these is summarization. Each depends on the model being able to point precisely at where in the document a finding comes from. Treating Gemini 2.5 Pro as "just a long context" does not deliver that pointing ability reliably.
Technique 1: Pre-segmentation — combine full-document and segmented prompts
The first high-impact technique is pre-splitting the document along structural boundaries before any prompting. Not fixed-length chunks — meaningful blocks: chapters, sections, articles. For contracts this means per-article; for meeting minutes, per agenda item; for specs, per heading.
I then run a two-pass pipeline:
Pass 1 — full-document scan. Feed the whole document and have Gemini 2.5 Pro build a short index: "which sections likely contain material for each of my target questions?" The output is intentionally short. The point is to force the model to form a global view.
Pass 2 — targeted inspection. Using the pass-1 index, extract only the relevant sections and prompt the model again, narrowing its attention explicitly.
On the 180-page contract I tested this on, single-pass extraction found 7 penalty-related clauses. Two-pass extraction found 11. Same model, same temperature, four clauses recovered from the "misses" of the single-pass approach.
Python skeleton:
from google import genai
from google.genai import types
client = genai.Client()
def pass1_table_of_contents(full_text: str) -> dict:
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[
"For the contract below, list section numbers relevant to these topics:",
"Topics: 1) termination 2) pricing changes 3) penalties 4) confidentiality",
"Return JSON with a list of section numbers per topic.",
full_text,
],
config=types.GenerateContentConfig(
response_mime_type="application/json",
temperature=0.0,
),
)
return response.parsed
def pass2_extract(segment: str, aspect: str) -> dict:
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[
f"From the contract excerpt below, list every condition related to: {aspect}",
"Output: JSON array. Each item includes condition, section (identifier), source_span (<=120 chars from original).",
segment,
],
config=types.GenerateContentConfig(
response_mime_type="application/json",
temperature=0.0,
),
)
return response.parsedMoving from "single-pass" to "index + inspect" is worth more than 20% accuracy for me, consistently.
Technique 2: Enforce a citation-bearing response schema
Gemini 2.5 Pro is particularly good at producing plausible-sounding summaries. That is also a trap: without tight instructions it will occasionally confidently produce content the original document does not contain.
The most reliable way to suppress this is forcing every output item to carry a direct quote from the source.
from pydantic import BaseModel
from typing import Literal
class Finding(BaseModel):
aspect: Literal["termination", "price_change", "penalty", "nda"]
condition: str # summary of the condition
section_id: str # e.g. "Article 12, Section 2"
source_quote: str # direct quote from the original, <=120 chars
confidence: Literal["high", "medium", "low"]
class FindingsResponse(BaseModel):
findings: list[Finding]
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[instruction, document],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=FindingsResponse,
temperature=0.0,
),
)Making source_quote mandatory is the key move. After generation, substring-search each quote against the original document and drop anything that does not match.
def validate_findings(findings, document):
validated = []
for f in findings:
if f.source_quote in document:
validated.append(f)
else:
log.warning(f"uncited: section={f.section_id} quote={f.source_quote[:50]}")
return validatedIn my use cases, citation validation raised reliable accuracy to 90–95%. The remaining 5–10% is usually light paraphrasing that does not match verbatim — acceptable if a human reviews final output.
Technique 3: temperature=0, but design for the reasoning step
Temperature 0 is the right default for this kind of work. Different answers for the same document and question are unacceptable in a business context. That alone is not enough, though.
Gemini 2.5 Pro has internal reasoning depth. You benefit by giving it room to use it. Two levers via the API:
- Generous
max_output_tokens. Small output caps make the model self-censor the number of findings it reports. - Specify the reasoning order in the prompt. Something like "first list relevant sections, then extract conditions, then verify consistency."
instruction = """
Follow these steps to analyze the long document.
Step 1: List the section numbers likely related to the target topics.
Step 2: Extract the relevant conditions from each section above.
Step 3: Verify the extracted conditions for internal contradictions and flag any.
Return JSON only.
"""You do not need to ask for the reasoning trace — just the final JSON. What the explicit ordering does is stabilize the internal reasoning the model applies.
Technique 4: For diff detection, put the old version first
Change detection between document versions is a common application. A subtle point: the order in which you pass the two versions matters.
Putting the old version first and the new version second produced better detection of changes in my experiments. The reverse order tended to use the new version as the reference, which weakened detection of clauses present only in the old version (deletions).
contents = [
"Old version (October 2025):\n" + old_document,
"New version (April 2026):\n" + new_document,
"""
Compare the old and new versions and list changes under these categories:
1. Deleted clauses (present in old, absent in new)
2. Added clauses (present in new, absent in old)
3. Modified clauses (present in both but substantively different)
Include direct quotes from both versions for every item.
""",
]As an aside: feeding in a mechanical line-level diff alongside the full texts stabilizes results further. The model spends less effort locating candidate changes and more effort classifying them. That is my current preferred approach.
Technique 5: Make every output verifiable
The last technique is about output format. Long-document outputs must be verifiable mechanically, or you cannot operate them over time.
My preferred move is embedding checkable facts in the structure. For contract clause extraction I always include:
section_id— unique within the documentsource_quote— direct quote from the originalpage_hint— approximate page number for human spot-checks
source_quote is verified by substring match. section_id is cross-referenced against the set of section IDs present in the document. page_hint guides the humans who spot-check.
The validator is short:
import re
def check_findings(findings, document):
section_ids = set(re.findall(r"Article\s+\d+(?:\s+Section\s+\d+)?", document))
issues = []
for f in findings:
if f.section_id not in section_ids:
issues.append(f"unknown section: {f.section_id}")
if f.source_quote not in document:
issues.append(f"uncited quote in {f.section_id}")
return issuesWire this into CI and you will see quality regressions immediately. Without a quality observation loop, long-context pipelines quietly degrade over time and no one notices.
Observed numbers on 180-page documents
Rough measurements on Japanese and English contracts I have worked with, using Gemini 2.5 Pro:
- Single-pass full document: 20–30% miss rate, same clause sometimes cited twice
- Two-pass (index + inspect): miss rate under 5%, duplicates almost gone, ~1.7× runtime
- Schema enforcement + citation validation: hallucination rate dropped from ≥10% to <2%, negligible runtime change
- Old-first diff ordering: deletion recall improved from ~60% to ~90%
Runtime goes up; human review time goes down more. For business workflows, that trade is a clear win. The citation validator pays for itself fastest because it lets reviewers trust the output at a glance.
Three traps to avoid
Trap 1: treating repeated calls as a cache. Gemini has an explicit context cache, but it requires deliberate configuration. Passing the same document without setting up the cache means paying full token cost every call. For repeated long-document workflows, configure the cache API explicitly.
Trap 2: sending PDFs without pre-extraction. Some APIs accept PDFs directly, but for corporate documents pre-extracting text usually improves accuracy. Tables in contracts are notoriously fragile through PDF extraction — invest in the conversion layer.
Trap 3: over-trusting temperature=0. Determinism is not total. Small prompt rewordings still shift outputs. Keep a fixed evaluation set and measure regressions when you change the model or prompt.
The last piece for business-grade use
None of these five techniques is flashy. Pre-segmentation, citation enforcement, reasoning order, diff ordering, verifiable output. But stacking them moves 200-page documents from "sometimes works" to "production grade."
If you are starting from zero, add the citation schema and citation validator first. Those two reduce reviewer time more than anything else, and everything else compounds on top.