P3.8 · D5 + D2 · Process33% of CCA-F26 min build

Long Document Processing.

A retrieval-augmented long-document agent. Semantic chunking (paragraph- and section-aware, not fixed-size) preserves meaning; an embedding index supports top-K retrieval so only the relevant chunks enter context; an immutable CASE_FACTS block anchors transactional values (doc_id, extracted_count, decisions made) at the prompt top, surviving every chunk; a checkpoint-and-resume pattern saves state on max_tokens and continues in a fresh session; citations (chunk_id + page) flow through every output for audit-grade provenance. The most-tested distractor: progressive summarization of CASE_FACTS. Exact values like '$247.83' get paraphrased to '~$250' and the audit fails.

Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Long Document Processing.
Share
01 · System & parts

What this system is, and its 5 parts

Think of this as how you read a 200-page contract and extract every clause that matters without losing your place. The naive way. Paste the whole document into the prompt. Fails because the model runs out of room around page 120 and forgets what it saw on page 1. The right way is to chunk the document into reasonable pieces, build a search index over the chunks, retrieve only the chunks relevant to the current question, and pin the immutable facts (document ID, extraction state, decisions already made) in a CASE_FACTS block at the top of every prompt. When the run hits the model's token limit mid-document, you save state and resume. Like a bookmark. The whole point is that long documents are not one big prompt; they're many small ones with an immutable thread.

5Components
D5Primary domain
8Build steps
9Decision traps
8Concept links
Stack · Claude SDK · embeddings provider (Voyage/OpenAI) · vector store · Batch API for bulkNeeds · context window · case-facts pattern · stop_reason
Long Document Processing component architecture.
5 components. Each owns one concept.
01

Semantic Chunker

paragraph + section boundaries, not fixed-size

Splits the document at natural boundaries (sections, paragraphs, list items) rather than at fixed byte counts. Preserves the meaning of each chunk; a sentence is never cut in half. Adds 10-20% overlap between adjacent chunks so a clause that spans a boundary still appears whole in at least one chunk. Fixed-size chunking destroys context; semantic chunking preserves it.

ConfigurationChunk size: 500-2000 tokens (≈400-1500 words). Overlap: 10-20%. Boundary precedence: section header → paragraph → sentence (never break mid-sentence). Each chunk gets a deterministic chunk_id and a page number for citation.
Concept: context-window
02

Retrieval Index (top-K)

embeddings + cosine similarity

Embeds every chunk once at ingest time; stores embeddings in a vector index (FAISS, pgvector, Pinecone). At extraction time, the agent's question gets embedded; the index returns the top-K most-similar chunks (K = 5 typical). Only those K chunks enter context. The full document never does. Latency p95 < 100ms even on 10K-chunk documents.

ConfigurationEmbedding model: Voyage-3 or OpenAI text-embedding-3-small (~$0.13 / M tokens). Distance: cosine similarity. K: 5 (sweet spot. Bigger dilutes context, smaller misses relevance). Re-rank with Claude Haiku for the top-20 → top-5 if precision matters.
Concept: context-window
03

CASE_FACTS Block

immutable anchor at prompt top

Pinned at the very top of every system prompt iteration. Holds doc_id, extracted_count, decisions already made, policy_cap. Survives every chunk swap. NEVER summarized. Exact values like '$247.83' stay exact across hundreds of turns. The conversation history below it CAN be summarized; case-facts cannot. The architectural difference between 'reliable extraction' and 'paraphrased nonsense'.

Configurationsystem: "CASE_FACTS (immutable; re-read every turn): doc_id={doc_id}, extracted_count={count}, last_clause_id={cid}". Updated by hooks after state-changing tool calls.
Concept: case-facts-block
04

Checkpoint-and-Resume

the architectural fix for max_tokens

When the model returns stop_reason: max_tokens, the harness writes the current case-facts + last extracted record + chunk position to a durable store (Convex DB, S3, local JSONL), then starts a FRESH session and reads the checkpoint as its case-facts. The new session continues from where the old left off. No data loss; no re-processing; no manual intervention.

ConfigurationOn stop_reasonmax_tokens: persist({doc_id, extracted_count, last_chunk_id, partial_extraction}). New session: system_prompt loads case-facts from checkpoint. Idempotent on the chunk_id key.
Concept: checkpoints
05

Citation Tracker

chunk_id + page on every output

