JJoeven

Curriculum/Large Language Models

Citations

Every number, id, and quote should point at a chunk or a tool payload you can open. Invented ids are fake evidence.

intermediate20 min18 / 24

A citation is an id the UI can click: doc_12, tool_get_job, ticket_88. Prose like “according to policy” is not a citation. A URL the model invented is not a citation. A paper title that does not exist is not a citation.

Rules that survive contact with users:

  • The id must exist in the working set you sent (the chunks and tool payloads in this prompt).
  • The claim must appear (or clearly follow) in that source — faithfulness, previous lesson.
  • Missing id → do not show the claim as fact.
  • Extra invented ids → treat as a hallucination.
  • The UI should 404 a missing id, not render a fake card.

Pass the allowlist in the prompt and check in code. Models invent doc_99 because it looks like the other ids. Code membership is cheap. Trusting the model to only cite real ids is how legal gets a screenshot of a ghost document.

What to cite

Numbers, ids, dates, quotes, and policy clauses. Not every adjective. “The job failed” should point at tool_get_job. “Refunds take 5-7 days” should point at doc_policy. “I’m sorry this happened” needs no citation.

Ask the model to put ids in a JSON field cites: [...] and validate membership. Parsing cites out of parentheses is fine as a backup; a field is easier to grade.

RAG tracks will add chunk ids and offsets. The habit starts here: no naked number. If you cannot open the source, you cannot ship the claim.

Every number needs a door you can open
ClaimCite idOpen source

Invented ids are fake evidence. Check membership in code, not only in the prompt.

Every number needs a door you can open
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

First line: ok:tool_get_job because job-17 is in the tool blob. Second: id not in sources: job-99 (and also no citation id from the allowlist). Third: no citation. Three user-visible answers, three different product outcomes: show with a link; block or warn; block or mark as ungrounded.

Tighten this for production: require a cite and a grounded id. job-99 with a random doc_policy mention should still fail. The toy stops at first problem, which is enough to learn the shape.

Allowlist in two places

The prompt may list You may cite: tool_get_job, doc_policy. Code still checks membership. Prompt-only allowlists fail the moment the model invents a plausible neighbor. Code-only allowlists still work if you forget to mention the ids in the spec — but then the model may not cite at all, which check_cite treats as no citation. Do both: tell the model the ids, reject anything else.

Colliding ids: doc_1 from the handbook and doc_1 from a vendor PDF are not the same card. Prefix: hb_doc_1, vendor_doc_1. The UI 404s unknown prefixes. Do not “helpfully” fuzzy-match doc_99 to doc_9.

Store what you sent. Retrieval ranks move. If legal opens a cite two weeks later, they need the chunk text from the trace (debug bucket) or a content-addressed store. A live fetch of “current chunk 12” is not a citation. It is a new document.

Quotes: if the answer puts words in quotation marks, those words should be a substring of the source (allowing whitespace). Paraphrase is allowed for status; paraphrase of a legal clause is how 5–7 days becomes next-day. Prefer copying the clause into a schema field quote and rendering that field in the UI, not the model’s rewrite.

The allowlist is the working set this step, not the whole corpus. A doc you did not send cannot be cited. If the packer dropped doc_policy to fit the observation, the model must not cite it — and code must not have it in allowed_ids. Packer and citation checker share the same list.

UI and legal

Render citations as links to the observation. Support people will use them. Legal will use them. If the click opens empty, you shipped theater. Store the source text you actually sent (or a hash plus a fetch by id), because retrieval ranks move. Yesterday’s chunk 12 is not today’s chunk 12.

Do not let the model invent URLs. If you need a URL, it comes from your tool payload. Display your URL.

Support’s first click on a wrong answer should open the observation, not a model playground. If the card 404s, you shipped theater and you will not debug faithfulness. Build the card from the same allowlist the validator used.

What goes wrong

  • Footnotes that 404.
  • One cite blessing a whole essay that smuggles an extra amount.
  • Allowlist only in the prompt, not in code.
  • Citing the user message as if it were a policy doc.
  • Colliding ids (doc_1 reused across corpora). Prefix by corpus.

How agents use this

Schema field cites as a list of strings. Validate subset of allowlist. Validate numbers against concatenated sources. UI: buttons, not decoration. Eval: items with a wrong id must fail even if the prose is nice.

Eval slice: invent doc_99; cite a real id but a wrong amount; no cite on a number; cite the user message as doc_user. The last one is a policy choice — default fail. User text is not a policy handbook.

The packer and the citation checker must share one allowlist object. If they drift, you will 404 a real cite or accept a ghost. Pass working_set_ids from build_context into check_cite.

Tip:One real id beats three adjectives. Ask for cites: [...] and check membership in code. If you cannot open the source, you cannot ship the claim.

Check your understanding

The answer cites doc_99, which was not in the retrieved set. What is that?