Lab 02 — Grounded Generation with Inline Citations
Phase 31 · Lab 02 · Phase README · Warmup
The problem
Naive RAG is "stuff the retrieved chunks into the prompt and hope": the documents influence the answer, but nothing in the output tells you which document supports which claim — or whether a given sentence is supported at all. For a consumer chatbot that's a quality problem; for an enterprise assistant answering from HR policy, contracts, or financial filings it's a dealbreaker, because the failure mode is a fluent, confident, unattributable sentence a customer acts on.
Cohere's Chat API made grounded generation with inline citations its signature: pass
documents alongside the message and the response carries a citations list — for each span of
the generated answer, character offsets {start, end, text} into the answer plus the
document_ids that support it. The answer is auditable by construction: a reviewer (or an
automated guardrail, or Phase 11's
eval harness) can jump from any claim to the exact passage that attests it.
This lab builds that grounding layer — and the enforcement Cohere's marketing implies but your production system must actually implement: a claim no document supports never reaches the user.
What you build
| Piece | What it does | The lesson |
|---|---|---|
Generator (injected) | a pure function (query, documents) -> claims; scripted in tests, hallucination included | grounding is downstream of generation — you verify the model, you don't trust it |
claim_support(claim, docs, threshold) | which documents attest a claim (term coverage ≥ threshold) and the minimal source span where | attribution is a checkable property, not a vibe |
_min_window_char_span | smallest character window in a source doc covering the claim's attested terms | citations point at passages, so offsets must be exact |
grounded_reply(...) | assemble supported claims into the answer; emit Cohere-shaped {start, end, text, document_ids} citations; drop the rest with an audit trail | the answer contains ONLY grounded spans; a hallucination becomes a logged DroppedClaim, not output |
grounding_score | supported / generated | the faithfulness number an eval harness tracks per model/prompt/corpus |
verify_citations(reply, docs) | independent auditor: offsets honest, every cited doc really supports its span, source spans really attest | trust nothing — re-verify the pipeline's own output |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 28 tests: support checking, span location, offsets, multi-doc citations, enforcement, auditor, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # worked example
Success criteria
-
claim_supportfinds the right document(s), computes coverage over content terms, respectsthreshold, and locates a minimal source span (not the whole document). -
grounded_replyproduces an answer wheretext[start:end] == citation.textfor every citation, and every character outside a citation is a joining space — zero ungrounded content. - A claim supported by two documents cites both ids (sorted); removing one document from the input changes the citations and drops the claim whose only supporter is gone.
- The planted hallucination is dropped with an audit trail (reason, best coverage, closest document) — and the zero-supporting-docs case yields an empty answer, not a confident one.
-
grounding_score= supported/generated (1.0 for an empty generation — an empty answer asserts nothing false). -
verify_citationspasses on an honest reply and raises on tampered offsets, unknown document ids, and misattributed (real-but-unsupporting) documents. -
All 28 tests pass under both
labandsolution.
How this maps to the real stack
- Cohere Chat with
documents: you pass a list of documents (each with an id and text fields); the model generates with them in context, and the response'scitationsarray carriesstart,end,text(offsets into the generated reply) anddocument_ids— exactly theCitationshape here. In Cohere's v2 API the citation carriessourcesreferencing the document objects; oursource_spansextension plays the same auditability role at character granularity. - How Cohere really does it: the Command R model family was trained to emit grounded spans and citation markers as part of its RAG fine-tuning — the citations come out of the decoder, not from a lexical post-processor. Our miniature inverts that (generate, then verify lexically) because a trained citation head isn't reproducible in stdlib — but the invariants (offsets index the answer, ids name real supporters) are identical, and a production system should verify model-emitted citations with exactly this kind of independent checker anyway, because a model trained to cite can still miscite.
- Citation modes: Cohere exposes fast vs accurate citation generation (a latency/quality
knob); the
thresholdparameter here is the analogous strictness knob — 1.0 demands every content term be attested, lower values admit paraphrase-ish partial support. - The enforcement layer is yours: Cohere returns citations; it does not refuse to emit an uncited sentence. Dropping unsupported claims (or flagging them for human review) is the deployment decision this lab hard-codes, the same "contextual grounding" idea Bedrock Guardrails ships (Phase 24) and the faithfulness metric Phase 11 scores.
Limits of the miniature. Term-coverage support is lexical: it cannot see that "credentials are rotated quarterly" supports "passwords change every three months," and it can be fooled by a document that contains all the words of a claim while asserting the opposite (negation). Real faithfulness checking uses an NLI model or an LLM judge for entailment; the lab's contribution is the pipeline contract — injected generator, checkable support, exact offsets, enforced grounding, independent audit — which stays the same when you swap the lexical checker for a learned one.
Extensions (your own machine)
- Swap
claim_supportfor an NLI-based entailment checker (e.g. a small cross-encoder NLI model) behind the same signature; measure how often lexical and entailment support disagree. - Add a
mode="flag"variant that keeps unsupported claims in the answer wrapped in[UNVERIFIED: ...]markers instead of dropping them — the human-review workflow. - Compute citation precision/recall against a hand-labeled set: of the spans cited, how many are truly supported (precision); of the claims that could be supported, how many got citations (recall).
- Stream it: emit the answer claim-by-claim with citations attached per chunk, mirroring how Cohere streams citation events after text events.
Interview / resume signal
"Built Cohere-style grounded generation as a verifiable pipeline: an injected (hallucinating) generator, per-claim support checking with minimal-source-span location, Cohere's exact citation contract ({start, end, text, document_ids} into the answer), hard enforcement that drops unattributable claims with an audit trail, a faithfulness score, and an independent citation auditor that catches tampered offsets and misattributed sources."