Every tool result includes the chunk_id(s) and page number(s) that supported the extraction. The model's output schema requires citations: [{chunk_id, page, span?}]; downstream consumers can click any extracted value and see the exact paragraph in the original document. Audit-grade provenance, structurally enforced.

Configurationextract_clause output_schema: { clause_text, clause_type, citations: [{ chunk_id, page, span?: 'character offsets within chunk' }] }. The model can't emit a clause without at least one citation.
Concept: structured-outputs
02 · Problem framing

The problem

What the user needs
  1. Process 200-page contracts without max_tokens errors and without losing the order/case ID partway through.
  2. Audit-grade citations. Every extracted clause traces back to a specific chunk and page number.
  3. Bulk overnight runs for backfills. 1000 documents in one batch, results next morning.
Why naive approaches fail
  1. Stuff the whole document into the prompt → max_tokens at page 120; lost-in-the-middle drops the order ID established on page 1.
  2. Progressive summarization of facts → '$247.83' becomes '~$250' in the case-facts; audit fails because exact values were paraphrased.
  3. RAG without citation tracking → model hallucinates source pages; auditor can't verify any claim against the original document.
Definition of done
  • Semantic chunking (paragraph + section boundaries), not fixed-size, with 10-20% overlap
  • Top-K retrieval (K = 5 typical) returns only relevant chunks; full document never enters context
  • CASE_FACTS block pinned at every prompt top. Never summarized, only the conversation history is
  • Checkpoint-and-resume on max_tokens: state saved, fresh session, resume from checkpoint
  • Citations (chunk_id + page) propagate through every extraction; auditor can verify each claim
  • Batch API for bulk overnight extraction (≥ 100 docs at 50% off)
03 · Data flow

One run, traced end to end

Long Document Processing sequence diagram.
Long Document Processing end-to-end flow.
04 · Build

8 steps to production

01

Semantic chunking with overlap

Walk the document; split at section / paragraph / sentence boundaries (in that precedence). Aim for 500-2000-token chunks; add 10-20% overlap between adjacent chunks so a clause spanning a boundary stays whole in at least one chunk. Each chunk gets a deterministic chunk_id (hash of content) and a page number for citation.

Concept: context-window
Python
import hashlib
from typing import TypedDict

class Chunk(TypedDict):
    chunk_id: str
    page: int
    text: str

def chunk_document(pages: list[str], target_tokens: int = 1200, overlap: float = 0.15) -> list[Chunk]:
    """Semantic chunking with overlap. Split at section, paragraph, sentence."""
    chunks = []
    buffer = ""
    page_buffer_started = 1

    for page_num, page_text in enumerate(pages, start=1):
        for paragraph in split_into_paragraphs(page_text):
            # If adding this paragraph would exceed target, emit current buffer
            if approx_tokens(buffer + paragraph) > target_tokens and buffer:
                chunks.append({
                    "chunk_id": hashlib.md5(buffer.encode()).hexdigest()[:12],
                    "page": page_buffer_started,
                    "text": buffer.strip(),
                })
                # Carry forward the last 15% as overlap
                tail = buffer[-int(len(buffer) * overlap):]
                buffer = tail + paragraph + "\n\n"
                page_buffer_started = page_num
            else:
                buffer += paragraph + "\n\n"

    if buffer.strip():
        chunks.append({
            "chunk_id": hashlib.md5(buffer.encode()).hexdigest()[:12],
            "page": page_buffer_started,
            "text": buffer.strip(),
        })
    return chunks

def split_into_paragraphs(page_text: str) -> list[str]:
    return [p for p in page_text.split("\n\n") if p.strip()]

def approx_tokens(text: str) -> int:
    return len(text) // 4  # rule of thumb
02

Embed and index every chunk

At ingest time, embed each chunk once with a strong embedding model (Voyage-3 or OpenAI text-embedding-3-small) and store in a vector index. Index by chunk_id; the embedding becomes the search key. Re-embedding only fires on content change (hash-keyed cache). For a 200-page document at ~500 chunks, embedding is a ~$0.05 one-time cost.

Concept: context-window
03

Retrieve top-K chunks; never the full document

