---
title: "RAG com Documentos Públicos em Python"
url: "https://python.dev.br/blog/rag-documentos-publicos-python/"
markdown_url: "https://python.dev.br/blog/rag-documentos-publicos-python.MD"
description: "Aprenda a criar um RAG em Python com documentos públicos brasileiros, MarkItDown, embeddings, SQLite, citações de fonte e avaliação simples."
date: "2026-05-18"
author: "Equipe python.dev.br"
---

# RAG com Documentos Públicos em Python

Aprenda a criar um RAG em Python com documentos públicos brasileiros, MarkItDown, embeddings, SQLite, citações de fonte e avaliação simples.


Projetos de **RAG (Retrieval-Augmented Generation)** são uma das formas mais práticas de transformar Python e IA em algo útil para empresas brasileiras. Em vez de pedir para um LLM responder com base apenas no que ele já sabe, você cria um pipeline que busca trechos relevantes em documentos próprios ou públicos e envia esses trechos como contexto para o modelo.

Isso muda a qualidade do projeto. Um chatbot genérico que responde sobre qualquer coisa é fácil de demonstrar, mas difícil de confiar. Um assistente que consulta PDFs, planilhas, atas, normas, editais ou dados públicos brasileiros consegue responder com mais precisão, citar fontes e mostrar valor de negócio. Para portfólio, carreira e automação interna, RAG é um dos projetos mais fortes que um desenvolvedor Python pode construir em 2026.

Neste guia, vamos montar uma arquitetura simples de RAG usando documentos públicos, [MarkItDown](/blog/markitdown-microsoft-documentos-markdown-python/), embeddings, SQLite e uma camada de avaliação. Se você ainda está começando em IA com Python, leia também o guia de [APIs de LLMs em Python](/blog/python-e-llms-apis-inteligencia-artificial/) e o roteiro de [como conseguir vaga Python com IA](/carreira/como-conseguir-vaga-python-ia/).

## O que é RAG?

RAG é uma técnica que combina busca de informação com geração de texto. O fluxo básico é:

1. carregar documentos;
2. quebrar o texto em pedaços menores, chamados chunks;
3. transformar cada chunk em um vetor numérico, chamado embedding;
4. salvar esses vetores em uma base pesquisável;
5. receber uma pergunta do usuário;
6. buscar os chunks mais parecidos com a pergunta;
7. enviar pergunta + trechos encontrados para o LLM;
8. gerar uma resposta citando as fontes usadas.

A vantagem é que o modelo não precisa “lembrar” tudo. Ele recebe o contexto certo na hora certa. Isso reduz alucinações, permite usar documentos recentes e cria rastreabilidade: quando a resposta menciona uma informação, você consegue mostrar de onde ela veio.

## Bons documentos públicos para um projeto brasileiro

Para um portfólio brasileiro, documentos públicos são melhores do que exemplos artificiais. Você pode usar:

- manuais e relatórios em PDF do Instituto Brasileiro de Geografia e Estatística (IBGE);
- normas, perguntas frequentes e comunicados de órgãos públicos;
- atas e proposições legislativas;
- editais municipais ou universitários;
- séries e metadados do Banco Central do Brasil (BCB);
- bases e arquivos catalogados no dados.gov.br;
- documentos de transparência, licitações e prestação de contas.

A ideia não é criar aconselhamento jurídico, financeiro ou médico. O melhor escopo para começar é informacional: “encontre a regra no edital”, “resuma os requisitos”, “liste prazos citados”, “explique este relatório em linguagem simples” ou “compare dois documentos”. Quando o tema for sensível, deixe claro que a resposta é uma ajuda de leitura, não uma decisão profissional.

## Arquitetura mínima do projeto

Um RAG pequeno pode rodar localmente com poucos componentes:

- **MarkItDown** para converter PDFs, DOCX e planilhas em Markdown;
- **Python** para orquestrar ingestão, chunking e busca;
- **SQLite** para guardar documentos, chunks e metadados;
- **NumPy** para calcular similaridade entre vetores;
- uma API de embeddings e uma API de LLM;
- uma interface simples, que pode ser CLI, FastAPI ou Streamlit.

Em produção, você pode trocar SQLite por Postgres com pgvector, Qdrant, Chroma ou outro banco vetorial. Mas para aprender, validar e mostrar no GitHub, SQLite é suficiente. Ele também deixa o projeto fácil de reproduzir por recrutadores e clientes.

## Preparando o ambiente

Crie um projeto limpo com `uv` ou `venv`:

