QAO // QUANT AGENTIC ORCHESTRATION
RAG · MODULE 05/12

Institutional-grade financial RAG

Structure-aware chunking, hybrid search with reranking, the text/numbers split, and RAGAS evaluation.

READ ~30 MIN LEVEL INTERMEDIATE-ADVANCED EDITION JUL 2026

Learning objectives

Module 4 left the institutional documentary sources (EDGAR, transcripts, regulatory texts) ingested and normalized. This module turns them into a comprehension system: a Retrieval-Augmented Generation pipeline (RAG — retrieval-augmented generation: document fragments are retrieved and injected as context into the LLM to ground its answer) at the evidence standard of a research or risk desk. The central thesis, backed with data in every section: in finance structure is the pipeline — generic chunking, retrieval, and evaluation destroy exactly the signal the analyst needs, and the difference between 19% and 83.7% accuracy is engineering, not the model. (API EvangelistAPI Evangelist)


5.1 Why tutorial RAG fails in finance

Tutorial RAG — load a PDF, RecursiveCharacterTextSplitter with 1,000 tokens, generic embeddings, top-k by cosine similarity — rests on three assumptions that the financial document systematically violates. First, that the text is flat prose: a 10-K is a structured artifact (Items, notes, tables, XBRL tags), and fixed-size chunks "flatten structured disclosures into arbitrary blocks of text", splitting risk factors apart and separating every figure from the section that gives it meaning. (🦜️🔗 LangChain) Second, that semantic similarity is enough: asking about year-end inventory can retrieve prose from the wrong period "because the embedding looked close enough" — flat RAG "confuses fiscal periods, misroutes balance-sheet facts, or cites narrative that does not support the answer's figure". (csdn.net) Third, that retrieving the right text implies answering correctly: the financial answer demands multi-step numerical reasoning over table and text, where the LLM alone is structurally weak. (getvocal.ai)

5.1.1 Benchmark evidence: what they measure and why they matter to the buy-side

The empirical reference is FinanceBench (Patronus AI, arXiv 2311.11944, Nov 2023): 10,231 expert-written questions on SEC filings from 40 U.S. companies, with an open 150-question subset and binary scoring with a ±2.5% tolerance on numeric answers. (API EvangelistAPI Evangelist) Its original results are the X-ray of naive RAG: GPT-4-Turbo closed-book gets 9% right; with a vector store shared across all documents, 19% — that is, it "answered incorrectly or refused to answer 81% of the questions"; with a vector store per document, 50%; with the full filing in context (long-context), 76%; and with the evidence pages provided (oracle), 85%. (API EvangelistAPI Evangelist) In 2026, an independent evaluation on the same benchmark (average of 10 runs with an LLM judge) places exhaustive agentic RAG — hybrid retrieval, rerank, structure navigation, and tools — at 83.7%, above full-context with 1M-token models (76.0%). (langchain.js) The 19% → 83.7% trajectory is the quantified demonstration that pipeline engineering moves the needle in document comprehension, the domain where the institutional ROI of LLMs is proven.

FinanceBench accuracy by RAG pipeline architecture

Source: original FinanceBench results (arXiv 2311.11944, 2023) (API EvangelistAPI Evangelist) and independent agentic-RAG evaluation (2026, average of 10 runs) (langchain.js) ; own elaboration, data as of July 2026.

The remaining benchmarks bound the problem from the numerical-reasoning side. FinQA (arXiv 2109.00122): 8,281 QA pairs annotated by eleven professionals on S&P 500 earnings reports, with heterogeneous evidence (table + text) and a numerical reasoning program; GPT-4 reaches 62.87 versus 91.16 for the human expert. (Model Agent Platform) ConvFinQA (arXiv 2210.03849) extends it to 3,892 multi-turn conversations with anaphora over figures: GPT-4 achieves 76.48 versus 89.44 human, and errors cascade — if one turn fails, "there is very little chance that subsequent turns will be correct". (🦜️🔗 LangChain) TAT-QA (ACL 2021): 16,552 hybrid tabular-textual questions (42% arithmetic); the paper's best model, TAGOP, reaches 58.0 F1 versus 90.8 F1 human. (博客园)

Table 5.1 — Financial QA and numerical-reasoning benchmarks: size, best documented result, and gap versus the human expert