When the agent asks a question (e.g., 'what's the indemnification clause?'), embed the question, retrieve the top-K=5 most-similar chunks, and pass ONLY those into context. The full document never enters the prompt. K=5 is the sweet spot. Bigger K dilutes context with marginally-relevant chunks; smaller K misses the right one. Re-rank top-20 with Claude Haiku if precision matters.

Concept: context-window
04

Pin CASE_FACTS at the prompt top. Never summarize

Every prompt iteration starts with a CASE_FACTS block: doc_id, extracted_count, last_clause_id, decisions already made. The block is rebuilt from durable state every turn. It survives summarization, model swaps, and session resets. Critically, EXACT VALUES ($247.83, not ~$250; cust_4711, not the customer) stay verbatim. The conversation history below it CAN be summarized; the case-facts cannot.

Concept: case-facts-block
05

Checkpoint on max_tokens; resume in a fresh session

When stop_reason 'max_tokens', the harness writes the current case-facts + last extracted record + chunk position to a durable store, then starts a fresh session and re-loads the checkpoint as its case-facts. Because case-facts are at the prompt top, the new session continues exactly where the old left off. No data loss; no manual intervention; no need for the agent to even know.

Concept: checkpoints
06

Citations. Chunk_id + page on every output

Every extraction tool emits a citations: [{ chunk_id, page }] array; the schema makes citations REQUIRED. The model can't extract a clause without pointing at the chunks that supported it. Downstream consumers (auditors, reviewers, regulators) click any extracted value and see the exact paragraph in the original. Audit-grade provenance, structurally enforced.

Concept: structured-outputs
07

Bulk extraction via Batch API (50% off, 24h)

When the use case is 'extract every payment clause from 1000 contracts overnight', the Batch API earns its 50% discount. Submit at 6 PM, results ready at 6 AM. No real-time retry inside the batch; failures get resubmitted in the next batch with their specific error in the next message. Combined with prompt caching on the system prompt + tool registry, bulk extraction cost drops 95%+ vs naive sync calls.

Concept: batch-api
08

Stratified accuracy + adversarial 'silent source' tests

Aggregate accuracy hides per-document-type weakness. Stratify by doc_type (MSA vs DPA vs SOW), by clause_type, by page-section (front/middle/back). Surface the worst stratum. Pair with an adversarial test set of 50 documents where the requested clause is GENUINELY absent. The right behaviour is clause_text='not_found' with empty citations, NEVER an invented clause. Hallucinated extractions = audit-fail.

Concept: evaluation
05 · Right call, wrong call

9 decisions the exam turns into distractors

01 · 200-page document into the prompt
Looks right

Stuff the whole document into one prompt

Actually right

Chunk + index + retrieve top-K (K=5)

DEC-01
02 · Storing transactional values across many turns
Looks right

Progressive summarization that paraphrases the conversation including facts

Actually right

CASE_FACTS block. Exact values, never paraphrased

DEC-02
03 · Hit max_tokens mid-document
Looks right

Increase max_tokens or just retry from scratch

Actually right

Save state + start fresh session + reload from checkpoint

DEC-03
04 · Bulk overnight processing of 1000 documents
Looks right

Sync API in a tight loop

Actually right

Batch API + cached system prompt + cached tool registry

DEC-04
05 · Stuff the whole document into the prompt
Looks right

Try to paste a 150-page contract into a single prompt. Hits max_tokens at page 120; lost-in-the-middle drops the order ID established on page 1; agent makes contradictory recommendations on later pages.

Actually right

Chunk + index + top-K retrieval. Only K=5 chunks ever enter context. The full document is searchable but never present. Length is bounded by retrieval, not document size.

AP-LDP-01
06 · Progressive summarization of facts
Looks right

Long conversation summarizes every 10 turns. Refund amount '$247.83' becomes '~$250' in the summary; customer ID 'cust_4711' becomes 'the customer'. Audit fails because exact values were paraphrased.

Actually right

CASE_FACTS block at every prompt top. Never summarized. Holds exact values verbatim. Only the message history below is summarized; the case-facts persist verbatim across every iteration.

AP-LDP-02
07 · No checkpoint on max_tokens
Looks right

Long batch job processes 200 pages, hits max_tokens at turn 15. The whole pipeline aborts; everything extracted so far is lost; operator has to restart from page 1.

Actually right

Checkpoint-and-resume: on stop_reason: max_tokens, persist state (case_facts + last extraction + chunk position) and start a fresh session that reloads the checkpoint as its case-facts. Idempotent on chunk_id.

AP-LDP-03
08 · RAG without citations
Looks right

Agent retrieves chunks and emits extracted clauses without saying which chunk supported each one. Auditor asks 'where does this come from?' and there's no answer; auditor flags the run as un-verifiable.

Actually right

Citation tracker: every extraction emits citations: [{ chunk_id, page }] with minItems: 1 in the schema. The model can't extract a clause without pointing at the supporting chunks. Audit-grade provenance is structurally enforced.

AP-LDP-04
09 · Chunking destroys context
Looks right

Fixed-size chunks (every 1000 characters) split mid-sentence and mid-paragraph. A clause that spans a chunk boundary appears truncated in both adjacent chunks; retrieval misses it; extraction is wrong.

Actually right

Semantic chunking with overlap. Split at section / paragraph / sentence boundaries (in that precedence). Add 10-20% overlap between adjacent chunks so a boundary-spanning clause stays whole in at least one chunk. Each chunk is meaning-complete.

AP-LDP-05
Long Document Processing failure map.
06 · Budget

Cost & latency

~$0.05One-time embedding (200-page doc, ~500 chunks)

500 chunks × ~1000 tokens × Voyage-3 at $0.13/M tokens ≈ $0.06. One-time cost at ingest; never re-paid unless content changes.

~$0.008-0.015Per-extraction retrieval + Claude call (cached system + tools)

Embedding query (~$0.0001) + vector lookup (~$0.0001) + 5 chunks × ~1000 tokens (cached) + ~500 output tokens. Cache hit rate ≥ 70% drops effective per-call cost ~80%.

~$0.004-0.008 per extractionBulk overnight (Batch API + caching)

Batch API 50% discount × prompt caching ~90% off the system + tools = ~95% off naive sync. 1000 extractions @ $0.008 each = $8 total overnight.

~5-15 KB per documentCheckpoint storage

JSON dump of case_facts + last extraction + chunk position. Negligible per-document; at 1000 docs in flight, <15MB total. Idempotent re-loads add no cost.

~2-4 secondsp95 latency per extraction (cached, sync)

Embed query (50ms) + vector lookup (50ms) + Claude call (2-3s with cache hit). Acceptable for interactive review of contracts; bulk uses Batch API.

07 · Ship checklist

Check every gate before release

0/11 checked
  • context-window
  • context-window
  • case-facts-block
  • checkpoints
  • structured-outputs
  • prompt-caching
  • batch-api
  • evaluation
08 · Practice

5 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 5 · D5Choose the best answer

150-page contract; the agent processes it in one prompt and dies at page 120 with max_tokens. Recovery without losing everything?

09 · FAQ

Frequently asked

What's the optimal chunk size?

500-2000 tokens (~400-1500 words). Smaller and you make too many retrieval calls; larger and you lose the granularity that makes top-K retrieval useful. 1000-1200 tokens is a good default. Always pair with 10-20% overlap so boundary-spanning content survives.

Should I retrieve all matching chunks or top-K?

Top-K (default K=5). All-matching floods context with marginally-relevant noise; the model wastes attention. Top-5 keeps the prompt focused. If precision is critical, retrieve top-20 with embeddings and re-rank with Claude Haiku to top-5.

How do I prevent lost-in-the-middle?

Anchor critical facts at context top (CASE_FACTS), retrieve top-K only, trim verbose tool results. Long contexts dilute attention to middle content. Keep the prompt structure: CASE_FACTS at top, retrieved chunks in the middle, the user's latest message at the end. Don't put case-facts in the middle of the prompt.

Does prompt caching help with RAG?

Partially. Cache the stable parts: system prompt + tool registry + (optionally) the CASE_FACTS scaffold. The retrieved chunks change every query, so they're always fresh. Realistic savings: ~30-50% total cost reduction depending on system-prompt size. Not as dramatic as caching a 200-page document would have been, but RAG never had that overhead in the first place.

Where do I store the checkpoint?

Durable, idempotent storage. Convex DB, S3, or a local JSONL file in dev. Key by doc_id. The checkpoint write must be atomic; partial writes confuse the resume path. Retain checkpoints until the document is fully processed; delete on completion or after 30 days, whichever comes first.

Can I combine Batch API with checkpoint-and-resume?

Yes. For the long-running ones. Submit each document as one batch request. If a request hits max_tokens, the harvest step writes a checkpoint and includes that document in the NEXT batch with the checkpoint as case-facts. Two batches usually finish a long document; rare cases need three.

How do I handle a 500-page doc that exceeds even the chunked + paged max_tokens cap?

Checkpoint after every ~50 pages or natural boundary (chapter, section). The harness writes checkpoints on max_tokens automatically; at 500 pages you'll see ~5-8 checkpoint events across multiple sessions. Each session is bounded; the document length isn't.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.