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

PieceWhat it doesThe lesson
Generator (injected)a pure function (query, documents) -> claims; scripted in tests, hallucination includedgrounding 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 whereattribution is a checkable property, not a vibe
_min_window_char_spansmallest character window in a source doc covering the claim's attested termscitations 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 trailthe answer contains ONLY grounded spans; a hallucination becomes a logged DroppedClaim, not output
grounding_scoresupported / generatedthe 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 attesttrust nothing — re-verify the pipeline's own output

File map

FileRole
lab.pyyour implementation (fill the # TODOs)
solution.pyreference implementation + worked example (python solution.py)
test_lab.py28 tests: support checking, span location, offsets, multi-doc citations, enforcement, auditor, determinism
requirements.txtpytest 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_support finds the right document(s), computes coverage over content terms, respects threshold, and locates a minimal source span (not the whole document).
  • grounded_reply produces an answer where text[start:end] == citation.text for 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_citations passes on an honest reply and raises on tampered offsets, unknown document ids, and misattributed (real-but-unsupporting) documents.
  • All 28 tests pass under both lab and solution.

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's citations array carries start, end, text (offsets into the generated reply) and document_ids — exactly the Citation shape here. In Cohere's v2 API the citation carries sources referencing the document objects; our source_spans extension 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 threshold parameter 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_support for 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."