Benchmark Size What it measures Best documented result Human expert Gap
FinanceBench (arXiv 2311.11944, 2023) 10,231 ques. (150 open) Open-book QA over SEC filings; figure accuracy with citation 83.7% (agentic RAG, 2026); 19% (naive RAG, 2023) ~100% (expert gold answers) Naive RAG fails 81%; agentic still leaves ~16 pp
FinQA (arXiv 2109.00122, 2021) 8,281 QA Multi-step numerical reasoning over table + text GPT-4: 62.87% accuracy (arXiv 2305.05862, 2023) 91.16% ~28 pp
ConvFinQA (arXiv 2210.03849, 2022) 14,115 ques. / 3,892 conv. Multi-turn numeric with anaphora over figures GPT-4: 76.48% (arXiv 2305.05862, 2023) 89.44% ~13 pp; error cascade across turns
TAT-QA (ACL 2021.acl-long.254, 2021) 16,552 QA Hybrid tabular + textual QA; 42% arithmetic TAGOP: 58.0 F1 90.8 F1 ~33 pp

Interpretation. The table orders the benchmarks by the skill they certify, and that reading is the one that matters to a desk. FinanceBench certifies documentary fidelity — the right figure, in the right filing, from the right period: the exact task of the research analyst or of the risk team validating a disclosure; that is why it is the template for the proprietary golden dataset (§5.5). FinQA and TAT-QA certify computation: the ~28-33 pp gap is not closed by better retrieval but by delegating calculation to tools (§5.4) — hence XBRL-Agent's +42 points with RAG + symbolic calculator. (getvocal.ai) ConvFinQA certifies conversational state, critical in analyst copilots where "and what about the following year?" is the norm; its error cascade demands evaluating the full conversation, not the isolated turn. (🦜️🔗 LangChain) Management conclusion: no benchmark shows the problem solved; the human-machine gap persists and justifies the human veto and the thresholds of §5.5.

Risk note — figure hallucination. The dangerous failure mode is not abstention but the confidently invented figure. In FinanceBench, Llama2 with a vector store produced incorrect answers 70% of the time with a shared store and 54% with a per-document store, versus GPT-4-Turbo, which tended to abstain (67%/38% failed-to-answer) with only 13%/11% incorrect; Patronus AI concludes that "all models examined exhibit weaknesses, such as hallucinations, that limit their suitability for enterprise use". (API EvangelistAPI Evangelist) A 2024 study cited by FailSafeQA estimates hallucination in up to 41% of financial queries (博客园) , and in filings extraction the fabricated figure accounts for ~10% of failures, with another 15% scale errors (reading "millions" as "thousands"). (🦜️🔗 LangChain) For a risk manager, "NOT_AVAILABLE" is a valid operational result; a hallucinated figure in a risk report is an incident. The pipeline must reward abstention and verify every figure against the source (§5.4), not just maximize coverage.


EXPANDE

In plain terms: from 19% to 83.7% without changing the model

FinanceBench does something uncomfortable for anyone selling models: it keeps the exam (150 questions on SEC filings) and the model fixed, and only changes the engineering around them. The GPT-4-Turbo that scores 19% with a shared vector store is exactly the same one that scores 50% with a per-document store. The difference is not intelligence: it is filing discipline.

Think of it as a research room. Tutorial RAG is an intern who shreds the 10-Ks of 40 companies into 1,000-token strips and tosses them all into the same sack; asked for a figure, he pulls out a handful of "similar-looking" strips — sometimes from the wrong company, almost always from the wrong period. The institutional pipeline is the analyst who files each filing in its folder, with dividers per Item, an index card stating company, year, and section, a calculator on the desk, and the obligation to cross-check every figure before writing it down. The module's entire 9% → 19% → 50% → 76% → 83.7% trajectory is that filing discipline turned into code. That is why the course insists: in document comprehension, the lever is the pipeline, not the model.

5.2 Structure-aware chunking

Chunking is the number-one lever of financial RAG because it decides which units of meaning survive indexing. The operating rule: the document's structure marks the boundaries; size only subdivides within them. LangChain offers the double-split pattern — first structural (for example, MarkdownHeaderTextSplitter, which preserves header lineage as metadata) and then by size with RecursiveCharacterTextSplitter, inheriting the structural metadata in each sub-chunk. (stackademic.com)

5.2.1 10-Ks by Items, earnings calls by turns, regulatory texts with hierarchy

10-K by regulatory Items. The semantic unit of a 10-K is the Item (1 Business, 1A Risk Factors, 7 MD&A, 8 Financial Statements): an analyst reads by "selecting the filing and period, inspecting the table of contents, and navigating to Item 1A, 7, or 8", and retrieval must mimic that path. (🦜️🔗 LangChain) Each chunk carries a lineage section_path ("10-K > Item 1A > Regulatory Risk > EU Operations") that enables section-precise citation and filtering before searching. (🦜️🔗 LangChain)

