Guides & How-tos 9 min read

Give AI Answers Verifiable Citations from Meeting Transcripts

Citations only help if users can check them. Number transcript sources, require quotes, verify each claim with fuzzy matching and NLI, and link to the exact moment.

Cover reading AI build guide: Verifiable citations from meeting transcripts, with three building blocks: numbered source IDs, verbatim quote checks and timestamp deep links.

Quick answer: Give every retrieved transcript chunk a stable ID with meeting, timestamp and speaker. Number the chunks in the prompt, and require the model to return each claim with one source ID and a verbatim quote. Then verify in code: the source exists, the quote appears in it, and an NLI model says the quote's chunk supports the claim. Render citations as links to the exact moment.

A meeting assistant that says "you agreed to a ten percent discount" is only useful if the user can check it in seconds. If the citation points at a whole meeting, or worse at a meeting where the number was later changed, the user has to re-listen to an hour of audio, and trust drops after the first wrong answer.

This article covers the answer side of meeting RAG: how to make citations specific, checkable and honest. It builds on the chunk format from the companion article, Chunk Meeting Transcripts for RAG, which keeps speaker names, timestamps and meeting IDs on every chunk. Without that metadata there is nothing precise to cite.

Why are citations harder for meeting transcripts?

  • Spoken language is not quotable prose. People restart sentences, trail off and agree with "yeah, let's do that". The quote that proves a decision is often short and only meaningful next to the turn before it.
  • ASR errors end up in quotes. If the transcript says "cooper netties", a verbatim quote will too. Models tend to silently correct such errors, and then the quote no longer matches the source text.
  • Attribution is part of the claim. "Dana approved the budget" is a different claim from "the budget was approved". A citation that points to the right minute but the wrong speaker is still wrong, and speaker labels come from diarization, which can make mistakes.
  • Long transcripts, scattered evidence. A decision may be proposed at minute 5 and confirmed at minute 48. The model has to cite the confirmation, not the proposal.
  • Decisions change across meetings. A citation to April can be accurate and still misleading if May reversed it. Good citations show the date, and good answers say when a later meeting changed things.
  • Sensitive content. A citation reveals the source. If the user is not allowed to see a meeting, its chunks must never reach the prompt in the first place; the permission-aware RAG article covers that filter.

What are the core concepts?

Attribution. A claim is attributable when a source exists that a reader would accept as support. The Attributed Question Answering paper from Google Research frames this as a task with human annotation as the gold standard and an automatic metric for development.

Citation recall and precision. The ALCE benchmark paper (Gao et al.) measures citation quality with an NLI model: recall asks whether each statement is entailed by the passages it cites, and precision penalises citations that are not needed to support the statement. The authors report that even their best models lacked complete citation support about half the time on the ELI5 dataset. Code and prompts are in the ALCE repository.

Granularity. You can cite a meeting, a chunk, a speaker turn or a time span. Meeting-level citations are easy and nearly useless for checking. Chunk-level citations with a start time are a good default; span-level citations (exact quote plus timestamp) are best for decisions and numbers.

Inline versus post-hoc citation. Inline: the generator writes the citation as it answers. Post-hoc: you generate first, then search for supporting evidence for each sentence. Inline is cheaper; post-hoc catches claims the model made from memory. The design below is inline generation with post-hoc verification.

Faithfulness is not truth. A claim can be perfectly supported by a transcript that contains a mistake, or by a decision that was later reversed. Citations make answers checkable; they do not make the transcript correct.

