Guides & How-tos 10 min read

Multi-Meeting RAG: Finding Decisions and Commitments Across Calls

An architecture for finding decisions and commitments across many calls: scoped hybrid search, extracted records with quotes, supersession links and cited, dated answers.

Cover for an AI build guide on multi-meeting RAG, showing three building blocks: scoped hybrid search, decision records and supersession links

Quick answer: Treat each meeting as a dated source, not a loose bag of chunks. Retrieve with hybrid search (BM25 plus embeddings) limited to the meetings the user may see. After each call, extract decisions and commitments into records that point back to the transcript, and link each record to any earlier decision it changes. Answer in timeline order, citing the meeting and the moment for every claim.

Single-meeting Q&A is mostly a summarization problem. Questions like "What did we finally agree on pricing for the renewal?" or "Who promised to send the security questionnaire, and did it happen?" are different. The answer is spread across several calls, it may have changed between them, and the newest statement is not always the one that ranks highest in a vector search.

This article covers the retrieval and data-modelling side of that problem: how to find decisions and commitments across many meetings and keep their history straight. Its companion, Build Permission-Aware RAG for Sensitive Company Meetings, covers who is allowed to see which of those meetings, which matters more as soon as answers combine several calls.

Why is finding decisions across meetings hard?

Classic retrieval-augmented generation, as described in the RAG paper by Lewis et al., assumes the knowledge source is a set of passages that are each true on their own. Meeting transcripts break that assumption in several ways.

  • Decisions are rarely announced. People say "OK, let's go with eight then" after ten minutes of crosstalk. The decision is implied by several turns, not stated in one sentence.
  • ASR errors hit the words that matter. Product names, customer names and numbers ("fifteen" vs "fifty") are where speech recognition errs most, and they are exactly what keyword search depends on.
  • Ownership depends on speaker attribution. "I'll send it by Friday" is only a commitment with an owner if the transcript knows who "I" is. A diarization mistake moves the task to the wrong person.
  • Decisions change. Example: on 3 March the team agrees a 12% discount for a renewal; on 17 March the CFO reverses it to 8% with a two-year term. Both passages are semantically close to "renewal discount", so top-k similarity returns both, and the model may quote the old one.
  • Transcripts are long. A quarter of weekly calls will not fit in one prompt, and even when it does, the Lost in the Middle study found that models use information at the start and end of a long context better than information in the middle.
  • Content is sensitive. A question across "all meetings" is also a question across meetings some users were never in. Access control has to be part of retrieval, not an afterthought.

Research benchmarks reflect this. QMSum defines query-based summarization over meeting transcripts and reports it as a hard problem even within one meeting. LongMemEval tests chat assistants on multi-session reasoning, temporal reasoning and knowledge updates, the same abilities a cross-meeting assistant needs.

Which core concepts do you need?

  • Meeting as a source. Every chunk and record carries a meeting ID, the meeting date, title and attendees. The date is the anchor for every "latest" question.
  • Evidence chunks. Speaker-turn chunks with start and end timestamps, as covered in Chunk Meeting Transcripts for RAG. These are what you cite.
  • Decision records. A structured row per decision: topic, statement, who decided, status (proposed, agreed, reversed), the verbatim quote and its timestamp.
  • Commitment records. Who will do what by when, with the owner taken from the speaker label, and a status that later meetings can update.
  • Supersession links. A pointer from a new decision to the earlier one it confirms, changes or reverses. This turns separate rows into a history.
  • Two kinds of time. When something was decided (the meeting date) and when your system learned it (ingestion time). The Zep paper calls this a bi-temporal model; its open-source engine Graphiti marks outdated facts as invalid instead of deleting them, so historical questions still work.

