RAG na Prática: Como construir uma IA que aprende com seus próprios documentos usando Python

Compartilhar:


Imagine que você contrata um assistente extremamente inteligente, mas que nunca leu nenhum documento da sua empresa. Ele responde bem sobre assuntos gerais, mas quando você pergunta sobre uma portaria interna, uma lei específica ou uma ficha funcional, ele chuta. Frustrante, né?

É exatamente esse problema que o RAG — Retrieval-Augmented Generation resolve. Em vez de tentar ensinar tudo ao modelo durante o treinamento, você dá a ele acesso a uma biblioteca particular. Na hora de responder, ele busca o que precisa nessa biblioteca e responde com base no que encontrou — indicando a fonte, sem inventar.

Exemplo real
Você tem 500 portarias municipais em PDF. Com RAG, qualquer pessoa pode perguntar: “Qual é o prazo do estágio probatório para Agente Administrativo?” — e a IA responde citando exatamente a portaria correta, sem alucinar.

O fluxo completo de um RAG

Antes de entrar no código, vale visualizar o caminho que um documento percorre até virar conhecimento pesquisável:

📄
Documento
Bruto
🧹
Limpeza
do Texto
✂️
Chunks
Semânticos
🔢
Embeddings
Vetoriais
🗄️
Banco
Vetorial
🤖
IA Responde
com Contexto

Por que Python?

Todos os exemplos de código deste artigo estão escritos em Python. Não é uma escolha aleatória — Python se tornou a linguagem padrão do ecossistema de IA por razões práticas e concretas:

Motivo O que significa na prática
Bibliotecas prontas ChromaDB, LangChain, PyMuPDF, Ollama — tudo tem suporte nativo em Python
Sintaxe simples O código é legível mesmo para quem não é programador de formação
Padrão da indústria OpenAI, Google, Meta e Anthropic publicam seus SDKs oficiais em Python primeiro
Comunidade ativa Qualquer dúvida ou erro tem documentação, fórum ou exemplo disponível online
Não precisa ser programador
Os scripts deste artigo são diretos e comentados. Com Python instalado e as bibliotecas certas, qualquer pessoa com noção básica de terminal consegue rodar um RAG funcional seguindo estes exemplos.

Quais documentos podem entrar no RAG?

Praticamente qualquer coisa que vira texto. No contexto de gestão pública ou de negócios, os mais comuns são:

Tipo Exemplos Reais Biblioteca Python
PDF Portarias, editais, leis, pareceres PyMuPDF
DOCX Fichas funcionais, contratos, memorandos python-docx
CSV / XLSX Tabelas de cargos e salários pandas
TXT / MD Bases de conhecimento internas, wikis nativo Python
Site / HTML FAQ, regulamentos online BeautifulSoup

Dados brutos ou filtrados?

Dúvida mais comum de quem está começando: devo inserir o arquivo direto ou limpar antes? A resposta direta: nunca jogue o texto bruto direto no banco. O problema não é o volume — é o ruído.

Quando você extrai o texto de um PDF de portaria, vem muito mais do que o conteúdo útil:

Texto Bruto (PDF) Texto Filtrado
❌ Bruto Fl. 23 …….. Rubrica ……..
PORTARIA Nº 0042/2024
Página 23 de 180
✓ Limpo PORTARIA Nº 0042/2024 – SMGP
Art. 1º Fica autorizado o servidor
João Silva, matrícula 12345…

Se o lixo entra, o lixo é recuperado. A IA vai citar número de página no meio de uma resposta ou confundir marcas d’água com conteúdo real.

Chunking: o passo mais crítico

Chunk é o pedaço de texto que vai para o banco vetorial. Se o chunking for mal feito, o RAG inteiro fica comprometido — mesmo com um ótimo modelo.

