Case study: compliance doc agent
A representative regulated-industry case study, end to end: a bank builds an agent that extracts obligations, flags risk, and answers questions over large regulatory and contract documents — with a citation for every claim, a confidence gate, human sign-off, and an immutable audit trail. Real Anthropic features (tool use, structured output, citations, prompt caching, evals) as the load-bearing parts; a wrong obligation is legal liability, so the discipline is abstain, cite, and gate.
Learning objectives
- State the requirements that make a regulated document agent different: accuracy, auditability, no-hallucination, and strict data handling.
- Design an architecture that ingests large documents, caches them, extracts to a schema with citations, gates on confidence, and logs an audit trail.
- Use Anthropic's real features — tool use, structured output with a JSON schema, citations, and prompt caching — as the load-bearing building blocks.
- Enforce the no-hallucination discipline: every obligation must cite a source span, or the agent abstains and escalates to a human.
- Evaluate the agent on three axes: extraction accuracy, citation integrity, and abstention correctness.
- Reason about cost with prompt caching on big documents, and harden against the failure modes that turn a wrong obligation into legal liability.
1 · The scenario & its requirements advanced
Picture the meeting where this project gets killed. An engineer demos a slick "chat with your contracts" bot. It answers a question about a termination clause instantly and confidently. Then someone from legal asks the only question that matters: "Where in the document does it say that?" — and the bot has no answer, because it summarized from a blur of retrieved text. In a consumer app that's a shrug. Here it's a showstopper: the bank cannot file, act on, or defend a compliance decision it can't trace to a specific line of a specific document. The demo dies in the room, and the lesson is that in a regulated domain the usual default flips — a confident wrong answer is worse than no answer at all.
That flip is why the first step of this case study is not architecture or code — it's requirements, and they read differently than the feature lists you're used to. When you build a normal product, requirements describe what the system does ("summarize a document", "answer questions"). Here they describe what the system is not allowed to do: never assert an obligation it can't cite, never treat a machine output as authoritative, never lose the paper trail. Think of it like the difference between a helpful research assistant and a court reporter. The assistant is judged on being useful; the court reporter is judged on being verifiable — every word attributable to a source, on the record, reviewable later. This agent must behave like the court reporter.
Before the table below, notice how the pieces hang together, because they aren't independent wishes — they form a single chain of trust:
- Accuracy without auditability is worthless — a correct obligation you can't trace back to a clause can't be defended when challenged, so it can't be relied on.
- No-hallucination is the load-bearing rule — it's what lets a human trust the pipeline enough to review rather than re-read every source from scratch.
- Human sign-off is the release valve — it converts "the machine proposed this" into "a person with authority approved this", which is the only status that counts as authoritative.
The common beginner mistake is to treat these as nice-to-haves bolted on after the model works. They are the opposite: they are the reason the model is allowed to run at all. Build the controls first and the model becomes a fast draft-generator inside a safe frame; build the model first and you get the demo that dies in the meeting.
Read the boxes left to right as a chain — each requirement depends on the one before it, and only the end of the chain produces something the bank can act on.
- Accuracy (first box) — the extracted obligation actually matches the document. Necessary, but on its own not enough.
- Auditability — a correct obligation you can trace back to a clause; a right answer you can't source can't be defended when challenged.
- No hallucination (the highlighted purple box) — the load-bearing rule: the agent asserts only what it can cite, or it abstains. This is what lets a human trust the pipeline enough to review rather than re-read everything.
- Human sign-off — the release valve that converts "the machine proposed this" into "a person with authority approved this".
- Follow the arrow down to the green authoritative box: only after every link holds and a human signs is the obligation something the bank can file and defend.
In short: these aren't four independent wishes — they're one chain. Break any link and an obligation stops being trustworthy, no matter how good the others look.
A mid-size bank has a shelf of long documents: vendor master-services agreements, regulatory rulebooks, and internal policies — each tens to hundreds of pages. A compliance analyst's job is to read them and answer three questions: What are we obligated to do? What's the risk if we don't? Where exactly does the document say so? Doing this by hand is slow, and a missed clause can become a regulatory finding. The bank wants an agent that extracts obligations, flags risk, and answers questions over these documents — but only if it can be trusted the way a human analyst is trusted: with a citation for every claim and a human sign-off before anything is filed.
That trust requirement is the whole design. This is not a chatbot where a wrong answer is embarrassing; it is a system where a wrong obligation is a legal liability. So the requirements read like a controls document, not a feature list:
| Requirement | What it means for the design |
|---|---|
| Accuracy | Extracted obligations must match the document. Measured against a labeled gold set per field, not judged by vibe. |
| Auditability | Every output is traceable: which document, which span, which model + prompt version, what confidence, who signed off. An immutable audit log. |
| No hallucination | The agent may only assert what it can cite. If it cannot ground a claim in a source span, it must abstain, not guess. |
| Data handling | Contracts contain confidential and often personal data. Retention, access control, and (where required) redaction are part of the pipeline, not an afterthought — verify your obligations under the applicable regime. |
| Human sign-off | No obligation is treated as authoritative until a person with authority reviews and approves it. The agent proposes; a human disposes. |
2 · The architecture, end to end advanced
Here's the trap that catches most teams building their first document agent. They wire the model straight to the document — "here's the contract, extract the obligations" — and it works beautifully in the demo. Then it meets a real 200-page rulebook and three things go wrong at once: the cost balloons because every question re-reads the whole document, the model quietly invents a clause that sounds right, and when an auditor asks "how did you arrive at this?" there's nothing to show. Each of those is a separate leak, and the architecture in this section is best understood as a pipeline built specifically to plug all three, in the right order.
The mental model that makes the whole diagram click: this is not one clever AI call, it's an assembly line with a quality-control station and a human inspector at the end. Think of how a pharmacy fills a prescription. The doctor's order (the document) comes in; a technician counts out the pills (extraction); a pharmacist checks the count against the label before it goes out the door (the confidence gate + citation check); and every fill is logged with who dispensed it (the audit trail). No single step is trusted on its own — the safety comes from the sequence, and specifically from a checkpoint that can reject work and a human who signs before anything leaves the building.
Why does the order of the stations matter so much? Because each one exists to protect the next:
- Chunk + cache comes first so you pay to read the document once, not once per obligation — the economics that make everything downstream affordable enough to run at all.
- Extract-with-citation comes before the gate because the gate has nothing to check unless every proposed obligation already carries a claimed source span.
- The confidence gate comes before the human so the human's scarce attention is spent only on the uncertain and the ungrounded, not on rubber-stamping obvious accepts.
- The audit log wraps everything because a decision you can't reconstruct later is, for compliance purposes, a decision that never happened defensibly.
The precise term for what this design achieves is grounding by construction: rather than hoping the model stays truthful, the pipeline makes an ungrounded obligation structurally impossible to accept. Keep that phrase in mind as you read left to right — every box is there to turn a "we hope it won't" into a "it can't".
Read the pipeline left to right. A large document is chunked and cached so we don't re-pay to read it on every question; each chunk is run through a schema-constrained extraction that must return a source span (a citation) for every obligation; a confidence gate decides what is trustworthy enough to auto-accept; everything uncertain goes to human sign-off; and every step writes to an append-only audit log.
This is the whole compliance agent in one row, read left to right. It shows how a large document becomes a set of trustworthy, cited obligations — and, crucially, where the agent hands off to a human.
- Large doc — the raw input: a contract or rulebook, often hundreds of pages, far too big to re-read on every question.
- Chunk + cache — the document is split on clause/section boundaries and marked with a cache breakpoint (
cache_control), so you pay to read it once, not once per question. - Extract + cite (the purple, highlighted box) — the one schema-constrained call per chunk. The schema requires a citation, so every obligation comes back pointing at the exact source span it came from.
- Confidence gate (the amber box) — plain code decides what's trustworthy: a verified citation plus high confidence auto-accepts; anything uncertain or ungrounded abstains and moves right.
- Human sign-off (green) — a person with authority approves before anything is treated as authoritative. The agent proposes; the human disposes.
- Audit log — every step is written to an append-only record, so you can replay exactly what was claimed, from which source, and who approved it.
In short: follow the single path left to right, then notice the safety valve in the middle — the confidence gate — and the human at the end. Nothing becomes authoritative until a person signs off, and the whole path is logged.
Three design commitments make this safe rather than merely clever. First, extraction is grounded: the schema requires a citation, so an obligation with no supporting span is invalid by construction. Second, the gate is conservative: uncertainty routes to a human, never to auto-accept. Third, the log is immutable: you can reconstruct exactly what the agent claimed, from which source, at which prompt version, and who approved it — which is what an auditor will ask for.
3 · Ingesting & caching big documents advanced
Imagine hiring a lawyer to answer twenty questions about one contract — and re-reading the entire contract from page one before every single answer. You'd be billed for twenty full read-throughs of a document that never changed. That's absurd for a human, and it's exactly what a naive document agent does: each question ships the whole 200-page document back to the model, and you pay full input price to "read" it every time. The lab in this section makes the number concrete, but the intuition is enough to feel the problem — the document is the expensive part, and you're paying for it over and over.
Prompt caching is the fix, and the analogy that makes it stick is a bookmark. Instead of re-reading, the lawyer reads the contract once, then keeps it open on the desk with the relevant sections tabbed. Each new question just consults the already-open book. Anthropic's prompt caching does the same thing at the API level: you mark a large, stable chunk of the prompt (the document) as cacheable; the first request pays a small premium to "read and bookmark" it (the cache write), and every later request that reuses that same prefix pays a steeply discounted cache read instead of full price. The document sits ready; only your small, changing question is billed at normal rates.
Two subtleties separate people who use caching correctly from people who get burned by it:
- The cache is a prefix, so ordering is everything. The stable stuff — document, system prompt, schema — must come first, and the volatile stuff — the specific question — must come after the cache breakpoint. Put the question before the document and you've changed the prefix every time, so nothing ever hits the cache and you quietly pay full price while believing you're saving.
- A cache is a snapshot, and snapshots go stale. This is the operational trap you'll meet again in the hardening section: if the contract is amended but the cache still holds the old text, the agent will confidently answer against a version that no longer exists — and cite it. The discipline is to key the cache by the document's version or hash and invalidate it the moment the document changes.
There's also a genuine trade-off worth naming, not hiding. Caching is a bet: you pay the write premium up front gambling that you'll reuse the document enough times to earn it back. Ask one question and walk away and caching actually cost you a little more. Ask twenty and the write premium is dwarfed by the cheap reads. That's why this pattern fits a compliance agent so well — analysts don't ask one question of a contract, they interrogate it — and why you'll see the savings climb as reuse climbs.
A 200-page rulebook is far too big to re-send on every question. Anthropic's prompt caching is built for exactly this: you mark a large, stable prefix (the document) with a cache breakpoint, pay a one-time cache-write premium, and then every subsequent request that reuses that prefix pays a deeply discounted cache-read instead of full input price. That is what makes "ask twenty questions of one big contract" economically sane.
cache_control ({"type": "ephemeral"}) to a content block to set a breakpoint; the cached prefix must clear a minimum token count (about 1024 for the larger models); a cache write costs roughly 1.25× the base input price and a cache read roughly 0.1× (about 90% off); the default cache lifetime is about 5 minutes, refreshed on use, with a longer 1-hour option. Numbers drift — confirm them live before you budget on them.Two ingestion decisions matter. Chunking: long documents are split so that each obligation lives whole inside one chunk (split on clause/section boundaries, not blindly every N characters), because a citation that straddles a chunk boundary is useless. Cache placement: the document (and the stable system prompt + schema) go in the cached prefix; only the small, changing question goes after the breakpoint. The lab below models the cost of that arrangement with pure arithmetic — no network.
Compare the two panels side by side: the left is the naive layout, the right is the cached one. Each row is one question in the session.
- The left red stack shows the naive approach: every question re-sends the full ~150k-token document and pays full input price to "read" it again. Three questions = three full re-reads.
- The right panel puts the document in the cached prefix: the first request pays a one-time cache write (~1.25× input, the blue box), and every later question pays a deeply discounted cache read (~0.1×, the green boxes) plus its own small question.
- The cache breakpoint marker is the key detail: the stable document sits before it, the volatile question after. Put the question first and the prefix changes every time — nothing hits the cache.
- Read the bottom labels as the payoff: naive cost grows as
n × doc; cached cost is roughly1× doc + n × Q, whereQ(the question) is tiny.
In short: stable stuff first, changing stuff last — pay to read the document once, then answer many questions cheaply. Reorder them and you quietly pay full price while thinking you're saving.
caching_cost.py# stdlib only, no network. Models the economics of caching one big document
# across many questions. Prices are $ per 1M tokens; multipliers reflect Anthropic's
# PUBLIC caching guidance (cache write ~1.25x input, cache read ~0.1x input).
# Verify the exact numbers in Anthropic's current docs before budgeting.
def session_cost(doc_tokens, n_questions, q_tokens, out_tokens,
in_price, out_price, write_mult=1.25, read_mult=0.10):
"""Compare re-sending the whole doc every question vs caching it once."""
# No cache: every question re-pays full input price for the whole document.
no_cache_in = (doc_tokens + q_tokens) * n_questions
no_cache = (no_cache_in * in_price + out_tokens * n_questions * out_price) / 1_000_000
# With cache: pay the write premium once for the doc, then cheap reads thereafter.
write = doc_tokens * in_price * write_mult
reads = doc_tokens * in_price * read_mult * (n_questions - 1)
questions_in = q_tokens * in_price * n_questions # the small changing part
outputs = out_tokens * out_price * n_questions
cached = (write + reads + questions_in + outputs) / 1_000_000
return {
"no_cache_usd": round(no_cache, 2),
"cached_usd": round(cached, 2),
"saved_usd": round(no_cache - cached, 2),
"saved_pct": round(100 * (no_cache - cached) / no_cache),
}
# A 200-page rulebook (~150k tokens), 20 questions, on a mid model ($3 in / $15 out).
r = session_cost(doc_tokens=150_000, n_questions=20, q_tokens=80, out_tokens=350,
in_price=3.00, out_price=15.00)
for k, v in r.items():
print(f"{k}: {v}")
no_cache_usd: 9.11
cached_usd: 1.53
saved_usd: 7.58
saved_pct: 83
This tiny program answers the money question behind the whole design: if analysts ask many questions of one huge document, is it affordable? It compares re-sending the document every time against caching it once — pure arithmetic, no network.
no_cacheassumes the naive approach: every one of then_questionsre-pays full input price to read the wholedoc_tokensdocument. That's the expensive baseline.- The cached path pays a one-time
writepremium (~1.25×input price) to store the document, then cheapreads(~0.10×, about 90% off) for the remaining questions. Only the small changing question and the output are billed normally each time. - The function returns both totals plus the dollars and percentage saved, so you can see the trade at a glance.
What the output means: Reading the 150k-token document dominates, so caching it once instead of 20 times saves about 83% — roughly $9.11 down to $1.53 for the session.
Try this: Bump n_questions from 20 to 100 and watch saved_pct climb: the more times you reuse the same cached document, the more the one-time write premium is dwarfed by cheap reads. Confirm the exact multipliers in Anthropic's current docs.
4 · Schema-constrained extraction with citations expert
Ask a model to "extract the obligations" as free text and you'll get prose you then have to parse, second-guess, and babysit. Worse, nothing stops it from writing a beautifully phrased obligation that isn't actually in the document — it has no obligation (pun intended) to point at a source, so on a hard clause it will smooth over the gap with something plausible. In a chatbot that's a mild annoyance. Extracting obligations from a bank's contracts, a single smoothed-over clause can become a regulatory finding. So the design does two things at this step that turn hope into guarantee: it constrains the shape of the output, and it makes a source citation a mandatory part of that shape.
Start with the shape. Schema-constrained extraction means you hand the model a rigid template — think of a paper form with labelled boxes — and require the answer to come back as that form filled in, not as an essay. Instead of "tell me about the obligations", it's "for each obligation, fill in: party, text, risk level, citation, confidence". A filled form is machine-checkable; an essay is not. This is the same schema-constrained-extraction pattern the Document Intelligence project uses, and its quiet superpower is that it moves correctness checks from "read the answer and judge it" to "check that box 4 is present and well-formed" — something code can do perfectly, every time.
Now the load-bearing move: make the citation box a required field of the form. This is a genuinely different kind of safety than a polite instruction like "please cite your sources". A requested citation is something the model can forget under pressure; a required field is something the reply is structurally invalid without. Put plainly:
- With an optional citation, "an obligation with no source" is a bad answer you have to notice and catch after the fact.
- With a required citation, "an obligation with no source" is an impossible shape — it can't be emitted as a valid reply in the first place.
That is the difference between hoping and enforcing. You've promoted "every claim must be grounded" from a wish written in the prompt to a type guarantee written in the schema. Be honest about the limit, though — and the next section is built entirely on this limit: a required-citation schema forces the model to attach a quote and offsets, but it cannot force that quote to be true. The model can still fill the box with a fabricated or misaligned citation. The schema guarantees the citation is present; only code (§5) can guarantee it is real. The lab below is deliberately an offline, deterministic stand-in for the model call, precisely so you focus on that data contract — the shape and the required span — rather than the network mechanics.
The heart of the agent is one disciplined call per chunk: structured output with a JSON schema forces the model to return obligations in an exact shape, and the schema requires a citation on every obligation. Anthropic's citations feature is designed to return grounded spans that point back into the source document; combined with a required-citation schema, an obligation the model cannot ground simply cannot be emitted as valid.
Below is the deterministic, offline stand-in for that call. A real system would send the chunk to Claude with a required-citation schema; here a fake extractor returns obligations, each with a claimed quote and character offsets, plus a confidence. The point of the lab is the shape of the data and the contract it must satisfy — not the model call, which is why it runs with no key.
citation a required schema field (right, highlighted) turns "an obligation with no source" from a bad answer into an impossible shape.
Contrast the two panels: the left is what you get asking for free text, the right is a schema-constrained obligation. Look especially at how each one treats the source citation.
- The left red box is prose. It reads well, but you have to parse it, and nothing forces a source — the "source? maybe · maybe not" line is the whole problem: an ungrounded claim is a bad answer you only catch after the fact.
- The right stack of boxes is the same obligation as a filled-in form:
party,risk,confidence, and — highlighted in purple — a requiredcitationcarrying a quote and character offsets. - The payoff is the bottom-right line: with citation as a required field, "an obligation with no source" is not a bad answer the model might give — it's an invalid shape the schema rejects before it ever reaches you.
In short: a requested citation can be forgotten; a required one makes an ungrounded obligation structurally impossible to emit. That's the move from hoping to enforcing.
extract.py# stdlib only, no network. A deterministic fake extractor that returns the SAME
# shape a schema-constrained Claude call would: obligations, each REQUIRED to carry a
# citation (quote + character span into the source) and a confidence score.
from dataclasses import dataclass, field
DOC = (
"Section 4.1 The Bank shall notify the Regulator within 30 days of any material "
"breach. Section 7.2 The Vendor shall maintain insurance of at least $5,000,000. "
"Section 9.0 Either party may terminate for convenience upon 90 days written notice."
)
@dataclass
class Citation:
quote: str
start: int
end: int
@dataclass
class Obligation:
party: str
text: str
risk: str # low | medium | high
citation: Citation
confidence: float
def fake_extract(doc):
"""Pretend-model output. In production this is one schema-constrained Claude call
per chunk, with the citations feature returning the grounded spans."""
spans = [
("Bank", "notify the Regulator within 30 days of any material breach",
"high", 0.94),
("Vendor", "maintain insurance of at least $5,000,000",
"medium", 0.88),
("Either party", "terminate for convenience upon 90 days written notice",
"low", 0.61),
]
obligations = []
for party, quote, risk, conf in spans:
start = doc.find(quote)
end = start + len(quote)
obligations.append(Obligation(
party=party, text=quote, risk=risk,
citation=Citation(quote=quote, start=start, end=end),
confidence=conf))
return obligations
for o in fake_extract(DOC):
print(f"[{o.risk:6}] {o.party:12} conf={o.confidence:.2f} "
f"@({o.citation.start},{o.citation.end})")
[high ] Bank conf=0.94 @(27,85)
[medium] Vendor conf=0.88 @(116,157)
[low ] Either party conf=0.61 @(188,241)
This is the shape of the agent's core call, with a deterministic fake standing in for the model so it runs offline. In production this is one schema-constrained Claude call per chunk, with the citations feature returning the grounded spans — but the data contract is exactly what you see here.
- The
@dataclasstypesCitationandObligationare the schema in miniature: an obligation must carry acitationwith aquoteand character offsets. "An obligation with no source" isn't a bad answer here — it's an impossible shape. fake_extractreturns three obligations. For each,doc.find(quote)computes where that quote actually starts in the document, andstart + len(quote)gives the end — so the offsets are real, not invented.- Each obligation also carries a
risklevel and aconfidencescore, the two signals the gate will use next.
What the output means: Three obligations print with their risk, party, confidence, and the @(start,end) span each citation points at — the exact structured data the rest of the pipeline consumes.
Try this: Add a fourth obligation whose quote is NOT in DOC. doc.find(...) returns -1, giving a nonsensical span — which is precisely the kind of ungrounded claim the next lab's verifier is built to catch.
citation a required field, "an obligation with no source" is not a bad answer the model might give — it's an invalid shape the schema rejects. You've moved a safety rule from a hope into a type guarantee, the same way the doc-intel project moved arithmetic sanity into a validator.5 · The no-hallucination discipline — abstain & cite expert
Here is the failure that keeps compliance leads awake, and it looks nothing like an obvious bug. The model returns an obligation, in perfect schema, with a citation filled in, and a confidence of 0.97. Everything looks green. But the quoted text isn't actually in the contract — the model paraphrased a clause from a different agreement it saw in training, or subtly shifted the numbers, and dressed it in a citation that points at plausible-looking offsets. It feels more trustworthy than the shaky 0.6 answers precisely because it's confident. If you trust it, a clause the bank never agreed to flows into a compliance filing. This is the hallucination that hurts, and this section is the single control that stops it.
The core insight is a distinction people conflate all the time: confidence is an opinion; verification is a fact. The confidence score is the model's own assessment of itself — and a model that hallucinates is, by definition, wrong about how sure it should be, so its confidence on a fabrication is worthless. You cannot fix a self-report by reading more of the self-report. You need an external, objective check, and here the check is beautifully simple and needs no AI at all: take the quote the model claims, take the offsets it claims, and ask a plain string comparison — does the source document, at exactly those character positions, contain exactly that text? If yes, the citation is grounded. If no, it's a fabrication, no matter how confident the model was.
Think of it like a bank teller checking a signature. It doesn't matter how confidently the customer signs or how genuine the signature looks in isolation — the teller compares it against the card on file. Match or no match. The confidence of the person signing is irrelevant to whether the signature checks out. Citation verification is that comparison, in code.
Two rules fall out of this, and their order is the whole discipline:
- Verify before you consider confidence. The check runs first and can veto anything. A citation that fails to verify is dropped and escalated even at confidence 0.99 — because a confident claim you cannot ground is exactly the dangerous case, not the safe one.
- When you can't ground it, abstain — don't guess. Abstaining routes the chunk to a human. That's a visible gap someone can fill. Guessing produces an invisible wrong answer that flows silently into a filing. In this domain a flagged gap is cheap and recoverable; a silent wrong obligation is neither.
That last line is the value inversion at the heart of the whole case study: being right or abstaining always beats being confidently wrong. A normal product optimizes for coverage — answer as much as you can. A compliance agent optimizes for the opposite — never assert what you can't back up. Hold that thought, because §7 turns it into a hard, measurable gate.
A required-citation schema stops the model from omitting a citation, but it cannot stop the model from fabricating one — quoting text that isn't actually in the document, or pointing at the wrong span. So the citation is not trusted; it is verified by code. This is the single most important control in the whole system, and it needs no model: you check that the claimed quote appears verbatim at the claimed offsets in the source. If it doesn't, the obligation is dropped and the chunk is escalated — the agent abstains rather than assert something it cannot ground.
Follow the arrows from the proposed obligation on the left. The order of the two checks is the entire discipline — read it as "verify first, confidence second".
- Proposed obligation — the model's output: a quote, claimed offsets, and a confidence score. Nothing here is trusted yet.
- Verify citation (the purple box) — plain code asks one objective question: does the source document, at exactly the claimed offsets, contain exactly the claimed quote?
- Follow the "no" arrow down: a citation that fails verification is dropped and escalated — and the red box spells out that this happens even at confidence 0.99. A confident claim you can't ground is the dangerous case, not the safe one.
- Only the "yes" arrow reaches the amber confidence check. High confidence auto-accepts (green); low confidence routes to human review. Both uncertainty and ungroundedness route away from silent acceptance.
In short: confidence is the model's opinion; citation verification is an objective fact checked in code. The fact runs first and can veto the opinion — which is why a fabricated citation never survives, however confident.
gate.py# stdlib only, no network. Verifies each claimed citation against the source text,
# then GATES: verified + high-confidence -> AUTO-ACCEPT; verified + low-confidence ->
# REVIEW; a citation that does NOT verify is a hallucination -> DROP + ESCALATE.
CONFIDENCE_THRESHOLD = 0.80
def verify_citation(doc, cite):
"""A citation is valid only if the exact quote sits at the claimed offsets.
This catches fabricated quotes and misaligned spans — with no model."""
if not (0 <= cite["start"] < cite["end"] <= len(doc)):
return False
return doc[cite["start"]:cite["end"]] == cite["quote"]
def gate(doc, obligations):
results = []
for o in obligations:
if not verify_citation(doc, o["citation"]):
results.append((o["party"], "DROP+ESCALATE (citation failed to verify)"))
elif o["confidence"] >= CONFIDENCE_THRESHOLD:
results.append((o["party"], "AUTO-ACCEPT"))
else:
results.append((o["party"], "REVIEW (low confidence)"))
return results
DOC = "The Bank shall notify the Regulator within 30 days of any material breach."
obligations = [
# verified + confident -> auto-accept
{"party": "Bank", "confidence": 0.94,
"citation": {"quote": "notify the Regulator within 30 days", "start": 15, "end": 50}},
# verified but low confidence -> review
{"party": "Bank-B", "confidence": 0.55,
"citation": {"quote": "material breach", "start": 58, "end": 73}},
# FABRICATED quote (not in doc) -> drop + escalate
{"party": "Ghost", "confidence": 0.99,
"citation": {"quote": "pay a penalty of $1,000,000", "start": 15, "end": 42}},
]
for party, decision in gate(DOC, obligations):
print(f"{party:8} -> {decision}")
Bank -> AUTO-ACCEPT
Bank-B -> REVIEW (low confidence)
Ghost -> DROP+ESCALATE (citation failed to verify)
This is the single most important control in the system. A required-citation schema stops the model from omitting a citation, but not from fabricating one. So citations are never trusted — they're verified in code against the source, before confidence is even considered.
verify_citationfirst bounds-checks the offsets, then does the real test:doc[start:end] == quote. The claimed quote must sit at exactly the claimed position. This catches both fabricated quotes and misaligned spans, with no model.gateapplies the rule in strict order: if the citation fails to verify, the obligation is dropped and escalated — no matter how confident the model was. Only a verified citation reaches the confidence check.- A verified, high-confidence obligation auto-accepts; a verified but low-confidence one goes to human review. Uncertainty and ungroundedness both route away from auto-accept.
What the output means: "Bank" (verified + 0.94) auto-accepts; "Bank-B" (verified + 0.55) goes to review; "Ghost" is dropped and escalated even at confidence 0.99, because its quote isn't in the document.
Try this: Give "Ghost" a real quote from DOC but leave the wrong offsets. It still fails, because verification checks the position, not just "is this text somewhere?" — the stronger guarantee for a legal document.
6 · Human-in-the-loop & the audit trail professional
Fast-forward eighteen months. A regulator disputes one obligation the bank acted on, and asks a single question: "On what basis was this set, and who approved it?" If the answer is "our AI extracted it, and we think a analyst reviewed it around then," the bank is in serious trouble — not because the obligation was necessarily wrong, but because it can't be defended. Everything in this section exists to make that dreaded question boring to answer. The two pieces are a human in the loop and an audit trail, and it's worth being precise about what each one buys you, because beginners tend to add a login screen, call it "human review", and miss the actual point.
First, the status distinction that the whole workflow turns on. The confidence gate's "AUTO-ACCEPT" bucket does not mean "authoritative" — it means proposed. The agent is always a proposer; authority comes only when a person with compliance responsibility signs off. This is the "the agent proposes, a human disposes" rule, and it's not bureaucratic theater — it's what places accountability on a named human who can be asked to explain the decision. A machine can't be held responsible in a regulatory sense; a person who reviewed the citation and approved it can.
Second, the audit trail — and the mental model here is a courtroom evidence log or a hospital chart, not an application log file. An ordinary debug log exists to help engineers; an audit trail exists to reconstruct the truth for an outsider who wasn't there and doesn't trust you. That reframing dictates two properties most logging gets wrong:
- It must be append-only (immutable). A record you can quietly edit later proves nothing — an auditor's first suspicion is that inconvenient history was rewritten. So approval must append a new record, never overwrite the original proposal. Both the machine's proposal and the human's approval survive, side by side. You can see the agent proposed and the human disposed, in order.
- It must carry full provenance — enough context to replay the decision cold. That's the five fields the code makes concrete: what (the obligation), where (document + exact span), how (model + prompt/schema version), how sure (confidence + whether the citation verified), and who (signer + timestamp). Drop any one and the trail has a hole an auditor will find.
Why bother hashing the whole log into a digest, as the lab does? Because immutability enforced only by convention is a promise, not a guarantee. A running hash over every record means any silent tampering with earlier history changes the digest — a cheap, tamper-evident seal on top of the append-only discipline. It turns "we promise we didn't change it" into "here's the proof nothing changed."
The gate produces three buckets — accept, review, escalate — but "accept" still means proposed, not authoritative. A person with compliance authority reviews the proposed obligations and signs off. What makes the sign-off defensible is the audit trail: an append-only log where every event — extraction, verification result, gate decision, and human approval — is recorded with enough context to reconstruct exactly what happened and why. An auditor's question is always the same: "show me where this came from and who approved it." The log is the answer.
Good audit records share five fields: what (the obligation), where (the document + span), how (model + prompt/schema version), how sure (confidence + verification result), and who (the signer + timestamp). Make the log append-only so history cannot be quietly rewritten — that immutability is the point.
Read the left column top to bottom as the log grows over time; the right box is the integrity seal computed over the whole log.
- Record #0 (top) is the machine proposal, carrying the five provenance fields — what, where, how, whether the citation verified, and the decision. Its
byfield is empty: it's proposed, not yet authoritative. - The downward arrow is the human sign-off. Crucially it does not edit record #0 — it appends a new record.
- Record #1 (green) is that new record: it copies #0's fields, flips the decision to
APPROVED, and adds the signer and timestamp. Both records now exist side by side — the proposal and the approval are both preserved. - The right purple box is a
sha256digest over every record. Because it hashes the whole log, quietly editing any earlier record would change the digest — a cheap tamper-evident seal on top of the append-only discipline.
In short: append, never overwrite — so an auditor sees the machine's proposal and the human's approval in order, and the digest proves nothing was rewritten after the fact.
audit.py# stdlib only, no network. An append-only audit trail: every decision is recorded
# with provenance (document, span, model+prompt version, confidence) and, on sign-off,
# who approved it and when. Records are frozen so history cannot be rewritten in place.
from dataclasses import dataclass, asdict
import json, hashlib
@dataclass(frozen=True) # frozen -> a written record can't be mutated
class AuditRecord:
obligation: str
doc_id: str
span: tuple
model_version: str
prompt_version: str
confidence: float
citation_verified: bool
decision: str
signed_off_by: str = "" # empty until a human approves
signed_at: str = ""
class AuditLog:
def __init__(self):
self._records = [] # append-only; never edited in place
def append(self, rec):
self._records.append(rec)
return len(self._records) - 1 # the record's immutable index
def sign_off(self, idx, approver, at):
"""Approval does not edit the original record — it appends a NEW one."""
old = self._records[idx]
approved = AuditRecord(**{**asdict(old),
"decision": "APPROVED",
"signed_off_by": approver, "signed_at": at})
return self.append(approved)
def digest(self):
"""A hash over the whole log — any tampering changes the digest."""
blob = json.dumps([asdict(r) for r in self._records], sort_keys=True)
return hashlib.sha256(blob.encode()).hexdigest()[:12]
log = AuditLog()
i = log.append(AuditRecord(
obligation="Bank shall notify the Regulator within 30 days",
doc_id="MSA-2026-0142", span=(15, 73),
model_version="claude-x", prompt_version="oblig-extract-v3",
confidence=0.94, citation_verified=True, decision="AUTO-ACCEPT"))
log.sign_off(i, approver="a.nguyen@bank.example", at="2026-09-06T10:22:00Z")
for n, r in enumerate(log._records):
who = r.signed_off_by or "(unsigned)"
print(f"#{n} {r.decision:11} verified={r.citation_verified} by={who}")
print("log digest:", log.digest())
#0 AUTO-ACCEPT verified=True by=(unsigned)
#1 APPROVED verified=True by=a.nguyen@bank.example
log digest: 8d79848114bc # varies per run — timestamp is hashed
This is the audit trail — what makes a human sign-off defensible to an auditor. Every decision is stored with full provenance, and approval appends a new record rather than editing the old one, so history can't be quietly rewritten.
@dataclass(frozen=True)makes eachAuditRecordimmutable once written — you cannot mutate a record in place. Each carries the five audit fields: what (obligation), where (doc + span), how (model + prompt version), how sure (confidence + citation_verified), and — after sign-off — who and when.appendonly ever adds to the list and returns the record's fixed index.sign_offdoes not edit record #0; it builds a new APPROVED record (copying the old fields, adding the approver) and appends it. Both the machine proposal and the human approval survive.digesthashes the entire log, so any tampering with earlier records would change the digest — a cheap integrity check.
What the output means: Record #0 is the unsigned AUTO-ACCEPT proposal; record #1 is the human APPROVED sign-off. The proposal and the approval are both preserved — the agent proposed, the human disposed.
Try this: Call sign_off twice with different approvers and watch a third record appear. The trail grows; nothing is overwritten — which is exactly what an auditor wants to see.
7 · Evaluating the agent — accuracy, citations, abstention professional
"It looked great in the demo" is how unsafe systems reach production. A compliance agent that answered a dozen questions impressively in a meeting tells you almost nothing about the thousandth contract it hasn't seen. Faith doesn't scale and it isn't defensible; the only thing that lets you say "this is safe to trust" is a measurement against a labeled gold set — a stack of documents where a human has already recorded, per field, what the true answer is (and, importantly, where the true answer is absent). Evaluation is just grading the agent's homework against that answer key, automatically, before you let it near real filings.
The subtle part is what to measure, because a single accuracy score is dangerously misleading here. Picture a student who aces most questions but occasionally invents a confident, wrong answer on the ones they don't know. Their average might look fine — and in compliance that student is a catastrophe, because the invented answers are exactly the ones that become legal liabilities. So this agent is graded on three separate axes that map one-to-one onto the three risks from the earlier sections:
- Extraction accuracy — did it find the obligations that are really there without inventing ones that aren't? This is the familiar precision/recall pair: precision asks "of what it claimed, how much was true", recall asks "of what was true, how much did it find".
- Citation integrity — of everything it asserted, what fraction had a citation that actually verifies against the source? This is grounding, measured. In this domain it should be 1.0; anything less means ungrounded claims are leaking through.
- Abstention correctness — when it genuinely didn't know, did it abstain (flag the gap for a human) rather than silently drop the obligation or paper over it? And did it abstain only when it should, not on everything?
The last one deserves special weight, and it's why the lab treats it as a hard gate rather than just another number. A missed obligation is tolerable if and only if the agent flagged the miss — a visible gap a human can close. A silent miss (the obligation is just gone, no flag) is the dangerous failure: nobody knows to look for it. So the eval encodes the value inversion from §5 directly into the release rule — a run can have decent precision and recall and still fail outright if it ever missed something silently. This is a deliberate reversal of the usual instinct to chase the highest average score. Here, "never pretend you found everything" outranks "find everything", and the gate enforces that ranking so no future model or prompt change can quietly trade it away.
You cannot ship this on faith. Three evals, run against a labeled gold set of documents, tell you whether it's safe to trust — and they map directly onto the three risks. Extraction accuracy: did it find the obligations that are actually there, without inventing ones that aren't? Citation integrity: do the citations it returns actually verify against the source? Abstention correctness: when it's genuinely uncertain or ungrounded, does it abstain — and does it abstain only then, not on everything?
Anthropic's published guidance on evals is the frame here: build a representative test set, score deterministically where you can, and treat the safety metric as a hard gate. The lab computes all three from a tiny gold set with pure Python.
Start at the run on the left and follow the three arrows out to the three eval axes — each maps one-to-one onto a risk from the earlier sections. Then note where one of them keeps going.
- Extraction accuracy (top) — precision and recall: did it find the obligations that are really there without inventing ones that aren't?
- Citation integrity (middle) — of everything it asserted, what fraction had a citation that actually verifies? In this domain it must be
1.0; anything less means ungrounded claims are leaking through. - Abstention correctness (the amber box) — when it genuinely didn't know, did it abstain (flag the gap) rather than silently drop the obligation?
- Follow the arrow from abstention to the red box: it's a hard gate. A single silent miss fails the release outright — a decent precision/recall average can't buy it back, because a single average score would hide exactly the dangerous failure.
In short: three numbers, but they aren't equal. "Never pretend you found everything" outranks "find everything", and the hard gate enforces that ranking so no future change can quietly trade it away.
evals.py# stdlib only, no network. Scores a run against a labeled gold set on the three
# axes that matter for a compliance agent. All deterministic — no model, no judge.
GOLD = { # doc_id -> set of true obligation ids present
"A": {"o1", "o2"},
"B": {"o3"},
}
# What the agent produced: (predicted ids, ids whose citation verified, ids abstained on)
RUN = {
"A": {"predicted": {"o1", "o2"}, "verified": {"o1", "o2"}, "abstained": set()},
# doc B: found o3 but its citation did NOT verify, and it correctly abstained on it
"B": {"predicted": set(), "verified": set(), "abstained": {"o3"}},
}
def extraction_accuracy(gold, run):
tp = fp = fn = 0
for doc, truth in gold.items():
pred = run[doc]["predicted"]
tp += len(truth & pred)
fp += len(pred - truth)
fn += len(truth - pred)
precision = tp / (tp + fp) if (tp + fp) else 1.0
recall = tp / (tp + fn) if (tp + fn) else 1.0
return round(precision, 2), round(recall, 2)
def citation_integrity(run):
"""Of everything the agent PREDICTED, what fraction had a verifiable citation?"""
pred = sum(len(r["predicted"]) for r in run.values())
ok = sum(len(r["verified"]) for r in run.values())
return round(ok / pred, 2) if pred else 1.0
def abstention_correct(gold, run):
"""A missed true obligation is SAFE only if the agent abstained on it (didn't
silently drop it). Silent misses are the dangerous failure."""
for doc, truth in gold.items():
missed = truth - run[doc]["predicted"]
if missed - run[doc]["abstained"]: # missed but NOT abstained = silent
return False
return True
prec, rec = extraction_accuracy(GOLD, RUN)
print(f"precision: {prec} recall: {rec}")
print(f"citation_integrity: {citation_integrity(RUN)}")
print(f"abstention_correct (hard gate): {abstention_correct(GOLD, RUN)}")
precision: 1.0 recall: 0.67
citation_integrity: 1.0
abstention_correct (hard gate): True
These are the three evals that tell you whether the agent is safe to trust, scored against a labeled gold set — all deterministic, no model or judge needed. Each maps onto one of the three risks: accuracy, grounding, and never-silently-wrong.
extraction_accuracycomputes precision (of what it predicted, how much was true) and recall (of what was true, how much it found) from true-positive / false-positive / false-negative counts.citation_integrityasks: of everything the agent predicted, what fraction had a citation that actually verified? Here it's 1.0 — nothing ungrounded slipped through.abstention_correctis the hard gate. A missed true obligation is only safe if the agent abstained on it (flagged it for a human) rather than silently dropping it.missed - abstainedbeing non-empty means a silent miss — an automatic fail.
What the output means: Precision 1.0, recall 0.67 (it missed one), citation integrity 1.0, and abstention_correct: True — the miss is acceptable because the agent abstained on it instead of pretending it found everything.
Try this: Change doc B so the agent neither predicts o3 nor abstains on it (empty abstained set). Re-run: abstention_correct flips to False — a silent miss, the dangerous failure this gate exists to catch.
abstention_correct is False fails no matter how high precision looks.8 · Tech-lead — failure modes & hardening tech-lead
An engineer asks "does it work?" A tech lead asks "how will it hurt us, and will we see it coming?" That shift is the whole job at this level. By now the happy path is solid — extract, cite, verify, gate, sign off, log. But a compliance system doesn't fail loudly with a stack trace; it fails quietly, producing a confident, wrong obligation that looks exactly like a correct one until a regulator disagrees months later. Your responsibility as a lead is to enumerate those silent-failure paths in advance and, for each, install a control that converts the silent error into a visible one — a dropped claim, an escalation, an alert. In this domain a visible gap is recoverable; a silent wrong answer is a liability. The table below is that enumeration; here's the way to think about it so the individual rows aren't just a checklist to memorize.
Notice that every failure mode is a variation on one theme: something the system believes is trustworthy silently isn't. Group them and the pattern is easier to hold in your head:
- The model lies, plausibly — a fabricated citation (a confident quote that isn't in the document). The control is code-side citation verification from §5, which drops and escalates it. This is the failure the earlier sections were built to catch.
- The pipeline drops something on the floor — an obligation split across a chunk boundary is seen whole by neither chunk and just vanishes. The control is structural: chunk on clause boundaries with overlap, reconcile by span, and let the recall eval catch what slips.
- A trusted input goes stale — a cached document outlives an amendment, so answers cite a version that no longer exists. The control is to key the cache by document version/hash and record that version in every audit record (the caching trap from §3, now with teeth).
- The controls themselves erode over time — a new model shifts confidence and the gate quietly lets weaker claims through (threshold drift), or reviewers rubber-stamp a flooded queue until sign-off means nothing (sign-off theater). The controls: re-run the eval set on every model/prompt version, pin and log the prompt version, keep the queue small by gating conservatively, and sample-audit approvals.
The single most useful mental model to carry out of this whole case study is this: the agent is a proposal engine wrapped in controls. Its job is to draft cited obligations quickly; the controls' job is to guarantee nothing untrustworthy is ever treated as authoritative. Design it that way and the system degrades safely — under stress, load, or a model regression, it abstains and escalates more, it does not start confidently asserting nonsense. That property — failing toward "I'm not sure, ask a human" rather than toward a wrong answer — is the definition of a hardened compliance system, and it's the bar the table and rubric below hold you to.
Read it left to right. The many failure modes on the left all funnel through one middle box and come out as one kind of thing on the right — that funnel is the pattern a tech lead installs.
- The red boxes on the left are silent failures: a fabricated citation, an obligation lost at a chunk boundary, a cache that outlived a document amendment. Each looks exactly like a correct result until someone disputes it later.
- The purple Control box is the fix installed for each: verify citations in code, reconcile chunks by span, key the cache by document version. Different mechanisms, one job.
- The green box on the right is what every control produces — a visible signal: the claim is dropped, escalated to a human, or an alert fires.
- The whole point is the conversion: turning a silent error into a visible one. In this domain a flagged gap is cheap and recoverable; a silent wrong obligation flows undetected into a filing.
In short: you can't prevent every failure, but you can force each one to announce itself. A hardened compliance system fails toward "ask a human", never toward a confident wrong answer.
A lead owns the ways this system can hurt the business. Each failure mode below is a silent-error path, and each control turns the silent error into a visible one — a dropped claim, an escalation, an alert — because in compliance a visible gap is recoverable and a silent wrong answer is not.
| Failure mode | Why it's dangerous | Hardening |
|---|---|---|
| 🔴 Fabricated citation | A confident obligation quoting text not in the doc gets filed as real. | Verify every citation against the source in code (Step 5); a citation that doesn't verify drops the claim and escalates. |
| 🔴 Chunk-boundary loss | An obligation split across two chunks is seen whole by neither, so it silently vanishes. | Chunk on clause/section boundaries with overlap; reconcile duplicates by citation span; eval recall against a gold set. |
| 🟠 Stale cache | A cached document outlives an amendment; answers cite the old version. | Key the cache by document version/hash; invalidate on change; record the document version in every audit record. |
| 🟠 Threshold drift | Model or prompt changes shift confidence, so the gate lets weaker claims through. | Re-run the eval set on every model/prompt version; pin and log prompt_version; alert on abstention-rate swings. |
| 🔴 Sign-off theater | Humans rubber-stamp so many proposals that review is meaningless. | Show the citation span inline; keep the review queue small by gating conservatively; sample-audit approved records. |
| 🟠 Data leakage | Confidential contract text lands somewhere it shouldn't (logs, prompts, retention). | Minimize what's logged; access-control the review UI and audit log; follow your retention/redaction obligations — verify the regime. |
The mental model for a lead: the agent is a proposal engine wrapped in controls. Its job is to draft cited obligations fast; the controls' job is to guarantee that nothing untrustworthy is ever treated as authoritative. Get citation verification, conservative gating, the immutable audit trail, and versioned evals right, and the system degrades safely: under stress it abstains and escalates more, it does not start asserting confident nonsense.
| Dimension | Meets the bar | Above the bar (tech-lead) |
|---|---|---|
| Grounding / citations | Every obligation carries a citation (required by schema); the agent asserts only what it cites. | Citations are verified against the source in code before use; a non-verifying citation drops the claim and escalates, and citation integrity is a tracked eval. |
| No-hallucination | The agent abstains when it cannot ground a claim rather than guessing. | Abstention is a first-class, hard-gated metric; a silent miss scores worse than an abstention, matching the legal cost of a wrong obligation. |
| Auditability | Decisions and provenance are logged (doc, span, confidence, decision). | The log is append-only/immutable with model + prompt versions and human sign-off; any record can be replayed back to its source span on demand. |
| Human-in-the-loop | Uncertain results route to a human before anything is authoritative. | The queue is calibrated so reviewers see the risky cases without drowning; approvals are sampled-audited to prevent rubber-stamping. |
| Cost / caching | Large documents are cached so repeated questions don't re-pay to read them. | Cache is keyed by document version and invalidated on change; cost per reviewed obligation is a monitored metric. |
| Robustness under change | Chunking keeps obligations whole; the pipeline doesn't crash on messy input. | Every model/prompt change re-runs the eval set; threshold and abstention-rate drift trigger alerts before bad claims ship. |
Score each row 0 (missing) / 1 (meets) / 2 (above). A passing design is 9+/12 with No-hallucination and Grounding both at 2 — an agent that can emit an un-verified obligation with no abstention path is an automatic fail, because a confident wrong obligation in a compliance filing is a legal liability, not a bug.
Exercise CS3.1 — Break the citation verifier
Context: A citation that is real text but points at the wrong offsets is the subtle attack a substring check misses. Predicting the verifier's verdict before running it is how you prove you understand the control.
Your task: In gate.py, add a fourth obligation whose quote is real document text but whose start/end offsets point at the wrong location; predict the decision, then explain why checking offsets beats a substring check for a legal document.
Requirements:
- Add a misaligned-span obligation: a genuine quote at deliberately wrong offsets
- Predict the decision before running — it should be DROP+ESCALATE because the text at those offsets won't match
- Explain that verification checks position, not just "is this quote somewhere in the doc"
- Argue why position-checking is the stronger integrity guarantee for a legal document
- Connect it to the rule: citation verification is a fact checked in code, independent of model confidence
💡 Hint: Ask what the substring-anywhere check would wave through that the exact-offset check catches.
Exercise CS3.2 — Design the escalation policy
Context: The pipeline over 500 contracts produced a 4,000-item review queue and you have two analysts. Every lever you could pull trades safety against throughput — and one of them you must refuse even under deadline pressure.
Your task: Using evals.py as your measuring stick, decide what you change to shrink the queue — the confidence threshold, the chunking, the schema, or staffing — and state which change you would refuse to make even under deadline pressure, tied to the danger callout.
Requirements:
- Use the eval metrics (accuracy, citation validity, abstention) as the measuring stick, not gut feel
- Evaluate the candidate levers: raise the confidence threshold, fix chunking, tighten the schema, or add staffing
- Show that each choice trades queue size against safety and defend your pick with the eval numbers
- Name the change you would refuse: weakening citation verification or the abstain path to auto-accept more
- Tie the refusal to the invariant that a confident wrong obligation is a legal liability, not a bug
💡 Hint: The one lever you can never pull is the one that lets an un-verified obligation reach "accepted" to clear the backlog.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: In a regulated domain the usual default flips: a confident wrong answer is the worst possible outcome. Extracting clauses from contracts means the requirements read like a controls document, not a feature list.
Your task: For a compliance clause extractor (governing law, liability cap, termination notice over long contracts), write three requirements that reflect the compliance stakes.
Requirements:
- Structured output: return each field with its value and a citation (document + location) — a bare value is unusable in compliance
- Abstention: if a clause isn't present, return "not found" rather than guessing — a fabricated liability cap is worse than a blank
- Auditability: log every extraction with model version, prompt, and source span so a reviewer can reconstruct why
- Frame the design to optimize for citations and abstention, not coverage
- State the flipped default explicitly: a confident wrong answer is the worst outcome
💡 Hint: Write each requirement as if a wrong extraction becomes a legal liability — because here it does.
Show solution
- Structured output: return each field with its value and a citation (document + location) — a bare value with no source is unusable in compliance.
- Abstention: if a clause isn’t present, return “not found” rather than guessing — a fabricated liability cap is worse than a blank.
- Auditability: every extraction is logged with the model version, prompt, and source span so a reviewer can reconstruct why the agent said what it said.
Compliance flips the usual default: a confident wrong answer is the worst outcome, so the design optimizes for citations and abstention, not coverage.
Context: A 200-page contract won't fit sensibly in one call, and re-reading it per question is ruinously expensive. The ingest design has to keep extraction both grounded and affordable.
Your task: Design the ingest + retrieval for a document too big for one prompt so extraction stays grounded and affordable.
Requirements:
- Chunk on structural boundaries (sections/clauses), keeping a stable location id per chunk for citations
- Index chunks (embeddings) once and cache the parsed/chunked form so re-runs don't re-ingest
- Per field, retrieve only the few chunks most likely to contain it rather than stuffing the whole doc
- Extract from just those chunks, citing the location id
- Note that prompt caching (public Anthropic feature) further cuts the cost of a large stable instruction prefix
💡 Hint: Keep each call small for cost and accuracy, and make citations precise by carrying a stable location id per chunk.
Show solution
- Chunk the document on structural boundaries (sections/clauses), keeping a stable location id per chunk for citations.
- Index chunks (embeddings) once; cache the parsed/chunked form so re-runs don’t re-ingest.
- Per field, retrieve the few chunks most likely to contain it (e.g. search “termination notice period”) rather than stuffing the whole doc.
- Extract from just those chunks, citing the location id.
This keeps each call small (cost + accuracy) and makes citations precise. For very large stable prefixes, prompt caching (public Anthropic feature) further cuts the cost of the repeated instruction block.
Context: The heart of the agent is one disciplined call that returns machine-checkable output where every value carries a citation. A required-citation schema turns "did it cite?" from a judgment call into a boolean.
Your task: Design the extraction call so the output is machine-checkable and every value carries a citation — show the target schema and the grounding instruction.
Requirements:
- Define a per-field schema with
value, acitation(doc + location), and afoundboolean - Allow explicit nulls:
value=null,citation=null,found=falsefor an absent field - Write an instruction that extracts only from the provided sources and never infers an unwritten value
- Have the instruction reward saying "not present" so the model doesn't drift toward always producing a value
- Add post-parse checks: reject a row if
found=truebut the citation is null, or the cited location isn't among retrieved chunks - Explain that the schema makes grounding a boolean your code enforces, not a judgment call
💡 Hint: Make an obligation with no source an invalid shape the schema rejects, not a bad answer you hope to catch.
Show solution
Target schema (one object per field):
{
"field": "liability_cap",
"value": "12 months of fees" | null,
"citation": {"doc": "MSA.pdf", "loc": "§8.2"} | null,
"found": true | false
}
Instruction to the model:
"Extract each field ONLY from the provided sources. For each, return value,
the citation (doc + location) it came from, and found=true. If a field is
not present in the sources, return value=null, citation=null, found=false.
Never infer a value that is not written in the sources."
Post-parse checks: reject the row if found=true but citation is null, or if the cited
location isn’t among the retrieved chunks. The schema makes “did it cite?” a boolean your code can enforce,
not a judgment call.
Context: A required-citation schema stops the model omitting a citation but not from fabricating one. In compliance, recall of correctness beats coverage — the agent must be allowed and expected to say "not found."
Your task: Explain why "abstain and cite" beats "always answer," design the two mechanisms that operationalize it, and give the failure it prevents.
Requirements:
- State the principle: a missing field routes to a human (cheap, safe); a hallucinated field can pass silently into a legal decision (expensive)
- Mechanism 1: an explicit abstain path in the schema (
found=false) plus a prompt that rewards saying "not present" - Mechanism 2: citation verification in code — a claimed value with no verifiable source is downgraded to "needs review," never surfaced
- Note that model confidence is an opinion but citation verification is a fact — verify before confidence matters
- Name the prevented failure: the model, pressured to answer, invents a plausible clause that sounds like boilerplate but isn't in this contract
💡 Hint: The schema stops omission; only code-side verification stops fabrication — you need both, in that order.
Show solution
Principle: in compliance, recall of correctness > coverage. A missing field routes to a human (cheap, safe); a hallucinated field can pass silently into a legal decision (expensive, dangerous). So the agent must be allowed and expected to say “not found.”
Two mechanisms:
- Explicit abstain path in the schema (
found=false) plus a prompt that rewards saying “not present” — otherwise models drift toward always producing a value. - Citation verification in code: a claimed value with no verifiable source in the retrieved chunks is downgraded to “needs review,” never surfaced as an answer.
Prevents: the classic RAG failure where the model, pressured to answer, invents a plausible clause that sounds like boilerplate but isn’t in this contract.
Context: "Accept" from the gate still means proposed, not authoritative. What makes a human sign-off defensible months later is an append-only audit trail that answers the auditor's only question.
Your task: Design the review workflow: what the agent hands a human, what the human does, and what gets logged so the whole thing is auditable months later.
Requirements:
- Hand the reviewer, per field: value, citation linked to the exact source span, confidence, and any "needs review"/found=false flags surfaced first
- Let the reviewer approve, correct (storing the correction), or reject — showing low-confidence and abstained fields first
- Log an immutable record: document id + hash, model version, prompt template version, retrieved chunk ids, raw model output, reviewer decision, timestamps/user
- Ensure the trail answers the dispute question: on what basis was this field set, and who approved it
- Keep corrections — they become the best eval data for the next model change
💡 Hint: Store enough provenance that any field can be replayed to its source span and its approver on demand.
Show solution
- Agent output to reviewer: for each field — value, citation (linked to the exact source span),
confidence, and any
found=false/ “needs review” flags surfaced at the top. - Reviewer action: approve, correct (with the correction stored), or reject; low-confidence and abstained fields are shown first so human time goes where it matters.
- Audit log (immutable): document id + hash, model version, prompt template version, retrieved chunk ids, the raw model output, the reviewer’s decision, and timestamps/user.
The audit trail answers the only question that matters in a dispute: “on what basis was this field set, and who approved it?” Store corrections — they become your best eval data for the next model change.
Context: You can't deploy a compliance agent on faith. Three evals — mapped to the three risks — decide whether it's safe, and a release gate must prefer a version that abstains more over one that fabricates more.
Your task: Design the eval that proves the agent is safe to deploy in compliance, and the gate that blocks a bad release.
Requirements:
- Field accuracy: value matches gold on present fields — core correctness
- Citation validity: the cited span actually contains the value — grounding is the whole point (must be 100%)
- Abstention recall: fraction of truly-absent fields returned as not-found — catches hallucination pressure
- False-fabrication rate: present-with-value but wrong/unsupported — the dangerous failure, target ~0
- Gate: block if false-fabrication rate rises above baseline or citation validity < 100%, even if raw accuracy improved
- Build the eval set from reviewed real docs (de-identified) plus deliberately-absent-clause cases to test abstention directly
💡 Hint: Treat citation validity and fabrication as hard gates — in compliance you'd rather ship the version that abstains more.
Show solution
| Metric | Definition | Why it matters here |
|---|---|---|
| Field accuracy | value matches gold on present fields | Core correctness |
| Citation validity | cited span actually contains the value | Grounding is the whole point |
| Abstention recall | % of truly-absent fields returned as not-found | Catches hallucination pressure |
| False-fabrication rate | % present-with-value but wrong/unsupported | The dangerous failure — target ~0 |
Gate: block release if false-fabrication rate rises above the current baseline or if citation validity < 100%, even if raw accuracy improved. In compliance you would rather ship a version that abstains more than one that fabricates more. Build the eval set from reviewed real docs (de-identified) plus deliberately-absent-clause cases to test abstention directly.
✓ Checkpoint — you can move on when you can…
- State the four requirements (accuracy, auditability, no-hallucination, data handling) and why human sign-off is non-negotiable.
- Draw the pipeline: large doc → chunk+cache → extract+cite → confidence gate → human sign-off → audit log.
- Explain how prompt caching makes many questions over one big document affordable, and roughly why (write once, read cheap).
- Say why a required-citation schema plus code-side citation verification is stronger than either alone.
- Describe the three evals and why abstention correctness is the hard gate.
- Name three failure modes and the control that turns each silent error into a visible one.
A proposed obligation comes back with confidence 0.97, but its cited quote does not appear at the claimed offsets in the source document. What should the agent do, and why does the high confidence not save it?
Show answer
Why is prompt caching the right tool for a Q&A agent over a 200-page rulebook, and what one operational detail must you get right for it to stay correct?