How do you build verifiable citations, step by step?

  1. Retrieve chunks with stable IDs and metadata. Each chunk carries meeting ID, title, date, start and end time and speaker names, as in the chunking guide linked above. Apply permission filters before anything else.
  2. Order and number the sources. Label chunks S1 to Sn with title, date, timestamp and speakers in a short header. Put the strongest evidence first or last: the lost-in-the-middle study found that models use information at the start and end of a long input better than in the middle. For "current state" questions, sort by date so the latest meeting is easy to find.
  3. Ask for structured claims. Require JSON where each claim has text, exactly one source ID and a verbatim quote. One source per claim keeps verification simple; split compound claims.
  4. Check the quote. Fuzzy-match the quote inside the cited chunk. A tolerant match (rather than exact equality) absorbs small whitespace and punctuation differences, while still catching quotes the model rewrote or invented.
  5. Check support with NLI. Run an entailment model with the cited chunk as premise and the claim as hypothesis. Flag claims that are not entailed.
  6. Decide what to show. Show supported claims with their citations. For flagged claims, either drop them, regenerate with feedback, or show them with a visible warning. If nothing survives, say the meetings do not answer the question.
  7. Render deep links. Turn S2 into "Pricing sync, 12 April, 30:35, Dana Okafor" linking to the recording at that time. Users verify by clicking, not by reading a footnote.

Citation pipeline for meeting answers: retrieve chunks with IDs, number the sources, generate claims with a source and verbatim quote, check the quote, check support with NLI, and render links to the meeting and timestamp.

Steps 2 and 3 in code: the context builder returns the prompt text and a lookup table from source ID to chunk, which the verifier needs later.

def build_context(chunks):
    """chunks: retrieved dicts with text, meeting_title, date, start, speakers."""
    blocks, sources = [], {}
    for n, c in enumerate(chunks, start=1):
        sid = f"S{n}"
        sources[sid] = c
        mm, ss = divmod(int(c["start"]), 60)
        blocks.append(
            f'[{sid}] {c["meeting_title"]} | {c["date"]} | {mm:02d}:{ss:02d} | '
            f'{", ".join(c["speakers"])}\n{c["text"]}'
        )
    return "\n\n".join(blocks), sources

INSTRUCTIONS = """Answer only from the numbered sources.
Return JSON: {"claims": [{"text": "...", "source": "S1", "quote": "..."}]}
Each claim gets exactly one source. The quote must be copied word for word
from that source and must show the claim is true.
If the sources do not answer the question, return {"claims": []}."""

A valid answer for an invented example meeting looks like this. The claim is a paraphrase; the quote is copied from the chunk:

{
  "question": "What annual discount did we agree on?",
  "claims": [
    {
      "text": "The annual tier discount was cut from twelve to ten percent, starting in June.",
      "source": "S2",
      "quote": "Okay, ten percent from June, I'll update the deck."
    }
  ]
}

Steps 4 and 5: a verifier using RapidFuzz for the quote and a sentence-transformers CrossEncoder for entailment. The model ID and label order follow the nli-deberta-v3-large model card; the sentence-transformers NLI docs show the same CrossEncoder pattern.

from rapidfuzz import fuzz
from sentence_transformers import CrossEncoder

nli = CrossEncoder("cross-encoder/nli-deberta-v3-large")
LABELS = ["contradiction", "entailment", "neutral"]  # order from the model card

def verify(claims, sources, min_quote=90):
    checked = []
    for c in claims:
        src = sources.get(c["source"])
        if src is None:
            checked.append({**c, "status": "unknown_source"})
            continue
        quote_ok = fuzz.partial_ratio(c["quote"].lower(), src["text"].lower()) >= min_quote
        scores = nli.predict([(src["text"], c["text"])])[0]
        label = LABELS[int(scores.argmax())]
        ok = quote_ok and label == "entailment"
        checked.append({**c, "status": "supported" if ok else "flagged",
                        "quote_ok": quote_ok, "nli": label})
    return checked

The threshold of 90 is a starting point to tune on your data, not a recommendation from the library. NLI models are trained on written sentence pairs and have a limited input length, so keep cited chunks short, or run the check against the quote plus its surrounding turn rather than a whole window.

Are there ready-made components?

LlamaIndex has a CitationQueryEngine that splits retrieved nodes into smaller citation sources and asks the model to cite them by number; its citation_chunk_size setting controls how granular the sources are. It is a quick way to prototype numbered citations. It does not know about speakers or timestamps, so you still need your own metadata and rendering, and you still need a verifier. For questions about it, the LlamaIndex GitHub Discussions are the place to ask.

