Como construí um agente RAG para eliminar interrupções operacionais na empresa
Projeto open source com Python, LangChain, ChromaDB, FastAPI e Discord — do problema real ao deploy em produção.
Toda empresa tem aquele ciclo silencioso que drena tempo sem que ninguém perceba.
Um funcionário tem uma dúvida sobre um procedimento. Não encontra a resposta nos documentos. Interrompe alguém mais experiente. Essa pessoa para o que estava fazendo, responde, e volta ao trabalho — já com o raciocínio quebrado. Multiplique isso por 10, 20, 50 vezes por semana.
Foi observando esse padrão que decidi construir o POPS AI: um agente de RAG (Retrieval-Augmented Generation) capaz de responder perguntas sobre os Procedimentos Operacionais Padrão de uma empresa, direto pelo Discord ou via API REST.
O problema que motivou o projeto
A empresa tinha dezenas de POPs documentados em PDF. O problema não era a falta de documentação — era o atrito para acessá-la. Ninguém abre uma pasta de rede, procura o arquivo certo e lê 15 páginas para responder uma dúvida pontual.
A pergunta que me fiz foi simples: e se a documentação pudesse responder sozinha?
A arquitetura em três etapas
O sistema funciona em três fases distintas, cada uma com responsabilidade clara.
1. Extração
O script extrair_texto.py lê os PDFs da pasta pops_originais/, extrai o texto completo com PyMuPDF e salva em .txt. Imagens das páginas também são extraídas para uso futuro.
import fitz # PyMuPDF
def extrair_texto_pdf(caminho_pdf):
doc = fitz.open(caminho_pdf)
texto_completo = ""
for pagina in doc:
texto_completo += pagina.get_text()
return texto_completo
Simples, mas importante: a qualidade da extração determina a qualidade das respostas. PDFs escaneados sem OCR são o inimigo número um aqui.
2. Geração de embeddings
Com os textos extraídos, o gerar_embeddings.py divide o conteúdo em chunks usando o RecursiveCharacterTextSplitter da LangChain, gera os vetores e persiste no ChromaDB.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_text(texto)
O chunk_overlap=200 foi uma decisão deliberada: ele garante que o contexto não seja cortado abruptamente entre um chunk e o próximo, o que melhorou visivelmente a coerência das respostas.
O projeto suporta dois modelos de embedding via config.py:
-
Gemini
models/embedding-001— qualidade alta, requer API key e gera custo por volume -
SBERT local (
paraphrase-multilingual-mpnet-base-v2) — roda offline, ótimo para evitar custos ou limites de requisição
Essa flexibilidade foi uma das decisões de design que mais agregou valor, especialmente para quem quer experimentar o projeto sem gastar nada.
3. Consulta (RAG)
Quando o usuário faz uma pergunta, o sistema:
- Converte a pergunta em vetor usando o mesmo modelo de embedding
- Busca os chunks mais semanticamente similares no ChromaDB
- Monta um prompt com os trechos recuperados como contexto
- Envia para o Gemini 2.0 Flash gerar a resposta final
resultados = collection.query(
query_embeddings=[embedding_pergunta],
n_results=5
)
contexto = "\n\n".join(resultados['documents'][0])
prompt = f"""Você é um assistente especializado nos POPs da empresa.
Use apenas as informações abaixo para responder.
Contexto:
{contexto}
Pergunta: {pergunta}
"""
As interfaces: Discord e API
O projeto expõe a base de conhecimento de duas formas.
Bot do Discord com slash commands:
-
/pop <pergunta>— consulta a base vetorial e retorna a resposta -
/addpop <arquivo.txt>— permite que administradores adicionem novos POPs em tempo real, sem precisar reprocessar toda a base
API FastAPI com endpoint POST /ask, pensada para integrar com outros sistemas internos:
// Request
{ "question": "Como configurar o scanner da impressora Samsung?" }
// Response
{
"answer": "Para configurar o scanner, siga os passos:\n1. Ligue a impressora...\n[Fonte: POP-ConfiguraçãoScanner.txt]"
}
O desafio que ninguém menciona: custo de tokens
Construir o RAG foi a parte divertida. O desafio real veio depois: como controlar o custo em produção?
Algumas decisões que fizeram diferença:
Usar SBERT para embeddings em vez da API do Gemini reduz o custo de indexação para zero — o modelo roda localmente. O custo só existe na geração de resposta, que é onde o valor real está.
Limitar n_results=5 na busca vetorial evita passar contexto desnecessário para o modelo. Mais contexto = mais tokens = mais custo, sem necessariamente melhorar a resposta.
Gemini 2.0 Flash foi escolhido intencionalmente sobre o Pro: para perguntas objetivas sobre procedimentos, a diferença de qualidade é mínima e a diferença de custo é expressiva.
Deploy: um container, dois processos
Uma decisão que me custou algumas horas foi rodar o bot do Discord e a API FastAPI no mesmo container Docker. A solução foi o Supervisor, que gerencia ambos os processos de forma leve e auto-recuperável.
# supervisord.conf
[program:api]
command=uvicorn api_bot:app --host 0.0.0.0 --port 8000
[program:discord]
command=python bot_discord.py
autostart=true
autorestart=true
O resultado é um container único, leve, que sobe os dois serviços em paralelo e reinicia automaticamente qualquer um que falhe. Para uma VPS de entrada, isso faz toda a diferença.
O que aprendi que não estava no plano
Chunking é uma arte. O tamanho e o overlap dos chunks afetam mais a qualidade das respostas do que o modelo em si. Passei mais tempo ajustando isso do que qualquer outra coisa.
Segurança desde o início. O .gitignore precisou ser configurado antes do primeiro commit público para garantir que nenhum PDF com dados confidenciais da empresa fosse parar no repositório. Um erro aqui é difícil de reverter.
O problema real não era técnico. A parte mais complexa foi entender que tipo de pergunta os usuários fariam e como estruturar os POPs para que o modelo conseguisse recuperar as informações certas. Garbage in, garbage out vale dobrado em RAG.
O projeto é open source
O POPS AI está disponível no GitHub com README completo, .env.example, Docker Compose configurado e passo a passo de instalação tanto local quanto via container.
Você pode clonar, adaptar para sua própria base de conhecimento e usar com seus próprios documentos — seja para POPs, wikis internas, manuais de produto ou qualquer documentação em PDF.
Stack utilizada
Python 3.10 LangChain ChromaDB FastAPI Discord.py Google Gemini 2.0 Flash SBERT Docker Supervisor PyMuPDF
Se você chegou até aqui e tem curiosidade sobre alguma decisão de arquitetura, custo de tokens em produção ou como adaptar para um caso de uso diferente — deixa nos comentários. Bora trocar ideia.
Top comments (0)