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.
- 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.
- Models can use long contiguous context — putting one complete section in the prompt preserves headings, cross-references, and table structure that chunking destroys.
- 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
namestrings — always list/discover prefixes per type and year
Limitations
- Section names vary by issuer and filing year; treat prefixes as filters, then confirm
namein 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.