import re
from langchain_core.documents import Document

ITEM_RE = re.compile(r"^\s*Item\s+(\d+[A-Z]?)\.?\s+(.+)$", re.IGNORECASE | re.MULTILINE)

def chunk_10k_by_items(text: str, ticker: str, fiscal_year: int) -> list[Document]:
    """Split a 10-K into chunks by regulatory Item, with lineage metadata."""
    matches = list(ITEM_RE.finditer(text))
    docs = []
    for i, m in enumerate(matches):
        start = m.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
        docs.append(Document(
            page_content=text[start:end].strip(),
            metadata={
                "ticker": ticker,
                "fiscal_year": fiscal_year,
                "filing_type": "10-K",
                "item": f"Item {m.group(1).upper()}",
                "item_title": m.group(2).strip(),
                "section_path": f"10-K > Item {m.group(1).upper()} > {m.group(2).strip()}",
            },
        ))
    return docs
# Then: re-split long Items with RecursiveCharacterTextSplitter,
# PROPAGATING the metadata to every sub-chunk.

Earnings calls by speaker turns. Transcripts already come with turn boundaries and speaker names (XML format), and the financial NLP literature validates the turn as the natural processing unit, dropping operator turns and turns under 10 tokens (signal-free greetings). (LangChain Forum) A sector practitioner (FinTech Studios, Feb 2026) reports a 34% improvement in retrieval precision for earnings queries with turn-based chunking versus fixed 512-token windows — a vendor figure with no published methodology, but whose direction (structure > fixed window) is corroborated by independent sources. (🦜️🔗 LangChain)

from langchain_core.documents import Document

def chunk_earnings_call(turns: list[dict], ticker: str, quarter: str) -> list[Document]:
    """turns = [{'speaker': 'Jamie Dimon', 'role': 'CEO', 'section': 'prepared_remarks',
                'text': '...'}, ...]"""
    docs = []
    for t in turns:
        if t["role"] == "Operator" or len(t["text"].split()) < 10:
            continue  # noise with no signal (cf. arXiv 1906.02868)
        docs.append(Document(
            page_content=f"[{t['speaker']}{t['role']}] {t['text']}",
            metadata={
                "ticker": ticker, "quarter": quarter,
                "speaker": t["speaker"], "role": t["role"],
                "section": t["section"],  # prepared_remarks | qa
            },
        ))
    return docs

Regulatory texts. Circulars, technical standards, and rulebooks have article hierarchies and cross-references ("pursuant to article 12.3.b"). Chunking must respect the chapter-section-article hierarchy and resolve cross-references by materializing the referenced text or its identifier in metadata; otherwise, a chunk that says "the provisions of the previous section shall apply" reaches the LLM without the content that completes it. The following table summarizes the technique by document type.

Table 5.2 — Structure-aware chunking by financial document type: unit, metadata, technique, and the risk of generic chunking

Document type Chunking unit Required metadata Technique / splitter Risk of generic chunking
10-K / 10-Q (EDGAR) Regulatory Item (1, 1A, 7, 8) and titled subsection ticker, fiscal_year, item, section_path HTML/XBRL parsing + Item regex; recursive re-split propagating metadata Split risk factor; figure without the section that contextualizes it (🦜️🔗 LangChain)
Earnings call Speaker turn (≥10 tokens, no operator) speaker, role, section (prepared_remarks/qa), quarter Turn segmentation of the XML transcript Q&A mixed with prepared remarks: the +34% retrieval precision contributed by turn-based chunking is lost (🦜️🔗 LangChain)
Regulatory text Article / hierarchical provision regulation, article_path, resolved cross_refs Header split (MarkdownHeaderTextSplitter) + reference resolution Orphaned cross-references; incomplete rule in the context (stackademic.com)
Financial tables XBRL row/fact as a first-class unit concept, period, currency, scale Docling/unstructured: rows and XBRL facts as nodes, never "word soup" (csdn.net) Numbers separated from header and unit; scale errors (🦜️🔗 LangChain)
Research / sell-side notes Thematic paragraph with header analyst, date, rating, sector Double split: headers → recursive 400-1,000 tokens Thesis and conditions mixed across chunks