Research systems go further. Self-RAG trains a model to decide when to retrieve and to critique its own output with special reflection tokens, and the authors report better citation accuracy on long-form generation. That requires training your own model, which most meeting products will not do; the verify-after-generate approach above gets much of the benefit with off-the-shelf parts.

What are the trade-offs and pitfalls?

  • Verbatim quotes versus fluent answers. Requiring a quote for every claim makes answers stiffer and longer. Keep quotes in the citation tooltip, not in the answer text.
  • ASR errors break quote matching. If the model "fixes" a misheard word, the quote check fails on a correct claim. Fuzzy matching helps; so does showing the raw transcript line next to the claim.
  • Over-citing. Models sometimes attach three sources where one would do. ALCE-style precision catches this; one-source-per-claim prevents most of it.
  • Strict metrics undercount. Transitional sentences ("Here is what changed") need no citation. One follow-up study argues that strict citation recall undercounts such responses and proposes a more lenient variant. Decide which sentences must be cited (numbers, decisions, owners, dates) and evaluate those.
  • Wrong speaker, right moment. Verify speaker claims against chunk metadata, not only text. If the claim names a person, check that the person is among the chunk's speakers.
  • Superseded decisions. If two cited meetings disagree, the answer should say so and cite both, with dates. Hiding the older one loses history; presenting it as current is wrong. The hallucinations article covers more failure modes.
  • Latency. An NLI pass per claim adds time. Batch the pairs in one predict call and only verify claims that carry facts.
  • Timestamp drift. A citation that opens the recording 20 seconds early is annoying; two minutes off is useless. Word-level alignment, as in WhisperX, gives you start times close to the quoted words.

How do you evaluate citation quality?

  • Citation recall: share of claims entailed by their cited chunk (ALCE style).
  • Citation precision: share of citations actually needed to support the claim.
  • Quote match rate: share of quotes found in the cited chunk above your fuzzy threshold.
  • Faithfulness: the Ragas paper defines a faithfulness metric that breaks an answer into statements and checks each against the retrieved context; the Ragas library implements it. Check the docs for your installed version, as metric APIs have changed between releases.
  • Timestamp accuracy: distance between the cited start time and where the quote is actually spoken.
  • Human spot checks: a reviewer clicks each citation and answers "does this moment support the claim, said by this person?" Automatic metrics are proxies for this.

Track "abstained when it should have" too: a system that answers every question with confident citations is not a good sign. The evaluation guide covers building the question set.

How does Acta approach this?

Acta's Ask Acta answers questions across the meetings you choose using retrieval-augmented generation (the approach introduced in the RAG paper by Lewis et al.) over their transcripts, and its answers cite the meeting. Transcripts carry speaker names, from the meeting and the calendar invite, and timestamps. The decision log keeps the history when a later meeting changes or reverses a decision, with the quote and the moment it was said, which is the same principle as a span-level citation. When AI assistants connect to Acta over MCP, their answers link back to the meeting in Acta, and they only see meetings the user can see.

Frequently asked questions

Should the model cite chunk IDs or timestamps?

Short IDs (S1, S2). Models copy short IDs reliably and mangle long ones. Map IDs to meeting, timestamp and speaker in your code when rendering.

Can I trust the model to only use the sources?

No. Instructions reduce unsupported claims but do not remove them. That is why the verifier checks every claim against its cited chunk after generation.

What should happen when a claim fails verification?

Pick one policy and apply it consistently: drop the claim, regenerate with the failed claim flagged, or show it with a warning. For numbers, owners and deadlines, dropping or regenerating is safer.

How small should a citation be?

Small enough that a user can confirm it in under a minute: a speaker turn or a few turns with a start time. The companion chunking guide explains how to produce units of that size.

Found this useful? Share it with your team.