What architecture works, step by step?

  1. Scope the search first. Resolve the set of meetings the user is allowed to see and has selected (a project, a customer, a date range). Apply it as a filter before ranking, never after generation.
  2. Index the evidence. Store speaker-turn chunks with meeting ID, date, speaker and timestamps, and index them for both BM25 and embeddings so exact names and paraphrases both match.
  3. Extract records after each meeting. Run an extraction pass that outputs decisions and commitments as JSON, each with a quote and a timestamp. Reject any record whose quote cannot be found in the transcript.
  4. Link records across meetings. For each new decision, retrieve earlier decisions on the same topic and classify the relation: new, confirms, changes or reverses. Store the link; keep the old record.
  5. Route and assemble context. "What did we decide about X?" reads the decision records first and pulls evidence chunks for the quotes. "What was said about X?" goes straight to chunks. Put context in date order and state the current status explicitly.
  6. Answer with citations. Every claim cites the meeting and timestamp. When a decision changed, show the history instead of only the last value.

Pipeline for multi-meeting RAG: scope meetings, index speaker-turn chunks, extract decisions and commitments, link changes across meetings, retrieve in timeline order, answer with citations

What does a decision record look like?

Here is an example record (invented for illustration) from the second of the two renewal calls above. The fields that matter most are the quote, the timestamp and the supersedes pointer.

{
  "id": "dec_0412",
  "topic": "acme-renewal/discount",
  "statement": "Renewal discount is 8% with a two-year term",
  "status": "agreed",
  "decided_by": "Dana Ruiz (CFO)",
  "meeting_id": "mtg_2291",
  "meeting_date": "2026-03-17",
  "quote": "Let's do eight percent, but only on a two-year term.",
  "start": "00:24:51",
  "supersedes": "dec_0377",
  "relation": "changes",
  "linked_commitments": ["com_1180"]
}

The topic key is scoped by account. Without the "acme-renewal" prefix, every discount decision for every customer would collapse into one history. The relation is one of a small fixed set so you can count and test it.

How do you retrieve across many meetings without losing the timeline?

Hybrid retrieval handles the two failure modes of meeting text: embeddings match paraphrases ("the price cut" vs "discount"), and BM25 matches the exact customer or product name. The sketch below uses rank_bm25 and sentence-transformers with the APIs shown in their READMEs, fuses the two rankings with reciprocal rank fusion, and then re-sorts the winners by meeting date.

from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer

# chunks: dicts with text, meeting_id, meeting_date (ISO), start, speaker,
# already limited to meetings this user may see and has selected
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
texts = [c["text"] for c in chunks]
bm25 = BM25Okapi([t.lower().split() for t in texts])
emb = model.encode(texts)

def ranks(scores):
    order = sorted(range(len(scores)), key=lambda i: -scores[i])
    return {i: r for r, i in enumerate(order)}

def search(query, k=20, c=60):
    lex = ranks(bm25.get_scores(query.lower().split()))
    sem = ranks(model.similarity(model.encode([query]), emb)[0].tolist())
    fused = {i: 1 / (c + lex[i]) + 1 / (c + sem[i]) for i in range(len(texts))}
    top = sorted(fused, key=fused.get, reverse=True)[:k]
    # timeline order: oldest first, so the latest statement comes last
    return sorted((chunks[i] for i in top),
                  key=lambda ch: (ch["meeting_date"], ch["start"]))

Two details matter more than the model choice. First, the scope filter is applied before scoring, so a forbidden meeting can never win a slot. Second, the final sort is by date, not by score, so the generator sees the story in order and the prompt can say "later statements override earlier ones". If precision is still low, add a cross-encoder reranker (the sentence-transformers README shows CrossEncoder) between fusion and the date sort. Pick embedding models on the MTEB benchmark, then confirm on your own transcripts.

How do you work out the current state of a decision?

Do not ask the model to infer the latest value from raw chunks every time. Walk the supersession links instead, then hand the model the chain with quotes.

def decision_histories(decisions):
    by_id = {d["id"]: d for d in decisions}
    replaced = {d["supersedes"] for d in decisions if d.get("supersedes")}
    histories = {}
    for head in (d for d in decisions if d["id"] not in replaced):
        chain = [head]
        while chain[-1].get("supersedes") in by_id:
            chain.append(by_id[chain[-1]["supersedes"]])
        # oldest first; the last element is the current state
        histories[head["topic"]] = list(reversed(chain))
    return histories