Interpretation. The table makes explicit that there is no "best financial chunk_size": there is a best unit per document, and size is a secondary parameter that only operates within it. Three cross-cutting decisions follow. First, metadata is not decoration: ticker, fiscal_year, and section_path enable filtering prior to similarity (§5.3) and auditable citation; a chunk without lineage cannot be defended before compliance. Second, tables demand leaving the text paradigm — the row with concept-period-currency is the unit — which connects to the text/numbers split of §5.4. (csdn.net) Third, the cost is asymmetric: parsing Items or turns is ~50 lines with measurable benefits (the reported +34%), while ignoring structure produces silent failures that no aggregate embedding metric reveals. A practical acceptance criterion: if, on inspecting ten random chunks, any of them cannot be attributed to a concrete section, the chunking is not institutional grade.


EXPANDE

Common pitfall: tuning chunk_size instead of changing the unit

The most repeated lab mistake: the team spends two weeks testing 500, 800, 1,200 tokens with RecursiveCharacterTextSplitter, gains half a point of recall, and concludes that "chunking is optimized". It is not: the failure is structural and no size fixes it. A risk factor split in half remains split at 800 and at 1,200 tokens; a figure separated from the section that contextualizes it remains orphaned; Q&A mixed with the prepared remarks remains contaminated. The second parameter is being fine-tuned while the first one — the chunking unit — is wrongly chosen.

The module's Table 5.2 says it plainly: there is no "best financial chunk_size", there is a best unit per document (Item, speaker turn, article, XBRL row), and size only subdivides within it. The cost is asymmetric in favor of doing it right: parsing Items or turns is ~50 lines with measurable benefits (the +34% retrieval precision reported on calls), while naive chunking produces silent failures that no aggregate embedding metric reveals. Apply the module's acceptance criterion before touching any parameter: if, on inspecting ten random chunks, any of them cannot be attributed to a concrete section, the problem is not the size.

5.3 Advanced retrieval

With the corpus chunked by structure, the retrieval layer decides which candidates reach the LLM. The 2026 production stack has four ordered components: metadata filter → hybrid search → reranking → parent-context expansion, all pluggable as a BaseRetriever into LangChain v1's canonical chain, create_retrieval_chain(retriever, create_stuff_documents_chain(llm, prompt)). (Github)

5.3.1 Hybrid search, reranking, metadata filtering, and parent context

Metadata filtering first. The most common financial RAG error is retrieving the right figure from the wrong period; the first line of defense is narrowing the subset before vector similarity with the lineage fields from chunking. (LangChain Forum)

retriever = vectorstore.as_retriever(search_kwargs={
    "k": 6,
    "filter": {
        "$and": [
            {"ticker": {"$eq": "JPM"}},
            {"fiscal_year": {"$gte": 2023}},
            {"item": {"$in": ["Item 7", "Item 8"]}},
        ]
    },
})

Hybrid BM25 + dense search. Dense search fails on exact identifiers (tickers, ISINs, instrument names, taxonomy codes) and BM25 fails on semantics; combining them with Reciprocal Rank Fusion (RRF — a fusion that scores each document by the sum of the inverses of its ranks in each list) is the production standard: 2026 vendor figures report 91% recall@10 versus 78% (dense only) and 65% (BM25 only), with ~6 ms of fusion overhead. (LangChain) In LangChain, EnsembleRetriever implements RRF with configurable weights; the typical starting point is 0.6 semantic / 0.4 keyword, raising the BM25 weight when queries contain exact terms such as tickers or codes. (🦜️🔗 LangChain)

from langchain_classic.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

bm25 = BM25Retriever.from_documents(chunks); bm25.k = 10
dense = vectorstore.as_retriever(search_kwargs={"k": 10})
hybrid = EnsembleRetriever(retrievers=[dense, bm25], weights=[0.6, 0.4])
# In finance, tickers/ISIN/instrument names argue for raising the BM25 weight.

The embedding is also a domain decision: in a financial retrieval benchmark (HR@5), finance-adapted embeddings reach 88% versus 86% for OpenAI text-embedding-3-large, 85% for Cohere embed-english-v3.0, and 84% for Google text-embedding-004, (CSDN博客) and replacing ada-002 with financial embeddings on FinanceBench adds +8 accuracy points. (Towards AI) Budgeting for a domain embedding (e.g., voyage-finance-2) is among the cheapest improvements in the pipeline.

Reranking. Hybrid retrieval returns candidates; the reranker reorders them by reading the full query and document. Cohere Rerank 3 accepts up to 4,000 input tokens, which allows reranking with full-document context instead of the isolated chunk, recovering relevance that chunking destroyed. (🦜️🔗 LangChain) It integrates as a compressor over any base retriever:

from langchain_classic.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank

reranker = CohereRerank(model="rerank-v3.5", top_n=5)
final_retriever = ContextualCompressionRetriever(
    base_retriever=hybrid,      # BM25+dense ensemble, or multi-query
    base_compressor=reranker,
)