```bash
uv init rag-documentos-publicos
cd rag-documentos-publicos
uv add markitdown openai numpy python-dotenv rich
```

Guarde a chave da API em `.env`:

```bash
OPENAI_API_KEY="sua-chave-aqui"
```

Nunca commite chaves no GitHub. Se o projeto for para portfólio, inclua um `.env.example` com os nomes das variáveis e explique no README como configurar o ambiente. Esse cuidado pesa positivamente porque mostra maturidade profissional, não apenas habilidade de copiar código.

## Convertendo documentos para Markdown

O primeiro passo é transformar documentos em texto estruturado. Com MarkItDown:

```python
from pathlib import Path
from markitdown import MarkItDown


def converter_documento(caminho: str) -> dict:
    arquivo = Path(caminho)
    conversor = MarkItDown()
    resultado = conversor.convert(str(arquivo))

    return {
        "nome": arquivo.name,
        "origem": str(arquivo),
        "texto": resultado.text_content,
    }


doc = converter_documento("dados/edital.pdf")
print(doc["texto"][:1000])
```

Markdown ajuda porque preserva títulos, listas e tabelas melhor do que texto puro. Para RAG, essa estrutura é importante: um chunk que começa em um título de seção costuma ser mais útil do que um trecho cortado aleatoriamente no meio de uma tabela.

## Quebrando o texto em chunks

Chunks pequenos demais perdem contexto. Chunks grandes demais poluem a resposta e custam mais tokens. Um ponto de partida razoável é usar blocos de 600 a 1.000 palavras com sobreposição pequena.

```python
import re


def normalizar_espacos(texto: str) -> str:
    return re.sub(r"\s+", " ", texto).strip()


def criar_chunks(texto: str, tamanho: int = 800, sobreposicao: int = 120) -> list[str]:
    palavras = normalizar_espacos(texto).split()
    chunks = []
    inicio = 0

    while inicio < len(palavras):
        fim = inicio + tamanho
        chunk = " ".join(palavras[inicio:fim])
        if len(chunk) > 200:
            chunks.append(chunk)
        inicio = fim - sobreposicao

    return chunks


chunks = criar_chunks(doc["texto"])
print(f"Chunks criados: {len(chunks)}")
```

Em projetos reais, vale melhorar essa função para respeitar cabeçalhos Markdown, páginas e seções. Mas a versão acima já funciona para um protótipo honesto.

## Salvando documentos e embeddings no SQLite

SQLite não é um banco vetorial completo, mas consegue guardar os vetores como JSON. Para uma base pequena, calcular similaridade em memória é suficiente.

```python
import json
import sqlite3
from openai import OpenAI

client = OpenAI()


def conectar():
    conn = sqlite3.connect("rag.db")
    conn.execute("""
        CREATE TABLE IF NOT EXISTS chunks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            documento TEXT NOT NULL,
            origem TEXT NOT NULL,
            posicao INTEGER NOT NULL,
            texto TEXT NOT NULL,
            embedding TEXT NOT NULL
        )
    """)
    return conn


def gerar_embedding(texto: str) -> list[float]:
    resposta = client.embeddings.create(
        model="text-embedding-3-small",
        input=texto,
    )
    return resposta.data[0].embedding


def indexar_documento(caminho: str):
    doc = converter_documento(caminho)
    chunks = criar_chunks(doc["texto"])
    conn = conectar()

    for posicao, texto in enumerate(chunks):
        embedding = gerar_embedding(texto)
        conn.execute(
            """
            INSERT INTO chunks (documento, origem, posicao, texto, embedding)
            VALUES (?, ?, ?, ?, ?)
            """,
            (doc["nome"], doc["origem"], posicao, texto, json.dumps(embedding)),
        )

    conn.commit()
    conn.close()
```

Para muitos arquivos, faça cache e evite gerar embeddings repetidos. Embeddings custam dinheiro e tempo. Uma boa melhoria é salvar um hash do conteúdo e só reindexar quando o arquivo mudar.

## Buscando trechos relevantes

A busca usa similaridade de cosseno entre o embedding da pergunta e os embeddings dos chunks.

