RAG com FastAPI e pgvector em Produção
Aprenda a transformar um protótipo de RAG em uma API Python com FastAPI, PostgreSQL, pgvector, filtros, citações, avaliação e cuidados de produção.
Criar um protótipo de RAG (Retrieval-Augmented Generation) em Python ficou relativamente simples: você converte documentos, gera embeddings, salva vetores em algum lugar e envia os trechos recuperados para um LLM. O problema aparece quando esse protótipo precisa virar uma API usada por outras pessoas. Aí entram autenticação, limites de custo, tempo de resposta, rastreabilidade, atualização dos documentos, filtros por cliente e uma pergunta essencial: como saber se a resposta é confiável o suficiente para aparecer em produção?
Para muitos projetos brasileiros, a combinação FastAPI + PostgreSQL + pgvector é um caminho prático. FastAPI entrega uma API clara, tipada e fácil de documentar. PostgreSQL já é conhecido por times de backend e dados. O pgvector adiciona busca vetorial sem obrigar a equipe a operar um banco separado logo no começo. Essa arquitetura não resolve todos os cenários, mas é forte para produtos internos, portais de conhecimento, assistentes sobre editais, bases de suporte, documentação técnica e projetos de portfólio mais maduros.
Este guia continua o artigo de RAG com documentos públicos em Python e aprofunda a parte de produção: endpoints, modelo de dados, ingestão, busca com filtros, resposta com citações, avaliação e deploy. Se você ainda não viu os fundamentos de LLMs, leia também Python e LLMs e o tutorial de APIs REST com FastAPI.
Arquitetura recomendada
Um RAG de produção não deve ser um notebook gigante. Separe o sistema em quatro partes:
- ingestão: recebe documentos, converte texto, quebra em chunks e gera embeddings;
- armazenamento: salva documentos, chunks, metadados e vetores no PostgreSQL;
- recuperação: recebe uma pergunta, aplica filtros e busca os chunks mais parecidos;
- geração: monta o prompt, chama o LLM e devolve resposta com fontes.
Essa separação evita que uma pergunta do usuário dispare processamento pesado de documento. Ingestão é trabalho assíncrono ou administrativo. Pergunta e resposta precisam ser rápidas, observáveis e previsíveis.
Em projetos pequenos, todos os componentes podem ficar no mesmo repositório. Em empresas, é comum separar o worker de ingestão da API pública. Python continua sendo uma boa escolha para os dois, mas os requisitos mudam: o worker tolera latência maior; a API precisa responder com timeout curto e mensagens de erro claras.
Modelo de dados com pgvector
Depois de instalar a extensão vector no PostgreSQL, crie tabelas simples para documentos e chunks. Um desenho inicial pode ser assim:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documentos (
id BIGSERIAL PRIMARY KEY,
titulo TEXT NOT NULL,
origem TEXT NOT NULL,
tenant_id TEXT NOT NULL,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE chunks (
id BIGSERIAL PRIMARY KEY,
documento_id BIGINT NOT NULL REFERENCES documentos(id),
tenant_id TEXT NOT NULL,
texto TEXT NOT NULL,
pagina INTEGER,
secao TEXT,
embedding vector(1536),
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX chunks_embedding_idx
ON chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
O campo tenant_id é importante mesmo em projetos pequenos. Ele impede misturar documentos de clientes, departamentos ou bases diferentes. Um assistente de RH não deve recuperar trecho de uma base financeira por acidente. Mesmo que o primeiro deploy tenha um único tenant, modelar isso cedo evita uma refatoração cara.
O tamanho vector(1536) depende do modelo de embeddings usado. Se você trocar para outro modelo com dimensão diferente, precisará migrar a coluna ou criar uma coluna nova. Por isso, registre também em algum lugar qual modelo gerou cada embedding.
Endpoint de pergunta com FastAPI
O endpoint principal deve receber pergunta, contexto de acesso e parâmetros controlados. Evite deixar o usuário escolher qualquer valor de top_k, modelo ou temperatura.
from pydantic import BaseModel, Field
class PerguntaRequest(BaseModel):
pergunta: str = Field(min_length=5, max_length=1200)
tenant_id: str
top_k: int = Field(default=6, ge=2, le=10)
class Fonte(BaseModel):
documento: str
trecho: str
pagina: int | None = None
score: float
class PerguntaResponse(BaseModel):
resposta: str
fontes: list[Fonte]
Com esse contrato, o frontend sabe que sempre receberá resposta e fontes. A API também limita abuso: pergunta gigante, top_k exagerado e tenant ausente são rejeitados antes de chamar embedding ou LLM.
O handler pode seguir este fluxo:
from fastapi import FastAPI, HTTPException
app = FastAPI(title="RAG API", version="1.0.0")
@app.post("/perguntas", response_model=PerguntaResponse)
async def responder(payload: PerguntaRequest):
embedding = await gerar_embedding(payload.pergunta)
fontes = await buscar_chunks(
embedding=embedding,
tenant_id=payload.tenant_id,
top_k=payload.top_k,
)
if not fontes:
raise HTTPException(
status_code=404,
detail="Não encontrei trechos relevantes para responder com segurança.",
)
resposta = await gerar_resposta_com_fontes(payload.pergunta, fontes)
return PerguntaResponse(resposta=resposta, fontes=fontes)
Na prática, gerar_embedding, buscar_chunks e gerar_resposta_com_fontes devem ter timeout, retry limitado e logs. Não deixe uma chamada externa travar indefinidamente. Em produção, timeout previsível é melhor do que uma requisição pendurada por minutos.
Busca vetorial com filtros
O erro mais comum em RAG de produção é fazer busca vetorial sem filtro. A similaridade semântica encontra texto parecido, mas não entende permissão de acesso, versão do documento ou escopo do produto. Sempre aplique filtros antes de ordenar por distância.
SELECT
c.texto,
c.pagina,
c.secao,
d.titulo AS documento,
1 - (c.embedding <=> $1) AS score
FROM chunks c
JOIN documentos d ON d.id = c.documento_id
WHERE c.tenant_id = $2
ORDER BY c.embedding <=> $1
LIMIT $3;
Esse exemplo usa distância de cosseno. O filtro por tenant_id vem antes do limite, garantindo que a API não retorne documento de outro escopo. Você pode adicionar filtros por data, tipo de documento, status, idioma ou categoria.
Para bases pequenas, uma busca exata pode ser suficiente. Para bases maiores, o índice aproximado (ivfflat ou hnsw, dependendo da versão e configuração) melhora desempenho, mas exige testes. Não escolha índice só por moda: meça latência, qualidade de recuperação e custo de manutenção.
Prompt com citações e recusa
Um bom prompt de RAG não pede apenas “responda”. Ele define limites:
Você é um assistente técnico. Responda em português brasileiro.
Use apenas os trechos fornecidos em CONTEXTO.
Se o contexto não for suficiente, diga que não há informação suficiente.
Cite as fontes pelo número do trecho ao final das frases relevantes.
Não invente dados, datas, valores ou regras.
Essa instrução reduz alucinação, mas não elimina o risco. Por isso a API deve devolver as fontes separadamente. O usuário precisa poder abrir o documento original, revisar o trecho e verificar se a resposta faz sentido.
Para temas sensíveis, como finanças, saúde, jurídico ou decisões trabalhistas, o cuidado deve ser maior. Um RAG pode ajudar a localizar e resumir informação, mas não deve substituir profissional habilitado. Mesmo em um site de programação, vale projetar o sistema com essa fronteira clara.
Avaliação antes do deploy
Antes de publicar, monte uma lista de perguntas esperadas. Para cada pergunta, registre:
- resposta ideal ou critérios de aceitação;
- documentos que deveriam aparecer nas fontes;
- casos em que o sistema deveria recusar;
- tempo máximo aceitável;
- custo aproximado por pergunta.
Um script simples de avaliação já ajuda muito. Ele executa perguntas fixas, salva resposta, fontes e latência, depois compara se os documentos esperados foram recuperados. Isso não substitui avaliação humana, mas detecta regressões quando você muda chunking, modelo de embedding ou prompt.
Também registre perguntas sem resposta. Em RAG, recusar corretamente é recurso, não falha. Se o documento não contém a informação, a resposta correta é explicar a limitação. Para portfólio, mostrar exemplos de “não sei com segurança” passa mais confiança do que forçar respostas bonitas.
Deploy e observabilidade
Um deploy mínimo pode usar Docker, FastAPI, Uvicorn/Gunicorn e PostgreSQL com pgvector. Se você ainda está estruturando containers, veja o guia de Docker para Python e o conteúdo de Python e PostgreSQL.
Monitore pelo menos estes pontos:
- latência total da pergunta;
- tempo de embedding;
- tempo de busca no banco;
- tempo de resposta do LLM;
- quantidade de tokens;
- número de fontes retornadas;
- erros por tenant;
- perguntas recusadas por falta de contexto.
Logs devem incluir IDs e metadados, não conteúdo sensível. Evite gravar documento privado, pergunta pessoal ou resposta completa se isso violar política interna. Para produto real, pense em retenção, anonimização e exclusão de dados desde o início.
Se a API precisar de alta concorrência, Python ainda funciona bem para I/O assíncrono, mas alguns serviços podem ganhar com outra linguagem no caminho crítico. Em arquiteturas mistas, é natural manter o pipeline de IA em Python e usar Go para serviços de backend concorrentes quando a equipe já domina esse ecossistema.
Checklist de produção
Antes de anunciar que seu RAG está pronto, revise:
- os documentos têm origem, data e identificador;
- chunks preservam seção, página ou link de referência;
- busca vetorial aplica filtros de acesso;
- prompt exige resposta baseada em fontes;
- API retorna citações junto com a resposta;
- chamadas externas têm timeout e retry limitado;
- perguntas sem contexto são recusadas;
- avaliação roda com perguntas fixas;
- custo por pergunta é conhecido;
- logs não vazam dados sensíveis.
Para carreira, esse tipo de projeto é muito mais forte do que um chatbot genérico. Ele mostra backend, banco de dados, IA, segurança, produto e capacidade de pensar em produção. Se a ideia é usar no GitHub, inclua README com arquitetura, variáveis de ambiente, comandos de execução, exemplo de pergunta, limitações conhecidas e prints da documentação do FastAPI. Depois conecte o projeto ao seu roteiro de portfólio Python para conseguir vaga ou ao guia de como conseguir vaga Python com IA.
RAG em produção não é apenas “perguntar para documentos”. É construir um sistema que sabe buscar, responder, citar, recusar e ser operado. FastAPI e pgvector dão uma base simples o suficiente para começar, mas séria o suficiente para crescer quando o protótipo vira ferramenta de verdade.
Equipe python.dev.br
Contribuidor do Python Brasil — Aprenda Python em Português