Parent context. The precise-embeddings vs. wide-context dilemma remains: ParentDocumentRetriever resolves it by indexing small child chunks (~400 tokens, precise embeddings) but returning the parent document (~2,000 tokens) on retrieval. (arXiv.org) Financial application: the child is the paragraph with the figure; the parent, the Item's full subsection (the entire "Liquidity and Capital Resources" note), so that the LLM receives the figure with its management narrative.

from langchain_classic.retrievers import ParentDocumentRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.storage import InMemoryStore

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=InMemoryStore(),
    child_splitter=RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50),
    parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200),
)
retriever.add_documents(filing_docs)  # indexes children in vectorstore, parents in docstore

MMR and multi-query. When the top-k results are near-duplicates (typical in repetitive annual filings), Maximal Marginal Relevance (MMR) balances relevance and diversity over a pool of fetch_k candidates, with lambda_mult between 1.0 (pure similarity) and 0.0 (pure diversity): (🦜️🔗 LangChain)

\[ \mathrm{MMR}(d) = \arg\max_{d_i \in R \setminus S} \Big[ \lambda \cdot \mathrm{sim}(d_i, q) - (1 - \lambda) \cdot \max_{d_j \in S} \mathrm{sim}(d_i, d_j) \Big] \]

where \(R\) is the candidate set, \(S\) the set of already-selected documents, \(q\) the query, and \(\lambda\) the weight of relevance versus diversity. Multi-query retrieval generates several paraphrases of the query with an LLM, retrieves for each one, and fuses, improving recall and robustness to phrasing; it is a standard production pattern alongside MMR, at the cost of additional LLM calls. (🦜️🔗 LangChain) LLM-based contextual compression improves precision at the cost of ~doubling latency: reserve it for high-value queries. (🦜️🔗 LangChain)

The resulting full pipeline is summarized in the following diagram.

Diagrama

EXPANDE

Worked example: the retrieval funnel, with numbers

Take the module's applied case — 40 covered banks, a corpus indexed by Items and turns — and follow a real query through the funnel: "What was JPM's CET1 ratio in 2024 and what did the CFO say about it?". The figures are illustrative but true to the case's scale:

Stage Action Candidates
0. Corpus 40 banks × filings and calls, structure-chunked 48,000 chunks
1. Metadata filter ticker=JPM, fiscal_year=2024 1,150 chunks
2. Hybrid RRF 0.6/0.4 dense k=10 + BM25 k=10, fused 20 unique candidates
3. CohereRerank top_n=5 reorders reading query + document 5 child chunks
4. ParentDocumentRetriever child 400 tok → parent 2,000 tok ~10,000 tokens of context
5. create_retrieval_chain citation prompt doc+section+period grounded answer

Three readings of these numbers. First, the metadata filter does the heavy lifting: it shrinks the search space by 98% before spending a single embedding, and it is free — that is why it goes first in the stack. Second, the hybrid costs ~6 ms of RRF fusion and is the stage that captures "CET1", an exact identifier where dense search falters: the module's reference is 91% recall@10 versus 78% dense-only. Third, the reranker and the parent context add no new candidates: they reorder and re-contextualize the ones you already have, recovering the management narrative that the child chunking sacrificed for precision.

Recall@10 by retrieval strategy and HR@5 by embedding family

Source: 2026 vendor figures and the financial retrieval benchmark cited in §5.3.1 · Own elaboration as of Jul 29, 2026.

5.4 The text/numbers split

5.4.1 Principle: text goes to the vector store, figures to structured tools

The embedding of a number does not preserve its value: it encodes distributional context, not magnitude. Hence the first-order rule of financial RAG — the golden data rule from Module 4 (§4.4.1) applied to retrieval —: prose goes to the vector store with section lineage; figures go to structured bases (SEC XBRL facts, SQL tables) accessible via tool calling; and every cited figure must exist verbatim in the source. Three pieces of evidence support it. Tables "are where the exact figures live", and standard chunking "turns tables into word soup and separates numbers from the sections that give them meaning": the documented solution (Red Hat, Jul 2026) keeps rows and XBRL facts as first-class nodes with concept, period, and currency metadata. (csdn.net) XBRL-Agent quantifies the benefit: RAG + symbolic calculators produced +17 points on taxonomy queries and +42 on numerical reasoning versus base LLMs, which "hallucinate or misinterpret financial content" over XBRL. (getvocal.ai) And scale errors (millions read as thousands) are 15% of extraction failures: an explicit scale field and reading from the structured source eliminate them by construction. (🦜️🔗 LangChain)

