Um pipeline de Retrieval-Augmented Generation para perguntas e respostas sobre documentos PDF, TXT e Markdown, construído com LangChain LCEL e ChromaDB.
Large language models alucinam. Eles geram respostas que soam plausíveis mas são factualmente incorretas, porque o modelo não tem acesso aos seus dados privados e nenhum mecanismo para verificar suas afirmações contra uma fonte de verdade. Retrieval-Augmented Generation resolve isso injetando fragmentos relevantes de documentos no prompt em tempo de inferência, fundamentando a resposta do LLM em material de origem real. Essa abordagem reduz alucinações, fornece citações rastreáveis e permite construir sistemas de Q&A sobre documentos proprietários sem fine-tuning.
graph LR
User -->|document| Ingest[Ingest Pipeline]
Ingest --> Loader[Document Loader]
Loader --> Splitter[Recursive Text Splitter]
Splitter --> Embeddings[OpenAI Embeddings]
Embeddings --> Store[ChromaDB Vector Store]
User -->|question| Query[Query Pipeline]
Query --> Embeddings2[OpenAI Embeddings]
Embeddings2 --> Retriever[ChromaDB Retriever]
Retriever --> Store
Retriever -->|top-k docs| Chain[LCEL RAG Chain]
Chain --> LLM[ChatOpenAI]
Chain -->|answer + sources| User
API[FastAPI Server] --> Ingest
API --> Query
CLI[Typer CLI] --> Ingest
CLI --> Query
Fluxo de dados: Os documentos entram pelo pipeline de ingestão, onde são carregados (PDF via PyPDF, TXT/MD via TextLoader), divididos em chunks sobrepostos usando RecursiveCharacterTextSplitter, transformados em embeddings com o modelo de embeddings da OpenAI e armazenados no ChromaDB. No momento da consulta, a pergunta é transformada em embedding, os top-k chunks mais similares são recuperados, e esses chunks são inseridos no prompt do LLM como contexto para geração da resposta.
LangChain LCEL ao invés de orquestração manual. LCEL (LangChain Expression Language) fornece um modelo declarativo de composição de chains que lida com templating de prompts, recuperação e geração de respostas em um único pipeline componível. A alternativa -- orquestrar manualmente chamadas de embedding, consultas ao vector store e invocações do LLM -- exige mais boilerplate e é mais difícil de estender com funcionalidades como streaming ou callbacks. O LCEL também se integra nativamente com o LangSmith para observabilidade.
ChromaDB ao invés de Pinecone/Weaviate. O ChromaDB roda in-process com zero infraestrutura. Sem chaves de API, sem serviço hospedado, sem latência de rede para operações vetoriais. Para um projeto de portfólio e desenvolvimento local, isso remove uma classe inteira de fricção de setup. O Pinecone seria a escolha certa para workloads em escala de produção com milhões de vetores, mas o modelo local-first do ChromaDB é ideal aqui.
Divisão recursiva de texto. O RecursiveCharacterTextSplitter tenta dividir em fronteiras naturais (parágrafos, sentenças, palavras) antes de recorrer à divisão por caractere. Isso preserva a coerência semântica dentro dos chunks melhor do que a divisão ingênua de tamanho fixo, que pode cortar sentenças ao meio e degradar a qualidade da recuperação.
Os valores padrão são chunk_size=1000 e chunk_overlap=200:
- 1000 caracteres equivalem a aproximadamente 250 tokens, o que cabe confortavelmente na janela de contexto e é grande o suficiente para conter um pensamento completo ou parágrafo. Chunks menores (256 caracteres) melhoram a precisão da recuperação mas perdem contexto. Chunks maiores (2000+ caracteres) fornecem mais contexto por resultado mas reduzem o número de passagens distintas que o retriever pode trazer.
- 200 caracteres de sobreposição garantem que informações que cruzam a fronteira de um chunk sejam capturadas em pelo menos um chunk. A razão de sobreposição de 20% é um padrão bem testado que equilibra o overhead de deduplicação com a perda de informação nas fronteiras.
Esses valores são configuráveis via variáveis de ambiente para ajuste conforme tipos específicos de documentos.
- ChromaDB in-process vs client-server: O modo in-process usado aqui persiste em disco, mas não suporta escritores concorrentes ou escala horizontal. Para deployments de produção multi-usuário, o modo client-server do ChromaDB ou um banco de dados vetorial gerenciado seria necessário.
- Escolha do modelo de embedding:
text-embedding-3-smalloferece um bom equilíbrio entre custo e qualidade.text-embedding-3-largeproduz embeddings de maior qualidade a aproximadamente 5x o custo. A escolha impacta diretamente a relevância da recuperação. - Sem etapa de reranking: O pipeline recupera os top-k chunks por similaridade de cosseno e os passa diretamente ao LLM. Adicionar um reranker cross-encoder (ex.: Cohere Rerank) entre a recuperação e a geração melhoraria a qualidade das respostas ao custo de latência adicional e chamadas de API.
- Estratégia stuff chain: Todos os documentos recuperados são concatenados em um único prompt. Isso funciona bem para valores pequenos de
k, mas atinge os limites da janela de contexto comkgrande ou documentos longos. Para esses casos, uma estratégia map-reduce ou refine seria mais apropriada.
- Otimização do pipeline RAG: Tamanho do chunk, sobreposição e
kdo retriever são os principais parâmetros de ajuste. Pequenas mudanças nesses parâmetros podem afetar significativamente a qualidade das respostas, e os valores ótimos dependem do tipo de documento e da complexidade da pergunta. - Seleção do modelo de embedding: Diferentes modelos de embedding produzem diferentes espaços vetoriais. Trocar de modelo exige re-indexação de todos os documentos, tornando esta uma decisão inicial crítica.
- Composição de chains com LCEL: A linguagem de expressão do LangChain torna simples compor recuperação, prompting e geração em uma única chain invocável, com suporte nativo para streaming e execução assíncrona.
- Persistência do vector store: O modelo de persistência do ChromaDB é baseado em diretórios, o que simplifica backup e portabilidade, mas exige tratamento cuidadoso de acesso concorrente.
cd rag-document-qa
uv sync
cp .env.example .env
# Edit .env and add your OPENAI_API_KEY# Ingest a document
uv run rag-qa ingest path/to/document.pdf
# Query the documents
uv run rag-qa query "What is the main topic of the document?"
# Start the API server
uv run rag-qa serve# Ingest a document
curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-d '{"file_path": "/path/to/document.pdf"}'
# Query documents
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question": "What is the main topic?", "k": 4}'
# Health check
curl http://localhost:8000/healthdocker compose upuv sync --dev
uv run pytest --cov=src -v
uv run ruff check .
uv run ruff format .
uv run mypy src/| Variável | Padrão | Descrição |
|---|---|---|
OPENAI_API_KEY |
- | Chave de API da OpenAI (obrigatório) |
OPENAI_BASE_URL |
None |
URL base customizada compatível com API OpenAI |
EMBEDDING_MODEL |
text-embedding-3-small |
Modelo de embedding para vetorização |
CHAT_MODEL |
gpt-4o-mini |
Modelo de chat para geração de respostas |
CHROMA_PERSIST_DIR |
./chroma_data |
Diretório de armazenamento do ChromaDB |
CHROMA_COLLECTION |
documents |
Nome da coleção no ChromaDB |
CHUNK_SIZE |
1000 |
Tamanho do chunk de documento em caracteres |
CHUNK_OVERLAP |
200 |
Sobreposição entre chunks adjacentes |
RETRIEVER_K |
4 |
Número de documentos recuperados por consulta |
MIT