```python
import numpy as np


def similaridade_cosseno(a: list[float], b: list[float]) -> float:
    va = np.array(a)
    vb = np.array(b)
    return float(np.dot(va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb)))


def buscar_contexto(pergunta: str, limite: int = 5) -> list[dict]:
    embedding_pergunta = gerar_embedding(pergunta)
    conn = conectar()
    linhas = conn.execute(
        "SELECT documento, origem, posicao, texto, embedding FROM chunks"
    ).fetchall()
    conn.close()

    resultados = []
    for documento, origem, posicao, texto, embedding_json in linhas:
        score = similaridade_cosseno(embedding_pergunta, json.loads(embedding_json))
        resultados.append({
            "documento": documento,
            "origem": origem,
            "posicao": posicao,
            "texto": texto,
            "score": score,
        })

    return sorted(resultados, key=lambda item: item["score"], reverse=True)[:limite]
```

Esse código não escala para milhões de chunks, mas escala o suficiente para um projeto de portfólio com dezenas de documentos. Quando a base crescer, use um índice vetorial de verdade.

## Gerando resposta com fontes

Agora vem a parte que diferencia um RAG útil de um chatbot solto: a resposta deve se limitar ao contexto recuperado e citar os documentos.

```python

def responder(pergunta: str) -> str:
    trechos = buscar_contexto(pergunta)
    contexto = "\n\n".join(
        f"[Fonte {i+1}: {t['documento']} — trecho {t['posicao']}]\n{t['texto']}"
        for i, t in enumerate(trechos)
    )

    prompt = f"""
Você é um assistente que responde apenas com base nos trechos abaixo.
Se a resposta não estiver nos trechos, diga que não encontrou informação suficiente.
Responda em português brasileiro e cite as fontes no formato [Fonte 1], [Fonte 2].

PERGUNTA:
{pergunta}

TRECHOS:
{contexto}
"""

    resposta = client.chat.completions.create(
        model="gpt-4.1-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.2,
    )
    return resposta.choices[0].message.content


print(responder("Quais são os requisitos principais do edital?"))
```

A temperatura baixa reduz criatividade indesejada. A instrução “se não estiver nos trechos, diga que não encontrou” ajuda a evitar respostas inventadas. Ainda assim, não confie cegamente: RAG melhora a rastreabilidade, mas não elimina erros.

## Avaliação simples antes de mostrar o projeto

Muita gente pula avaliação. Isso é um erro. Mesmo um projeto pequeno precisa de um arquivo `perguntas_teste.json` com perguntas esperadas.

```json
[
  {
    "pergunta": "Qual é o prazo de inscrição?",
    "deve_conter": ["inscrição", "prazo"],
    "fonte_esperada": "edital.pdf"
  },
  {
    "pergunta": "Quais documentos são exigidos?",
    "deve_conter": ["documentos", "comprovante"],
    "fonte_esperada": "edital.pdf"
  }
]
```

Depois, rode essas perguntas sempre que mudar chunking, modelo ou prompt. O objetivo não é provar que está perfeito. É demonstrar disciplina: você mede se o sistema continua encontrando as informações certas.

## Como transformar isso em portfólio

Um bom projeto de RAG para portfólio precisa de mais do que código funcionando. Inclua:

- README com problema, arquitetura e limitações;
- screenshots ou vídeo curto da interface;
- exemplos de documentos usados e links para fontes públicas;
- instruções para rodar localmente;
- avaliação com perguntas de teste;
- aviso claro sobre uso informacional;
- logs básicos de custo, tempo de indexação e número de chunks.

Se quiser evoluir, crie uma API com [FastAPI](/blog/apis-rest-com-fastapi/), uma interface com [Streamlit](/blog/criando-dashboards-com-streamlit/) e testes com [pytest](/blog/testes-unitarios-python/). Para comparar alternativas de backend e performance em sistemas de IA, também vale estudar como outras linguagens do portfólio lidam com serviços de alta concorrência, como <a href="https://golang.com.br/aprenda/concorrencia-go/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">concorrência em Go</a> e <a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust para componentes de performance crítica</a>.

## Conclusão

RAG com documentos públicos é um projeto excelente porque junta habilidades que o mercado brasileiro já valoriza: Python, APIs de IA, tratamento de documentos, busca, dados locais e explicabilidade. Ele também foge da armadilha do “chatbot genérico”. Você mostra um problema concreto, fontes verificáveis e uma solução que pode virar ferramenta interna para jurídico, RH, compras, educação, dados ou atendimento.

Comece pequeno: um conjunto de PDFs, SQLite, embeddings e uma CLI. Depois adicione interface, avaliação, cache, filtros por fonte e deploy. O mais importante é manter a resposta conectada aos documentos. Em IA aplicada, confiança não vem de uma demo bonita; vem de conseguir mostrar de onde cada resposta saiu.