The resulting architecture splits the query into two planes:

Diagrama

Verbatim verification is code, not prompting: a second pass checks that every figure in the output appears literally in the cited excerpt (the two-step verification pattern; the dominant failure mode of LLM reasoning is arithmetic, not logic, as PAL demonstrated: 16 of 25 chains of thought were identical except for the numbers; the full development of this evidence is in §6.2). (Financial Wisdom TV)

import re

def cifras_en_respuesta(texto: str) -> list[str]:
    """Extract numeric literals from an answer (supports scale and separators)."""
    return re.findall(r"\$?\d[\d,.]*\s*(?:million|billion|thousand|%)?", texto)

def verificacion_verbatim(respuesta: str, contextos: list[str]) -> dict:
    """Every cited figure must exist VERBATIM in some retrieved context."""
    corpus = " ".join(contextos).replace(",", "")
    no_soportadas = [
        c for c in cifras_en_respuesta(respuesta)
        if c.replace(",", "").strip() not in corpus
    ]
    return {
        "ok": not no_soportadas,
        "cifras_no_soportadas": no_soportadas,
        # In production: if ok=False -> abstain ("NOT_AVAILABLE")
        # or route to a human review queue, NEVER publish the figure.
    }

In institutional practice — Kensho (S&P Global). Kensho, S&P Global's AI division, operates a multi-agent framework on LangGraph for financial data retrieval, and its evaluation practice is the sector reference: multi-stage evaluation with exact match on routing and tool calling — not LLM judgment over the final output — because a generative judgment over a risk figure is circular. (🦜️🔗 LangChain) The industry-documented pipeline replicates that discipline: FinanceBench-style golden dataset → trajectory evaluation (the right tool with the right arguments?) → numeric exact match → CI replay. It is the same principle as the text/numbers split: what is deterministically verifiable is not evaluated with an LLM. Grounding: the property that every statement in the output is traceable to a retrieved source with provenance (document, section, period); it is not declared in the prompt — it is demonstrated with verbatim verification and measured with faithfulness (§5.5) (the full Kensho case is presented in §1.1.2).


EXPANDE

In plain terms: the library and the calculator

A vector store is a magnificent library for finding where something is discussed, but the librarian does not do the books. The embedding of "158,104" lands close to that of "158,014" even though the two differ by 90 million, because the embedding encodes distributional context — where that number usually appears — and not its magnitude. Asking the vector store to "understand" a figure is asking the librarian to audit the balance sheet.

The module's text/numbers split is, at bottom, an office division of labor: prose — risks, management narrative, the CFO's tone — goes to the library with its section index card; figures go to the accountant, that is, to XBRL facts and SQL tables where concept, period, currency, and scale are explicit fields; and arithmetic goes to the calculator, a deterministic tool. The LLM coordinates the visit: it phrases the question, retrieves the source, invokes the tool, and drafts the answer citing both. What it never does is the math "in its head".

EXPANDE

Common pitfall: letting the LLM do the math

A real scene: retrieval brought the right fragment — revenues of 158,104 and 177,798 million in consecutive years — and it is asked for "the year-over-year change". The LLM answers, in impeccable prose, "≈14%". The correct figure is 12.5% (\((177.798-158.104)/158.104 = 0{,}1246\)). The error is not retrieval or comprehension: it is improvised arithmetic, the dominant failure mode of LLM reasoning documented by PAL — 16 of 25 chains of thought were identical except for the numbers, as the module recalls (§6.2 develops it).

The defense has two layers, and neither is prompting. First, the calculation is delegated: xbrl_lookup for the facts and the ratio calculator for the quotient — XBRL-Agent's +42 points on numerical reasoning measure exactly this decision. Second, verbatim verification as code: if a figure in the output does not literally exist in the cited source, the answer abstains ("NOT_AVAILABLE") or goes to the human queue. Watch the scale: "millions read as thousands" is 15% of extraction failures, and a literal-matching regex does not catch it if the text says "$158 billion" and the source "158,104 million" — that is why the explicit scale field of XBRL facts is part of the verification, not an ornament.

5.5 RAG evaluation

An unevaluated RAG pipeline is a pipeline that has already failed silently. Institutional evaluation is done separately for retrieval and generation, and then end-to-end over a golden dataset: (towardsai.net) retrieval is measured with ranking metrics (context precision/recall), generation with faithfulness and answer relevancy. (ActiveWizards: data science and engineering lab)

5.5.1 RAGAS, a proprietary golden dataset, and acceptance thresholds

