Na parte 1, o documento era comportado: uma lei, só texto, com artigos numerados. Na vida real, o documento que chega é um PDF. Com tabelas, gráficos, duas colunas e nota de rodapé.
Peça a um assistente de IA um RAG para PDFs e o código vem quase sempre igual: pypdf para extrair o texto, um divisor de texto, embeddings, pronto. Talvez ele sugira uma biblioteca "que entende tabelas". Raramente ele pergunta o que acontece com o número que estava na tabela.
Então medimos. Pegamos o Relatório de Política Monetária do Banco Central, escrevemos 33 perguntas com resposta conhecida (no texto, numa tabela ou só num gráfico) e comparamos quatro jeitos de transformar o PDF em texto, em português e em inglês.
O resumo: o extrator mais sofisticado reconstruiu as tabelas muito melhor que os outros, e o LLM leu a célula errada mesmo assim. Os gráficos só passaram a ter resposta quando um modelo de visão os descreveu, ao custo de duas horas por idioma. E pelo caminho achamos meia dúzia de armadilhas que o código gerado por IA não trata. No fim, mostramos como ler tabelas e gráficos direito com um modelo grande via API, e quanto isso custa.
O código completo, com as perguntas, está em salada-dados/blog. Tudo roda de graça, na sua máquina, sem chave de API.
Por que PDF é difícil
PDF não guarda texto. Guarda instruções de desenho: "escreva '2,8' na posição (512, 340)". Não existe "linha 3, coluna 11" numa tabela, nem "isto é um gráfico". Quem extrai o texto precisa adivinhar a estrutura a partir das posições.
Veja a mesma linha da Tabela 1.2.1 (o PIB por setor) saindo de três extratores. A pergunta é: quanto a agropecuária cresceu no segundo trimestre de 2026? A resposta é 2,8.
O pypdf devolve o texto na ordem em que foi desenhado:
Discriminação Variação anual
2021 2022 2023 2024 2025
I II III IV I II
PIB a preços de mercado 4,8 3,0 3,2 3,4 2,3 1,3 0,4 0,1 0,2 1,1 0,5
Agropecuária 0,0 -1,1 16,3 -3,7 11,7 16,1 -3,2 0,5 0,7 2,3 2,8Os números estão todos lá. O que sumiu foi o cabeçalho: "I II III IV I II" não diz que os quatro primeiros trimestres são de 2025 e os dois últimos, de 2026. Para achar o 2,8, é preciso contar colunas.
O pymupdf4llm tenta montar uma tabela em Markdown, e o cabeçalho sai picotado:
|Discriminação|Variaçã|o anual|||||Tr|i ante tr|i -1 (a.s.|)|%|
||2021|2022|2023|2024|2025||20|25||20|26|
|||||||I|II|III|IV|I|II|
|Agropecuária|0,0|-1,1|16,3|-3,7|11,7|16,1|-3,2|0,5|0,7|2,3|2,8|O Docling, da IBM, usa um modelo de layout para reconstruir a tabela:
| Discriminação | Variação anual | Variação anual | Variação anual | Variação anual | Variação anual | % | Tri | ante tri | -1 (a.s.) | % | % |
| | 2021 | 2022 | 2023 | 2024 | 2025 | | 2025 | | | | 2026 |
| | | | | | | I | II | III | IV | I | II |
| Agropecuária | 0,0 | -1,1 | 16,3 | -3,7 | 11,7 | 16,1 | -3,2 | 0,5 | 0,7 | 2,3 | 2,8 |Não é perfeito: o título do grupo trimestral ainda sai em pedaços, e cada ano ocupa uma célula só, no meio do seu grupo. Mas as palavras estão inteiras, "Variação anual" se repete em cada coluna que cobre, e cada número está na sua coluna. É, de longe, a versão mais legível.
A pergunta do experimento é se essa diferença, que salta aos olhos, chega até a resposta.
O documento: o RPM do Banco Central
O Relatório de Política Monetária (RPM) é o documento trimestral em que o Copom explica a conjuntura e as projeções de inflação. Usamos o de setembro de 2026, por três motivos:
- Tem de tudo: cerca de 90 páginas, 20 tabelas e 100 gráficos, texto em duas colunas, boxes e notas de rodapé.
- Tem versão oficial em português e em inglês, com as mesmas tabelas e os mesmos gráficos. O mesmo experimento roda nas duas línguas.
- Pode ser reproduzido citando a fonte: "Relatório de Política Monetária, volume 2, nº 3".
Antes de escrever qualquer código, abrimos o PDF e olhamos. Isso evitou o primeiro erro. A versão em inglês tem 317 imagens, e a em português, duas. Parecia que um idioma tinha gráficos como imagem e o outro não. Era a capa, cortada em centenas de pedaços. Nos dois idiomas, os gráficos são vetoriais: linhas e textos desenhados, não figuras.
Isso muda tudo para os gráficos: os rótulos dos eixos, a legenda e o título são texto, e um extrator simples os lê.
Quatro jeitos de extrair
| Extrator | O que faz | Tempo (pt / en) |
|---|---|---|
pypdf | Texto puro, na ordem do desenho. É o que quase todo tutorial usa. | 15 s / 22 s |
pymupdf4llm | Markdown, com títulos e tentativa de tabelas. | 64 s / 49 s |
| Docling | Modelo de layout: reconstrói tabelas e separa as figuras. | 8 min / 5 min |
| Docling + visão | Docling, e cada figura descrita por um modelo de visão local. | +2,4 h / +2,1 h |
Tudo em CPU, num notebook comum. O modelo de visão é o qwen2.5vl:3b, rodando no Ollama.
Do extrator em diante, nada muda entre os quatro: o mesmo divisor de texto (blocos de até 400 tokens, sem atravessar páginas), o mesmo modelo de embeddings da parte 1 (multilingual-e5-small), os mesmos 3 trechos enviados ao LLM, e o mesmo LLM (qwen2.5:3b). Se o resultado muda, foi o extrator.
Como medir sem achismo
Escrevemos 33 perguntas, cada uma com a página e a resposta exata:
- 12 de texto: a resposta está num parágrafo. "Quanto variou o IBC-Br em julho de 2026?"
- 11 de tabela: a resposta está numa célula. "Quanto o Brasil importou de janeiro a julho de 2024?"
- 10 de gráfico: a resposta só existe no desenho. "Em que ano a taxa de desocupação atingiu o maior valor desde 2004?" Escolhemos perguntas cuja resposta não está escrita no texto do relatório.
E medimos três coisas, em sequência:
- Página certa: a busca trouxe, entre os 3 trechos, algum da página que tem a resposta?
- Número no contexto: a resposta exata (o 2,8, o "Japão") está escrita nesses trechos?
- Resposta certa: o LLM respondeu com ela?
A parte 1 parou no primeiro número. Aqui não dá: com tabelas, a busca pode acertar a página e o número chegar sem significado.
A correção é automática, e um detalhe dela importa: "2,8" não pode contar como acerto quando a resposta é "12,8" ou "2,85". Por isso a resposta precisa aparecer como um número inteiro, não como pedaço de outro:
def contains(text: str, answers: list[str]) -> bool:
for a in answers:
if re.search(rf"(?<![\w,.]){re.escape(a)}(?![\d]|[,.]\d)", text, re.I):
return True
return FalsePor fim, uma linha de base sem RAG: as mesmas perguntas ao LLM, sem nenhum trecho. Se ele acertasse sozinho, o acerto com RAG não provaria nada. Acertou 1 de 33 em português e 0 em inglês, e o único acerto foi "ureia ou enxofre?", que é cara ou coroa.
Resultados
Respostas certas, de 33 perguntas:
| Extrator | Texto (12) | Tabela (11) | Gráfico (10) | Total (pt) | Total (en) |
|---|---|---|---|---|---|
| Sem RAG | 0 | 0 | 1 | 1 | 0 |
pypdf | 8 | 2 | 0 | 10 | 9 |
pymupdf4llm | 9 | 2 | 0 | 11 | 8 |
| Docling | 10 | 2 | 0 | 12 | 9 |
| Docling + visão | 11 | 2 | 4 | 17 | 10 |
As colunas de texto, tabela e gráfico são do português. Os tokens enviados ao LLM ficaram entre 830 e 1.080 por pergunta em todos os extratores, então ninguém venceu mandando mais texto.
À primeira vista, a história é simples: quanto mais sofisticado o extrator, melhor, e o modelo de visão é o que destrava os gráficos. Mas olhe a coluna de tabela: 2 de 11 para todos. O extrator que reconstruiu a tabela acertou tanto quanto o que embaralhou o cabeçalho.
O que a tabela esconde
O LLM pequeno lê a célula errada
Fomos ler as respostas uma a uma. Nas perguntas de tabela, o problema quase nunca era o número faltar. Era o LLM pegar o vizinho:
| Pergunta | Certo | O LLM respondeu | O que ele leu |
|---|---|---|---|
| Importações de jan. a jul. de 2024 | 155 | 147 | a coluna de 2023 |
| IPCA Livres, 4º tri de 2027 | 4,1 | 5,1 | a linha de baixo (Administrados), na primeira coluna |
| Chance de estourar a meta em 2027 | 33% | 16% | a linha de 2028 |
O qwen2.5:3b tem 3 bilhões de parâmetros. Acompanhar uma linha até a coluna certa numa tabela de 12 colunas é difícil para ele, com ou sem estrutura.
Para testar essa hipótese, rodamos as perguntas de tabela de novo com o qwen2.5:7b, usando exatamente os mesmos trechos. Contando só as perguntas em que o número certo chegou ao LLM (pt e en somados):
pypdf (texto puro) | Docling (tabela estruturada) | |
|---|---|---|
qwen2.5:3b | 5 de 11 | 5 de 12 |
qwen2.5:7b | 7 de 11 | 10 de 12 |
Com o modelo pequeno, a estrutura não faz diferença. Com o maior, faz. No pypdf, sem cabeçalho, até o 7B se perde: perguntado sobre a produção de veículos em agosto, respondeu "-0,0", o valor da coluna seguinte.
Ou seja: reconstruir a tabela só compensa se o LLM souber ler tabela. Pagar de 15 a 30 vezes mais tempo de extração e mandar o resultado para um modelo de 3B é jogar o trabalho fora.
Metade das tabelas nem chega
Mesmo com o 7B, sobra o outro problema: em cerca de metade das perguntas de tabela, o número não estava nos 3 trechos. A busca não trouxe a tabela.
O caso mais claro é o pymupdf4llm: ele achou a página certa em 9 de 11 perguntas de tabela, mas o número estava no contexto em só 3. A busca trazia o parágrafo que fala da tabela ("o PIB avançou 0,5%... a agropecuária cresceu fortemente"), e não a tabela.
Faz sentido. A pergunta "quanto a agropecuária cresceu" se parece muito com uma frase sobre agropecuária. Um bloco de |0,0|-1,1|16,3|-3,7| não se parece com nada. E tabela em Markdown é cara: a página da Tabela 1.2.1 custa cerca de 1.000 tokens em texto puro e de 1.700 a 1.850 com as tabelas em Markdown. Com o limite de 400 tokens por pedaço, a tabela é fatiada em vários pedaços, e só o primeiro leva o cabeçalho.
Isso aponta o próximo passo, que fica para outro experimento: guardar cada tabela com um resumo em texto ("Tabela 1.2.1: PIB por setor, variação anual de 2021 a 2025 e trimestral até o 2º tri de 2026") e repetir o cabeçalho em cada pedaço.
O Docling joga fora o texto dos gráficos
Na coluna de gráficos, o Docling sem visão foi o pior em achar a página certa: 5 de 10 em português e 4 de 10 em inglês, contra 7 e 9 do pypdf.
O motivo: ao reconhecer um gráfico como figura, o Docling troca tudo o que está dentro dele por <!-- image -->. O título, a legenda e os rótulos dos eixos, que o pypdf despeja como texto solto, somem. E o Docling quase nunca liga a figura à sua legenda: 96 de 123 figuras ficaram sem legenda em português, e 88 de 126 em inglês. Em vários casos, dois gráficos lado a lado viraram uma figura só.
Uma descrição sem "Gráfico 1.2.14: Taxa de desocupação" é muito mais difícil de encontrar. Então lemos a legenda direto do PDF, na área logo acima da figura:
def caption(item, doc, pdf):
if text := item.caption_text(doc).strip(): # quando o Docling ligou a legenda
return text
prov = item.prov[0]
page = pdf[prov.page_no - 1]
box = prov.bbox.to_top_left_origin(page_height=page.rect.height)
area = pymupdf.Rect(box.l, box.t - 30, box.r, box.b) # a figura e um pouco acima
... # a primeira linha que começa com "Gráfico 1.2.14 –" é a legendaCom isso, 118 das 123 figuras ganharam legenda. As que sobraram são a capa e ícones.
O modelo de visão descreve antes da pergunta
Com a legenda resolvida, cada figura foi descrita pelo qwen2.5vl:3b, e a descrição entrou no lugar do <!-- image -->. Foi isso que levou os gráficos de 0 para 4 acertos em português.
O primeiro prompt, porém, foi um desastre. Era detalhado: título, eixos, unidade, cada série, máximo e mínimo de cada uma. O modelo inventou um título que não existe ("Crescimento Econômico: Projeções 2025-2026"), entrou em loop repetindo "Não especificado no gráfico" para 32 combinações de país e trimestre e levou 7 minutos e meio num gráfico só. Nos 250 gráficos, seriam 31 horas.
Um prompt curto, com limite de saída, resolveu:
PROMPT = ("Descreva este gráfico em até 5 frases, em português: o que ele mede, o período, "
"as séries da legenda e o que se destaca (máximos, mínimos, quem é maior ou menor, "
"valores aproximados). Use só o que está visível.")
options = {"temperature": 0, "num_predict": 300, "repeat_penalty": 1.15}Cerca de 66 segundos por gráfico. Mas olhe o que ele escreveu sobre o Gráfico 1.1.4, o do crescimento do PIB por país:
Este gráfico compara diferentes países em termos de um indicador específico entre 2025 e 2026.
...
Os países comparados são EUA, China, Japão, Área do euro, Reino Unido e América Latina."Um indicador específico". A pergunta era qual país cresceu menos, e a descrição não diz. Em inglês foi pior: a descrição do mesmo gráfico afirma, com toda a confiança, que o Reino Unido "quase dobrou" e que a média global chegou perto de 5%. Nenhuma das duas coisas está no gráfico. Talvez por isso a visão quase não tenha ajudado em inglês (1 de 10).
O problema de fundo é a ordem das coisas. O modelo descreve cada gráfico uma vez, antes de qualquer pergunta existir, sem saber o que vai importar. Depois, só a descrição vai para a busca, e nem a busca nem o LLM veem a imagem.
E por que, quando você manda um print para um assistente como o Claude ou o ChatGPT, ele lê tudo direitinho? Porque ali é o contrário: um modelo muito maior olha a imagem junto com a sua pergunta. É aí que está a saída.
Então, como ler direito?
Tudo até aqui foi feito de graça, em CPU, com modelos de 3 e 7 bilhões de parâmetros. Foi de propósito: dá para repetir sem gastar nada, e os defeitos aparecem com clareza. Mas se o objetivo é acertar tabelas e gráficos, o caminho é usar um modelo grande, via API. Ele tem custo e tem riscos. E é a solução.
O que o experimento já indica
Os números acima separam bem os problemas, e cada um tem um remédio diferente:
- Ler a tabela é questão de tamanho de modelo. Com os mesmos trechos, o 7B foi de 5 para 10 acertos em 12 nas tabelas do Docling. Um modelo de fronteira lê tabela estruturada sem esforço.
- Ler o gráfico é questão de ordem. O modelo precisa olhar a imagem com a pergunta na mão, e não descrevê-la antes, às cegas.
- Achar a tabela não melhora com modelo nenhum. Se a busca não trouxe o trecho, o LLM mais caro do mundo não tem o que ler. Isso se resolve no índice (resumo da tabela e cabeçalho repetido), não no LLM.
A arquitetura: busca no texto, resposta na imagem
A ideia é fazer no pipeline o que o chat faz:
- A busca continua no texto. Extraia com
pypdfou Docling, como fizemos. A busca serve só para achar a página. - A resposta vem da imagem da página. Renderize as páginas dos 3 trechos encontrados e mande as imagens, junto com a pergunta, para um modelo grande com visão.
import base64, pymupdf
def page_png(pdf, page_no, dpi=150):
pix = pdf[page_no - 1].get_pixmap(dpi=dpi)
return base64.b64encode(pix.tobytes("png")).decode()
pages = sorted({page for _, page in top3}) # páginas dos 3 trechos achados
content = [{"type": "image", "source": {"type": "base64", "media_type": "image/png",
"data": page_png(pdf, p)}} for p in pages]
content.append({"type": "text", "text": pergunta})
# client.messages.create(model=..., messages=[{"role": "user", "content": content}])Repare que aqui o "extrator ruim" ajuda. O pypdf achou a página certa em 7 de 10 perguntas de gráfico em português (9 em inglês), justamente porque despeja os rótulos e a legenda do gráfico como texto. Com a imagem da página indo para o modelo, esse é o teto: no máximo 7 de 10 em português, porque nas outras 3 a página nem chega. Ainda assim, é muito acima dos 4 de 10 que conseguimos com duas horas de modelo local.
A alternativa mais simples é manter o pipeline que já testamos e só trocar o modelo que descreve as figuras por um grande. Melhora as descrições, mas continua descrevendo antes da pergunta.
Quanto custa
As páginas do RPM são A4. Renderizada em 150 dpi, cada uma tem 1241 × 1754 pixels. Na conta da Anthropic (pixels ÷ 750), isso dá cerca de 2.900 tokens por página; outros provedores usam contas parecidas. Com preços de um modelo intermediário, de US$ 2 por milhão de tokens de entrada e US$ 10 por milhão de saída (preços de novembro de 2026; confira antes de orçar):
| Abordagem | Tokens por uso | Custo aproximado |
|---|---|---|
| Descrever as figuras uma vez (123 por idioma) | ~3.000 de entrada e ~300 de saída por figura | ~US$ 1 por idioma, uma vez só |
| Mandar as páginas a cada pergunta | ~9.000 de entrada (3 páginas) e ~100 de saída | ~US$ 0,02 por pergunta |
Dois centavos de dólar por pergunta parece nada, e para um teste é. Em produção, com 1.000 perguntas por dia, são uns US$ 600 por mês, só de imagem. Um modelo do topo de linha custa de 2 a 4 vezes mais. Dá para cortar: mandar só a página do primeiro trecho, reduzir a resolução, mandar a imagem só quando a pergunta for sobre número, ou usar processamento em lote quando não houver pressa.
Essas contas são estimativas, não medições. O teste com o modelo grande fica para o próximo artigo.
Os riscos
- O documento sai da sua máquina. Um relatório público do Banco Central, tudo bem. Contrato, prontuário, dado de cliente: aí entram a LGPD, o contrato com o provedor (retenção, uso para treino, região onde roda) e, às vezes, sigilo bancário ou profissional. Pergunte ao jurídico antes, e não depois.
- O custo escala com o uso, não com o documento. No modelo local, o custo foi o tempo de indexar. Aqui, cada pergunta paga de novo. Coloque limite de gasto e meça o custo por pergunta desde o primeiro dia.
- Modelo grande também erra. Ler um valor no meio de uma curva é aproximação, para gente e para modelo. Peça a página de onde veio a resposta e mantenha a correção estrita que usamos aqui.
- A imagem também é entrada. Um texto escondido numa figura ("ignore as instruções anteriores") chega ao modelo como qualquer outro texto. Em documentos de terceiros, isso é um vetor de prompt injection.
- Dependência do fornecedor. Preço, modelo e limite de uso mudam. Guarde as perguntas e o avaliador deste experimento: são eles que dizem se a troca de modelo piorou alguma coisa.
Se nada disso puder sair de casa, existe o meio-termo: um modelo de visão maior rodando localmente, numa GPU. Fica mais caro em hardware, mas o documento não sai.
O que o assistente de código não vai te avisar
Juntando tudo, eis o que fez diferença neste experimento e que dificilmente aparece no código gerado por um assistente de IA:
- Abrir o PDF antes de escolher a ferramenta. As 317 imagens eram a capa; os gráficos eram vetoriais. A escolha do extrator depende disso.
- Conferir a ligação entre figura e legenda. O Docling deixou 78% das figuras sem legenda, sem erro e sem aviso.
- Testar o prompt de visão em uma figura antes de rodar em lote. O prompt "completo" custaria 31 horas e geraria descrições inventadas.
- Medir a resposta, não só a busca. Página certa não é número certo, e número certo não é resposta certa.
- Rodar uma linha de base sem RAG. Sem ela, você não sabe quanto do acerto é o documento e quanto é chute.
- Corrigir de forma estrita. Um
if "2,8" in respostaaceita "12,8". - Ler as licenças e os efeitos colaterais. O
pymupdf4llmdepende do PyMuPDF, que é AGPL (ou licença comercial), o que importa se o seu código for fechado. E ele roda OCR sozinho nas páginas sem texto (aqui, a capa), sem que ninguém peça.
Cuidados antes de generalizar
33 perguntas é pouco. São 10 a 12 por tipo, então cada pergunta vale quase 10 pontos. Por isso mostramos contagens, e não porcentagens. Diferenças de um ou dois acertos podem ser sorte; o "10 de 12 contra 7 de 11" do 7B é um indício forte, não uma prova.
O resultado vale para este documento. O RPM é um PDF gerado digitalmente, com gráficos vetoriais. Num PDF escaneado, o pypdf não extrai nada, e o OCR entra na conta. Com gráficos em imagem, os rótulos que salvaram o pypdf aqui não existiriam.
Os modelos são pequenos de propósito. Tudo roda de graça, em CPU. Os resultados medem os extratores e mostram onde um modelo pequeno quebra. Não são o teto do que dá para alcançar com um modelo grande.
O inglês foi pior em quase tudo, e não investigamos o porquê a fundo. As perguntas são traduções das mesmas perguntas, e o documento é o mesmo. A descrição inventada do Gráfico 1.1.4 é uma pista, não a explicação inteira.
Como repetir no seu PDF
- Olhe o PDF. Gerado digitalmente ou escaneado? Gráficos vetoriais ou em imagem? Quantas tabelas?
pymupdfresponde isso em cinco linhas. - Escreva perguntas separadas por tipo: texto, tabela, figura. Elas falham por motivos diferentes, e a média esconde isso.
- Meça os três passos: página certa, resposta no contexto, resposta certa. É assim que você descobre se o problema é a extração, a busca ou o LLM.
- Só pague pelo extrator caro se o resto da cadeia aproveitar. Tabela reconstruída com LLM pequeno não rendeu nada aqui.
- Teste o modelo de visão em três ou quatro figuras e leia as descrições antes de rodar no documento inteiro.
Próximos passos
Esta é a parte 2 da série sobre RAG. O próximo artigo coloca a arquitetura acima à prova: as mesmas 33 perguntas, com as páginas indo como imagem para um modelo grande, com o custo real medido. E explica por que o chat lê o seu print tão bem e o pipeline não.
Depois, na parte 3, o banco vira uma ferramenta de um agente, via MCP, continuando a série de LangGraph com FastAPI.
