Why Section Prefixes Beat Embeddings for CVM Filing Retrieval

Most teams building LLM pipelines over Brazilian filings default to the same pattern: chunk every PDF, embed, store vectors, retrieve top-k. That stack is familiar. For many CVM questions it is also the wrong first move.

When the user intent maps to a known section — risk factors, board composition, financial notes — retrieving by document name prefix beats semantic search. You get the right file, put the full section (or its pages) into a large context window, and the model reads contiguous regulatory prose instead of shuffled chunks. apicvm exposes those section names as the name field on GET /v1/documents, plus GET /v1/document-prefixes to discover them.

The problem with embedding-first CVM RAG

Embedding RAG optimizes for open-ended similarity. CVM catalogs already solve a harder problem: section identity.

Failure mode What happens with embeddings What happens with section prefixes
Wrong section "Risk" chunks mix market risk, operational risk, and ESG boilerplate name=DescricaoFatoresRisco returns the risk-factor document
Chunk boundaries Tables and CPC/IAS footnotes split mid-argument You extract one document's pages in order
Recall theater Top-k looks plausible; the decisive paragraph is chunk 47 The whole section is in context
Index drift Re-embed when models or chunkers change Catalog name is stable per filing upload

Semantic search is still useful for unknown questions across a corpus. It is a poor default when the agent already knows the section type.

Why larger context windows changed the tradeoff

Older RAG assumed you could not afford to load a full FRE annex. That constraint is weaker now.

  1. Section PDFs are bounded — a FRE risk-factor file or DFP notes annex is often tens of pages, not hundreds of unrelated sections glued together.
  2. Models can use long contiguous context — putting one complete section in the prompt preserves headings, cross-references, and table structure that chunking destroys.
  3. Precision compounds quality — retrieval error is usually worse than generation error. A perfect summarizer on the wrong section still fails.

The practical architecture becomes:

intent → section prefix → list documents → extract pages → LLM over full section

not:

intent → embed query → hope top-k hits the right annex

Map intent to CVM prefixes

Define a small router before any vector store:

User intent type Start with name prefix
Risk factors FRE DescricaoFatoresRisco
Market risks FRE DescricaoRiscosMercado
Board / independence FRE InformacoesConselhoAdm
Compensation FRE PoliticaPraticaRemuneracao
Legal contingencies FRE discover via list (Processos, etc.)
Financial notes DFP / ITR Notas
Management report DFP / ITR Relatório da Administração

Discover available prefixes for a type:

export APICVM_KEY='apicvm_...'
export APICVM_URL='https://apicvm.dev'

curl -H "Authorization: Bearer $APICVM_KEY" \
  "$APICVM_URL/v1/document-prefixes?type=FRE"

Then fetch only matching documents:

curl -H "Authorization: Bearer $APICVM_KEY" \
  "$APICVM_URL/v1/documents?ticker=VALE3&type=FRE&year=2025&name=DescricaoFatoresRisco&perPage=5"

Extract page markdown for that document ID via POST /v1/document-text-extractions and send the pages to the model in order.

When embeddings still make sense

Keep a vector index for cases where section routing cannot decide:

  • Cross-issuer thematic search ("which FRE mentions CBAM?")
  • Fuzzy questions that span multiple annexes
  • Corpus exploration without a typed intent

Even then, constrain the candidate set first — embed only Notas pages for footnote Q&A, or only FRE risk sections for risk chatbots. Hybrid beats "embed the entire FRE dump."

Example agent policy

import os, requests

BASE = os.environ["APICVM_URL"]
H = {"Authorization": f"Bearer {os.environ['APICVM_KEY']}"}

INTENT_PREFIX = {
    "risk_factors": ("FRE", "DescricaoFatoresRisco"),
    "board": ("FRE", "InformacoesConselhoAdm"),
    "notes": ("DFP", "Notas"),
}

def list_section_docs(ticker: str, year: int, intent: str):
    doc_type, prefix = INTENT_PREFIX[intent]
    r = requests.get(
        f"{BASE}/v1/documents",
        params={
            "ticker": ticker,
            "type": doc_type,
            "year": year,
            "name": prefix,
            "perPage": 20,
        },
        headers=H,
    )
    r.raise_for_status()
    return r.json()["data"]

The agent never embeds the query for these intents. It resolves the section, extracts it, and reasons over the text.

What this is not

  • Not a claim that vector databases are obsolete
  • Not automatic page classification inside a PDF — apicvm uses catalog document names from CVM filings
  • Not a guarantee every issuer uses identical name strings — always list/discover prefixes per type and year

Limitations

  • Section names vary by issuer and filing year; treat prefixes as filters, then confirm name in the list response
  • Some filing types are less granular than FRE
  • Very long annexes may still need page windows or secondary retrieval inside the section
  • Extraction is async via callback and consumes page credits on Pro plans

Next steps

Ready to integrate?

Get an API key and start querying Brazilian CVM filings programmatically.