ADR-0004: Stack de RAG
- Status: Accepted
- Data: 2026-03-18
- Responsáveis pela decisão: Waldemar Szemat
Contexto e Definição do Problema
Seção intitulada “Contexto e Definição do Problema”O agente fundamenta cada afirmação clínica em uma pequena base de conhecimento de 30 a 50 cartões cobrindo resumos de interações entre medicamentos, barreiras de adesão, pontos de conversa de entrevista motivacional e critérios de escalonamento. As fontes da KB são restritas a material de domínio público ou devidamente atribuído: DailyMed (FDA SPL), MedlinePlus (governo dos EUA) e entradas parafraseadas da Lista de Medicamentos Essenciais da WHO. A camada de recuperação não precisa de escalonamento horizontal; ela precisa ser barata, reproduzível e autocontida dentro da imagem Docker que distribuímos.
Ao mesmo tempo, esta é uma implementação de referência. Ela tem que mostrar quando um armazenamento vetorial embarcado é a escolha certa e quando um banco de dados vetorial gerenciado é a escolha certa. A narrativa é “comece embarcado, documente o caminho gerenciado”.
Como escolhemos um armazenamento vetorial e um modelo de embeddings que (a) rodem a $0 sem contas externas na demo padrão, (b) demonstrem consciência de banco de dados vetorial gerenciado como caminho alternativo, (c) correspondam à qualidade à qual o LLM-como-juiz vai nos cobrar e (d) mantenham reprodutibilidade determinística para o harness de avaliação?
Direcionadores da Decisão
Seção intitulada “Direcionadores da Decisão”- Zero serviços externos para o caminho padrão da demo; o armazenamento vetorial deve funcionar dentro da imagem Docker
- Reprodutibilidade: a mesma KB mais o mesmo modelo de embeddings mais a mesma consulta devem produzir a mesma recuperação, para que o avaliador de fundamentação seja estável
- Custo: gratuito na escala da demo (50 cartões ou menos, centenas de consultas por dia), com uma alternativa gerenciada de nível gratuito documentada
- Qualidade de embeddings: a avaliação do juiz penalizará a recuperação fraca por meio dos portões de fidelidade e alucinação; o modelo de embeddings primário deveria ser um recente e forte, com um fallback offline disponível no extra de dev/eval quando nenhuma chave de API for configurada
- Licença: cada componente com licença permissiva; os embeddings gerados para a KB são distribuídos dentro da imagem sem custo por consulta em tempo de execução quando o fallback offline de dev/eval é usado
Opções Consideradas
Seção intitulada “Opções Consideradas”- Chroma embarcado (SQLite + HNSW) + Voyage AI
voyage-3.5como embeddings primários,sentence-transformers BAAI/bge-small-en-v1.5como fallback offline de dev/eval no extraembeddings-local(escolhida) - Nível gratuito do Qdrant Cloud + Voyage AI
voyage-3.5: serviço gerenciado, nível gratuito generoso, mas dependência externa - FAISS como armazenamento embarcado: alto desempenho, mas a história de metadados é mais fina que a do Chroma
- Postgres + pgvector: coalocado com o saver Postgres do LangGraph, mas adiciona superfície operacional para uma KB de 50 cartões
- OpenAI
text-embedding-3-largecomo modelo de embeddings
Resultado da Decisão
Seção intitulada “Resultado da Decisão”Opção escolhida: Chroma embarcado como o armazenamento vetorial primário, com
Voyage AI voyage-3.5 como o modelo de embeddings primário e
sentence-transformers BAAI/bge-small-en-v1.5 como o fallback offline
de dev/eval no extra embeddings-local. O nível gratuito do Qdrant Cloud está documentado como o caminho
alternativo gerenciado; é a resposta certa para qualquer leitor cujo
caso de uso tenha mais de ~50K chunks ou precise de um dashboard hospedado.
A Voyage AI dá 200 milhões de tokens gratuitos na família voyage-3.5 para
novos usuários, o que excede em muito o que a KB precisa (o
corpus inteiro de 50 cartões é embarcado em menos de um milhão de tokens). O
fallback de sentence-transformers vem no extra opcional embeddings-local
usado por dev, eval e checkouts locais sem chave, de modo que essas execuções
rodam com zero chaves de API externas se o usuário preferir; a imagem de
produção instala embeddings-cloud em vez disso e descarta o torch, então
quando nenhuma chave de API da Voyage está definida ela degrada para
recuperação léxica BM25 em vez do modelo local.
A escolha mantém a demo ao vivo a custo zero, dá uma alternativa limpa de banco de dados gerenciado para os leitores que a queiram, e usa dois caminhos de embeddings que ambos pontuam bem nos benchmarks de recuperação.
Confirmação
Seção intitulada “Confirmação”- A demo roda o Chroma embarcado dentro da imagem Docker; nenhum serviço externo é necessário para subi-la
- A fábrica de embedder seleciona a Voyage AI se uma chave de API da Voyage estiver definida, e recorre ao modelo local de sentence-transformers caso contrário; um teste unitário exercita ambos os ramos
- O ingest da KB faz upsert de uma linha de Chroma por sub-chunk e imprime a contagem de linhas inseridas; o embedder fica fixado por config, para que o harness de avaliação rode contra a superfície de recuperação esperada
Consequências
Seção intitulada “Consequências”Positivas
Seção intitulada “Positivas”- A demo roda offline: nenhum serviço externo é necessário, o que mantém rápido e determinístico o caminho de despertar a frio
- O harness de avaliação vê uma superfície de recuperação determinística (Chroma
- embeddings fixados + o embedder fixado por config), exatamente o que o avaliador de fundamentação precisa
- A Voyage AI
voyage-3.5é um modelo de embeddings recente e forte (anunciado em 2025-05-20); o nível de 200M tokens gratuitos cobre a KB muitas vezes - O fallback offline remove a leitura de “precisa de uma chave de API” para qualquer leitor que queira clonar e executar
- O Qdrant Cloud como caminho alternativo documentado permite que o projeto sinalize consciência de banco de dados vetorial gerenciado sem herdar o risco de suspensão do nível gratuito
Negativas
Seção intitulada “Negativas”- O modelo
sentence-transformersde fallback aumenta o tamanho do extraembeddings-localusado por dev e eval, não o da imagem de produção, que instalaembeddings-cloude descarta o torch; esse extra compra embeddings offline sem ida e volta para essas execuções - O Chroma embarcado escala mal além de centenas de milhares de chunks; irrelevante para uma KB de 50 cartões mas vale a pena sinalizar
- Dois caminhos de embeddings significam duas assinaturas de recuperação; o embedder fixado por config torna a diferença auditável, mas os resultados de avaliação devem ser comparados dentro de um caminho de embeddings, não entre eles
Neutras
Seção intitulada “Neutras”- O projeto ganha as dependências
chromadbevoyageai - A imagem de produção não carrega os pesos de
sentence-transformers; eles são distribuídos apenas no extraembeddings-localpara dev e eval, intencional e documentado - Uma migração futura para o Qdrant Cloud é uma troca em nível de Protocol, não uma reescrita: a abstração de armazenamento cobre ambos os backends
Prós e Contras das Opções
Seção intitulada “Prós e Contras das Opções”Chroma embarcado + Voyage AI primário + bge-small-en-v1.5 fallback
Seção intitulada “Chroma embarcado + Voyage AI primário + bge-small-en-v1.5 fallback”- Boa, porque o caminho padrão roda com zero serviços externos
- Boa, porque o nível gratuito de 200M tokens da Voyage AI cobre a KB
- Boa, porque o fallback offline remove a leitura de “precisa-de-uma-chave”
- Boa, porque o harness de avaliação vê uma superfície de recuperação determinística
- Ruim, porque o modelo de fallback aumenta o extra
embeddings-localde dev e eval, embora a imagem de produção o descarte - Ruim, porque o Chroma embarcado não escala para centenas de milhares de chunks
Nível gratuito do Qdrant Cloud + Voyage AI
Seção intitulada “Nível gratuito do Qdrant Cloud + Voyage AI”- Boa, porque o dashboard gerenciado e o nível gratuito (1 GB, sem cartão) são generosos
- Ruim, porque a demo dependeria de um serviço externo e da política de contas do Qdrant; cada leitor teria que se cadastrar
- Mantida como alternativa documentada
FAISS embarcado
Seção intitulada “FAISS embarcado”- Boa, porque o FAISS é rápido e testado em batalha
- Ruim, porque a ergonomia de metadados + filtragem é mais fraca que a do Chroma
Postgres + pgvector
Seção intitulada “Postgres + pgvector”- Boa, porque o Postgres já é usado para o saver de estado da conversa
- Ruim, porque coalocar o estado da conversa e o armazenamento vetorial complica a operação para uma KB de 50 cartões, e distribuir Postgres para a recuperação contradiz a postura embarcada por padrão
OpenAI text-embedding-3-large
Seção intitulada “OpenAI text-embedding-3-large”- Boa, porque é um modelo de embeddings forte e bem conhecido
- Ruim, porque forçaria a demo a exigir uma chave de OpenAI só para a recuperação, e de todo modo não há um fallback offline limpo com qualidade comparável fora dos sentence-transformers
Mais Informações
Seção intitulada “Mais Informações”- Documentação do Chroma: https://docs.trychroma.com/
- Nível gratuito do Qdrant Cloud: https://qdrant.tech/documentation/cloud/
- Anúncio da Voyage AI
voyage-3.5(2025-05-20): https://blog.voyageai.com/2025/05/20/voyage-3-5/ - Preços e nível de tokens gratuitos da Voyage AI: https://docs.voyageai.com/docs/pricing
- Model card do
BAAI/bge-small-en-v1.5: https://huggingface.co/BAAI/bge-small-en-v1.5 - DailyMed (FDA SPL): https://dailymed.nlm.nih.gov/dailymed/
- MedlinePlus: https://medlineplus.gov/
- Lista de Medicamentos Essenciais da WHO: https://www.who.int/groups/expert-committee-on-selection-and-use-of-essential-medicines/essential-medicines-lists
- MADR 4.0.0: https://adr.github.io/madr/
Embedder e recuperação assimétrica tal como construídos
Seção intitulada “Embedder e recuperação assimétrica tal como construídos”Embedder primário: Voyage voyage-3.5, com um fallback local offline no extra
embeddings-local. A fábrica de embedder é orientada por chave: resolve a
Voyage voyage-3.5 quando uma chave de API da Voyage está configurada, como na
implantação ao vivo. O fallback local BAAI/bge-small-en-v1.5 vem no extra
opcional embeddings-local usado por dev, eval e checkouts locais sem chave; a
imagem de produção instala embeddings-cloud em vez de embeddings-local e
descarta o torch, de modo que não carrega o modelo local. Sem uma chave da Voyage, a imagem de
produção degrada para recuperação léxica BM25 em vez do embedder local. O
fallback é um modelo leve, de aproximadamente 130 MB, compatível com CPU em uma
instância pequena, de modo que uma execução de dev ou eval também roda a $0 sem
chaves externas mantendo uma forte qualidade de recuperação.
A recuperação é assimétrica e consciente de instruções. A família BGE
v1.5 é ajustada por instruções e assimétrica. O código distribuído honra isso:
uma consulta é embarcada com o prefixo documentado de instrução de
recuperação do BGE (Represent this sentence for searching relevant passages: );
uma passagem é embarcada sem prefixo; cada vetor é normalizado por L2 para que
a busca por produto interno do Chroma se comporte como similaridade de cosseno. Um
modelo simétrico de propósito geral (por exemplo all-MiniLM-L6-v2) não recebe
prefixo de instrução. Usada sem o tratamento assimétrico, a qualidade de
recuperação do BGE se degrada; a camada de recuperação é construída para
aplicá-lo.
O limiar de similaridade de recuperação é distribuído desativado. Existe um ajuste de similaridade mínima de recuperação mas vem desativado por padrão. No corpus de KB de domínio único um limiar não consegue separar uma pergunta clínica fora do corpus por pouco de uma genuinamente dentro do corpus sem recusar falsamente esta última. O agente recusa diante de uma recuperação de zero acertos; uma pergunta fora do corpus por pouco é respondida contra o cartão mais próximo. O limiar é deixado no lugar, desativado, para que um corpus mais amplo e tematicamente mais diverso possa habilitá-lo mais tarde. Ver a model card para a limitação completa.
Parte do portfólio de Waldemar Szemat · szemat.pro
GitHub · LinkedIn