The four RAGAS metrics (operational definitions from arXiv 2409.09046): (ActiveWizards: data science and engineering lab)

The retrieval metric underlying context recall is recall@k: given the set \(R_q\) of relevant passages for query \(q\) and the \(k\) retrieved passages \(D_k\),

\[ \mathrm{recall@k} = \frac{|D_k \cap R_q|}{|R_q|} \]

That is, the fraction of the necessary evidence that appears within the top \(k\) results; it is the metric that justifies hybrid search (91% vs. 78% reported recall@10) (LangChain) and the one a proprietary golden dataset measures against the annotated evidence passages.

RAGAS's key diagnostic value is failure separation: low faithfulness with high context recall indicates a generation problem (the model had the documents but distorted them); low context recall indicates a retrieval problem (the documents were never retrieved). Faithfulness and Answer Relevancy are reference-free (they require no golden answer), which makes them scalable to real traffic. (shafiqulai.github.io)

from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
    Faithfulness, AnswerRelevancy,
    LLMContextPrecisionWithReference, LLMContextRecall,
)
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

judge_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini", temperature=0))
judge_emb = LangchainEmbeddingsWrapper(OpenAIEmbeddings(model="text-embedding-3-small"))

rows = {"question": [], "answer": [], "contexts": [], "reference": []}
for item in EVAL_QUESTIONS:  # e.g. FinanceBench-style questions
    result = rag.invoke({"input": item["q"]})
    rows["question"].append(item["q"])
    rows["answer"].append(result["answer"])
    rows["contexts"].append([d.page_content for d in result["context"]])
    rows["reference"].append(item["ref"])

result = evaluate(
    Dataset.from_dict(rows),
    metrics=[Faithfulness(), AnswerRelevancy(),
             LLMContextPrecisionWithReference(), LLMContextRecall()],
    llm=judge_llm, embeddings=judge_emb,
)
print(result.to_pandas().mean(numeric_only=True))

A proprietary FinanceBench-style golden dataset. The template: expert-written questions about the firm's own filings, each one with (a) a golden answer with the exact figure and tolerance (±2.5% on numeric answers, as in FinanceBench), (b) annotated evidence passages (doc, section, page) that define \(R_q\) for recall@k, and (c) a question type (extractive, cross-period comparative, multi-hop, with calculation) to break down failures. (API EvangelistAPI Evangelist) Some 100-200 questions over 20-40 portfolio filings cover 80% of a desk's query modes. LangSmith complements this with a structured-output correctness grader ("grade ONLY for factual accuracy against the ground truth; additional information is acceptable if it does not contradict it") and per-trace drill-down. (matt-harrison.com)

Acceptance thresholds for production. The 2026 RAGAS guide with CI gating proposes as a starting point: faithfulness ≥ 0.85, answer relevancy ≥ 0.80, context precision ≥ 0.75, and context recall ≥ 0.80; (shafiqulai.github.io) real financial pipelines on FinanceBench report faithfulness around 0.86-0.87 as an achievable reference. (CallSphere) Two finance-specific gates are added to these thresholds: (1) verbatim verification rate = 100% of the figures in the evaluation sample (§5.4) — deterministic, non-negotiable; (2) adequate abstention rate: on questions with no evidence in the corpus, the correct answer is "NOT_AVAILABLE", and a fabrication rate >0% on that subset blocks deployment. Judge cost control is solved with a cheap model (gpt-4o-mini), temperature 0, and PR sampling. (shafiqulai.github.io)


EXPANDE

Worked example: a golden dataset question, field by field

The FinanceBench-style golden dataset is built question by question, and each one is a record with three mandatory fields. This is a real item from the research desk of the applied case (§5.5.1: 150 questions written by the analysts themselves about their coverage filings):

{
    "question": "¿Cuál fue el ratio CET1 de JPMorgan a cierre de 2024?",
    # (a) Golden answer with tolerance (±2.5% on numeric answers, as in FinanceBench)
    "reference": "15,7 % (aceptable: 15,3 %–16,1 %)",
    # (b) Evidence passages: they define R_q for recall@k
    "evidence": [
        {"doc": "JPM 10-K FY2024", "section_path": "10-K > Item 8 > Note 27",
         "page": 312}
    ],
    # (c) Question type, to break down failures by category
    "type": "extractiva",   # extractiva | comparativa | multi-hop | con_cálculo
}