Por que chunks importam tanto?
A busca vetorial retorna pedaços, não documentos inteiros. Se um chunk cortar um artigo de lei no meio, o contexto vai incompleto para a IA — e ela vai preencher o que falta com imaginação. O tamanho ideal fica entre 300 e 800 caracteres, com um overlap de 15 a 20% entre chunks consecutivos para garantir continuidade.

Um script ou vários? Dois — e cada um tem seu momento

O fluxo de um RAG se divide naturalmente em duas etapas com propósitos distintos. Você não precisa rodar tudo toda vez — cada script tem seu momento de uso:

📥 ingestao.py

Roda só quando tem documento novo. Extrai o texto, limpa, divide em chunks e insere no banco com a origem registrada. O banco persiste no disco — os dados ficam lá até você deletar.

🔍 consulta.py

Roda sempre que o usuário faz uma pergunta. Busca os chunks mais relevantes, monta o prompt com o contexto encontrado e chama o modelo para gerar a resposta. Não precisa reingerir nada.

📥 ingestao.py — script completo

Este script faz tudo em sequência: extrai o texto do arquivo, limpa o conteúdo removendo ruídos típicos de PDFs públicos, divide em chunks semânticos e insere no ChromaDB. O ponto central é que cada chunk entra no banco junto com a origem do documento — sem isso, a IA responde sem conseguir indicar de onde veio a informação.

