Pular para o conteúdo

ADR-0004: Stack de RAG

  • Status: Accepted
  • Data: 2026-03-18
  • Responsáveis pela decisão: Waldemar Szemat

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?

  • 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
  • Chroma embarcado (SQLite + HNSW) + Voyage AI voyage-3.5 como embeddings primários, sentence-transformers BAAI/bge-small-en-v1.5 como fallback offline de dev/eval no extra embeddings-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-large como modelo de embeddings

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.

  • 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
  • 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
  • O modelo sentence-transformers de fallback aumenta o tamanho do extra embeddings-local usado por dev e eval, não o da imagem de produção, que instala embeddings-cloud e 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
  • O projeto ganha as dependências chromadb e voyageai
  • A imagem de produção não carrega os pesos de sentence-transformers; eles são distribuídos apenas no extra embeddings-local para 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

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-local de 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
  • 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
  • Boa, porque o FAISS é rápido e testado em batalha
  • Ruim, porque a ergonomia de metadados + filtragem é mais fraca que a do Chroma
  • 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
  • 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

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