Three practical decisions. The ±2.5% tolerance exists because the model can cite the figure with different rounding and still be correct; without it, binary scoring penalizes good answers. The evidence passages are not decoration: they are the \(R_q\) against which context recall is measured — without them, recall@k is not computable and the retrieval-vs-generation diagnosis collapses. And the type field is what allows discovering that "40% of failures concentrate on queries with exact terms", as in the applied case: aggregated, a 0.62 recall does not say where to act; broken down by type, it does. With 100–200 such questions over 20–40 filings you cover 80% of a desk's query modes — write the first 30 by copying the questions analysts already ask by email.

EXPANDE

Worked example: reading the diagnostic matrix before changing anything

The module's applied case is the demonstration of the procedure: context recall 0.62 diagnosed retrieval (and was fixed with BM25 weight and the year filter, not with prompts); faithfulness 0.79 then diagnosed generation (and was fixed by moving calculation to tools and activating verbatim verification). Neither improvement came from prompting. The operational diagram:

Diagrama

Two practical details. The order of the questions matters: measuring faithfulness with low recall is judging the writer by books that never reached them — fix retrieval first, then generation. And the two final gates are of a different nature than the RAGAS metrics: verbatim verification is deterministic (it admits no "0.99") and the fabrication rate on no-evidence questions is a binary veto. That is why the CI gate blocks the merge even when all four metrics are green.

Applied case: research desk on banking filings

A banking research team deploys the module's pipeline over the 10-K/10-Qs and earnings calls of its 40 covered banks: chunking by Items with section_path and turns with role; financial embedding; Qdrant with metadata filtering; EnsembleRetriever (0.6/0.4) → CohereRerank (top 5) → ParentDocumentRetriever; XBRL facts and ratios in SQL/calculator tools; mandatory verbatim verification. Golden dataset: 150 FinanceBench-style questions written by the analysts themselves.

The first RAGAS measurement shows context recall 0.62 — a retrieval problem: 40% of failures concentrate on queries with exact tickers and instrument codes. Raising the BM25 weight to 0.5 and adding the fiscal_year filter lifts recall to 0.84. The second measurement shows faithfulness 0.79 — now a generation problem: the LLM was deriving year-over-year changes in text. All calculation is moved to the ratio tool and verbatim verification is activated: faithfulness rises to 0.88 and unsupported figures drop to zero. The production gate (faithfulness ≥ 0.85, recall ≥ 0.80, verbatim 100%, 0% fabrication on no-evidence questions) is passed, and the system enters a pilot with human review of every figure feeding client notes. The transferable lesson is the diagnostic loop: each metric pointed to a different layer of the pipeline, and no improvement came from prompting.


Exercises

  1. Chunking a 10-K by Items. Download JPMorgan's most recent 10-K from EDGAR (Module 4) and implement chunk_10k_by_items (§5.2.1) with a complete section_path. Verify that Item 1A and Item 7 end up separated, re-split Items >2,000 tokens propagating metadata, and write a test: no chunk without item and fiscal_year.
  2. Measured hybrid search. On the exercise 1 corpus, build a dense-only retriever and a 0.6/0.4 EnsembleRetriever. With 20 questions mixing semantic queries ("liquidity risks") and exact ones ("Common Equity Tier 1 ratio", tickers), measure recall@10 against annotated evidence and compare with the 78% vs. 91% reference.
  3. Reranking and parent context. Add CohereRerank (or a local cross-encoder ms-marco-MiniLM-L-6-v2) and ParentDocumentRetriever (child 400 / parent 2,000). Measure RAGAS context precision before and after on the same 20 questions and quantify the added latency per query.
  4. Text/numbers split. Implement xbrl_lookup (SEC company facts) and the ratio calculator; rewrite five calculation questions ("year-over-year change in net margin") to be solved via tool calling. Apply verificacion_verbatim and report the unsupported-figure rate before and after.
  5. RAGAS evaluation with a CI gate. Run evaluate() (§5.5.1) with a proprietary golden dataset of ≥30 FinanceBench-style questions (golden answer + evidence passages). Diagnose retrieval-vs-generation with the faithfulness × context recall matrix and set the 0.85/0.80/0.75/0.80 thresholds as a CI job that blocks the merge.
  6. Controlled abstention. Add 10 questions with no answer in the corpus (another period or another company). Measure the fabrication rate and adjust prompt and verifier until 0% invented figures with "NOT_AVAILABLE" abstention. Discuss why this gate outweighs a point of accuracy for a risk manager.

Module 5 of 15 — LangChain for Quantitative Trading. Data and versions verified as of July 2026; market prices and figures subject to change. LangChain 1.3.14 / langchain-core 1.5.2.


EXPANDE

Elite resources to go deeper

CHECK

Check your understanding

Self-assessment with instant feedback. No scores are stored: it is just for you.