A question like "What's our discount for Acme?" then becomes: find the topic with hybrid search over the records, read its history, and answer "8% on a two-year term, agreed on 17 March (changed from 12% on 3 March)" with both citations. The same pattern works for commitments: a later "Priya sent the questionnaire yesterday" closes the earlier "I'll send it by Friday".

For broad questions ("What were the main themes across all customer calls this quarter?"), record lookup is too narrow. Graph-based approaches such as Microsoft GraphRAG build entity graphs and community summaries for that kind of global question; its README warns that indexing can be expensive and advises starting small.

What are the trade-offs and pitfalls?

  • Extract at ingest or at query time. Extracting once per meeting makes queries cheap and lets you show a decision log, but extraction errors persist. Keep the quote and timestamp on every record and re-run extraction when you change the model or prompt.
  • Over-linking. Two "pricing" decisions for different customers must not be linked. Scope topic keys by account or project, and require the linker to cite both quotes.
  • Tentative language. "We'll probably go with Postgres" is a proposal, not a decision. Store a proposed status rather than forcing a yes or no.
  • Relative dates. Resolve "next Friday" against the meeting date, not the date you ingested the transcript.
  • Missing owners. "Someone should check the contract" is an unassigned commitment. Do not guess an owner from who spoke nearby.
  • Context order. Long, unordered context invites stale answers. Keep it short, dated and ordered.
  • Links that leak. A decision in a meeting you can see may supersede one in a meeting you cannot. The link itself, and any summary of the old decision, must respect the same permissions as the meetings. The permission-aware RAG guide covers this.

How do you evaluate multi-meeting RAG?

Build a test set from real (consented) meeting series: each item has a question, the meetings that contain the evidence, the expected current answer and the expected history. Then measure:

  • Meeting-level recall@k: did retrieval return every meeting where the decision was made or changed, not just one of them?
  • Context precision and recall: available in Ragas; see its context recall definition.
  • Current-state accuracy and stale-answer rate: how often the answer gives the latest decision, and how often it confidently gives a superseded one. Track the second separately; it is the error users notice.
  • Link precision and recall: supersession links compared with human labels.
  • Commitment extraction: precision, recall and owner accuracy, which also exposes diarization errors.
  • Citation correctness: does each cited timestamp actually contain the quoted claim?

LongMemEval's categories (knowledge updates, temporal reasoning, abstention) are a good template for question types. More detail on metrics is in Evaluate a Meeting RAG System.

How does Acta approach it?

Acta transcribes meetings after they end, with speaker names taken from the meeting and the calendar invite. Ask Acta answers questions across the meetings and files you choose, using RAG over their transcripts, and its answers cite the meeting. The decision log tracks decisions across meetings with who decided, the owner and linked action items, and keeps the history when a later meeting changes or reverses a decision, with the quote and the moment it was said. Action items from all meetings are tracked with owners and due dates. See asking questions across meetings for how it looks to a user.

Frequently asked questions

Can I skip retrieval and put every transcript in a long context window?

For a handful of short meetings, yes. Beyond that you pay for every token on every question, and long-context studies show that models use information in the middle of the prompt less reliably. Retrieval plus a dated, ordered context is cheaper and easier to cite.

Do I need a knowledge graph?

Not to start. A table of decision and commitment records with supersession links answers most "what did we decide" questions. Consider a graph when you need many entity types and relations, or global questions across a large corpus.

How do I handle a decision that was reversed and later reinstated?

Add a new record that links to the reversal with the relation "changes". The chain then shows all three steps with quotes, and the current state is simply the last record.

How do I find commitments people never closed?

Keep a status on each commitment and update it only from evidence in a later meeting or an explicit user action. Anything still open past its due date is your follow-up list.

For community help with retrieval pipelines, the LlamaIndex GitHub Discussions are active.

Found this useful? Share it with your team.