# ingestao.py
# Roda uma vez por documento novo.
# Extrai limpa chunka insere no banco com a origem registrada.
import re
import chromadb
from chromadb.utils import embedding_functions
# Conexao com o banco (persiste em disco automaticamente)
client = chromadb.PersistentClient(path=“./rag_banco”)
embedding_fn = embedding_functions.OllamaEmbeddingFunction(
url=“http://localhost:11434/api/embeddings”,
model_name=“nomic-embed-text”
)
colecao = client.get_or_create_collection(
name=“documentos”,
embedding_function=embedding_fn
)
# Etapa 1: limpeza
def limpar(texto):
texto = re.sub(r‘Pagina \d+ de \d+’, , texto) # rodapes
texto = re.sub(r‘Fl\.\s*\d+’, , texto) # numeracao de folhas
texto = re.sub(r‘Rubrica.*’, , texto) # marcas de rubrica
texto = re.sub(r‘\n{3,}’, ‘\n\n’, texto) # linhas em branco extras
texto = re.sub(r‘[ \t]{2,}’, ‘ ‘, texto) # espacos excessivos
return texto.strip()
# Etapa 2: divisao em chunks semanticos
def chunkar(texto):
# Divide por paragrafo, respeitando a estrutura natural do documento
paragrafos = texto.split(‘\n\n’)
chunks = []
atual = “”
for p in paragrafos:
if len(atual) + len(p) < 800:
atual += p + “\n\n”
else:
if atual:
chunks.append(atual.strip())
atual = p + “\n\n”
if atual:
chunks.append(atual.strip())
return chunks
# Etapa 3: ingestao com origem registrada
def ingerir(caminho_arquivo, nome_fonte):
with open(caminho_arquivo, ‘r’, encoding=‘utf-8) as f:
texto = f.read()
texto = limpar(texto)
chunks = chunkar(texto)
# Cada chunk entra no banco junto com a origem do documento.
# Sem isso, a IA responde mas nao consegue indicar a fonte.
colecao.add(
documents=chunks,
ids=[f“{nome_fonte}_chunk_{i}” for i in range(len(chunks))],
metadatas=[{“fonte”: nome_fonte} for _ in chunks]
)
print(f“✓ {len(chunks)} chunks inseridos — origem: ‘{nome_fonte}'”)
# Uso: rode uma vez por documento novo
ingerir(“portaria_0042_2024.txt”, “Portaria no 0042/2024)
ingerir(“lei_complementar_68.txt”, “Lei Complementar no 68)
ingerir(“ficha_funcional.txt”, “Ficha Funcional – Modelo v2”)

🔍 consulta.py — script completo

Este script roda sempre que o usuário faz uma pergunta. Ele busca os chunks mais similares no banco, monta um prompt instruindo o modelo a responder apenas com base no que foi encontrado, e exibe a resposta junto com as fontes consultadas.

# consulta.py
# Roda sempre que o usuario faz uma pergunta.
# Busca chunks monta prompt com contexto chama o modelo.
import requests
import chromadb
from chromadb.utils import embedding_functions
# Conexao com o mesmo banco da ingestao
client = chromadb.PersistentClient(path=“./rag_banco”)
embedding_fn = embedding_functions.OllamaEmbeddingFunction(
url=“http://localhost:11434/api/embeddings”,
model_name=“nomic-embed-text”
)
colecao = client.get_collection(
name=“documentos”,
embedding_function=embedding_fn
)
# Etapa 1: busca os chunks mais relevantes
def buscar(pergunta, n=3):
resultado = colecao.query(
query_texts=[pergunta],
n_results=n
)
contextos = []
for texto, meta in zip(
resultado[‘documents’][0],
resultado[‘metadatas’][0]
):
contextos.append({
“texto”: texto,
“fonte”: meta[“fonte”] # origem registrada na ingestao
})
return contextos
# Etapa 2: monta o prompt e chama o modelo
def perguntar(pergunta):
contextos = buscar(pergunta)
# Cada trecho vai identificado com sua origem
contexto_str = “\n\n—\n\n”.join([
f“[Fonte: {c[‘fonte’]}]\n{c[‘texto’]}”
for c in contextos
])
prompt = (
“Responda com base apenas nos documentos abaixo.\n”
“Se a resposta nao estiver nos documentos, diga que nao encontrou.\n”
“Ao responder, indique de qual documento veio a informacao.\n\n”
f“DOCUMENTOS:\n{contexto_str}\n\n”
f“PERGUNTA: {pergunta}\n\nRESPOSTA:”
)
response = requests.post(
“http://localhost:11434/api/generate”,
json={“model”: “llama3”, “prompt”: prompt, “stream”: False}
)
resposta = response.json()[‘response’]
# Exibe a resposta e as fontes consultadas
print(“\n📌 Resposta:”)
print(resposta)
print(“\n📂 Fontes consultadas:”)
for c in contextos:
print(f” · {c[‘fonte’]}”)
# Uso
perguntar(“Qual e o prazo do estagio probatorio para Agente Administrativo?”)
perguntar(“A portaria 0042 se aplica a servidores comissionados?”)
perguntar(“Quais cargos tem adicional de insalubridade conforme a lei 68?”)

O que aparece na tela

Com o script acima, a saída para cada pergunta terá este formato:

📌 Resposta:
O estágio probatório para o cargo de Agente Administrativo
tem duração de 36 meses, conforme o Art. 1º da Portaria nº 0042/2024.

📂 Fontes consultadas:
  · Portaria nº 0042/2024
  · Lei Complementar nº 68

Os erros que destroem um RAG

Erro Efeito Correção
Chunk < 100 chars Perde contexto, resposta fragmentada Mínimo 300–400 chars
Chunk > 1500 chars Dilui o sinal, busca imprecisa Máximo 800–1000 chars
Texto bruto sem limpeza IA recupera rodapés e lixo Sempre pré-processar
Ingerir sem registrar a origem IA responde sem indicar a fonte Sempre salvar o nome do documento junto ao chunk
Prompt sem âncora LLM alucina fora do contexto “Responda só com os documentos”

RAG é infraestrutura, não magia

A diferença entre um RAG que funciona e um que decepciona está quase sempre na qualidade dos dados inseridos — não no modelo. Limpe bem, chunke com critério, registre sempre a origem do documento, e você terá uma IA que responde com a precisão de quem leu cada página dos seus arquivos.

Com dois scripts simples em Python — um para ingestão, um para consulta — qualquer organização consegue transformar sua base de documentos em uma fonte de conhecimento pesquisável por IA, sem depender de nenhuma plataforma externa.