Estudo de caso

CiteRAG

Um sistema de RAG fail-closed para documentação técnica local

O problema

CiteRAG é um sistema de perguntas e respostas self-hosted para documentação técnica local. Ele indexa arquivos Markdown e texto, recupera os trechos relevantes para uma pergunta, e responde com citações anexadas. A maioria dos demos de RAG retorna uma resposta mesmo quando o contexto recuperado não a sustenta de fato. O CiteRAG verifica cada resposta em busca de citações antes de retorná-la. Quando o modelo não consegue embasar uma afirmação com uma fonte, a resposta é substituída por uma recusa em vez de ser mostrada como fato.

Arquitetura

Ingestão

Docs
.md / .txt
Limpa + divide
Embedding
denso + esparso
Qdrant
indexado

Busca e resposta

Pergunta
Busca híbrida
Qdrant
Reordena
cross-encoder
LLM + citações
Ollama
Resposta
ou recusa

Como funciona

Ingestão

Os documentos são carregados, limpos e divididos em chunks. Cada chunk recebe um embedding denso (BAAI/bge-m3) e um embedding esparso, e ambos são indexados no Qdrant. A ingestão é incremental. Os arquivos são hasheados, então uma nova execução pula o que não mudou, e chunks de arquivos editados ou removidos são limpos automaticamente em vez de se acumularem como dados obsoletos.

Busca e resposta

Uma pergunta é transformada em embedding e buscada no Qdrant com recuperação híbrida densa e esparsa, combinadas com Reciprocal Rank Fusion, para que correspondências semânticas e por palavra-chave apareçam. O conjunto de candidatos é reordenado com um cross-encoder (BAAI/bge-reranker-v2-m3) antes de chegar ao modelo de linguagem. O context builder anexa marcações de citação aos trechos recuperados, o modelo gera uma resposta baseada nesse contexto, e a resposta é verificada quanto a citações antes de ser retornada. Uma resposta sem citação vira uma recusa.

Recuperação HyDE opcional

Um modo HyDE opcional recupera usando uma resposta hipotética gerada pelo modelo em vez da pergunta original. Isso ajuda em perguntas curtas ou pobres em palavras-chave. Ele só muda a recuperação. A resposta final continua baseada na pergunta real que foi feita.

Interfaces e infraestrutura

Uma CLI, um backend FastAPI e uma interface Streamlit chamam o mesmo RAGService, então a lógica de ingestão e busca não é duplicada por interface. O Qdrant roda em Docker para armazenamento vetorial e o Ollama roda o modelo de linguagem localmente. Toda a inferência (embedding, reranking e geração) acontece na máquina local. Nenhum conteúdo de documento ou pergunta sai dela.

Testes e avaliação

137 testes unitários cobrem o código, com as chamadas de modelo e embedding mockadas. Eles confirmam que o código roda corretamente, não que as respostas são boas. O CI roda esses testes, além do ruff para formatação e lint e do mypy para checagem de tipos, a cada push.

Um harness de avaliação separado verifica a qualidade das respostas contra o pipeline real: 10 perguntas fixas (6 com palavras-chave esperadas e um número mínimo de citações, 4 que deveriam ser recusadas por serem fora do escopo) rodam contra um stack real de Qdrant, Ollama e FastAPI e são avaliadas como passou ou falhou. Ele precisa desse stack ativo, então não roda no CI. Também permite comparar duas configurações de recuperação entre si, como o baseline contra o HyDE, escrevendo cada execução em seu próprio relatório.

Limitações

  • O harness de avaliação não faz parte do CI. Ele precisa ser rodado manualmente contra um stack ativo, então regressões na qualidade das respostas entre commits não são detectadas automaticamente.
  • O golden set tem 10 perguntas. Isso é suficiente para pegar uma regressão ou confirmar uma mudança intencional, não para um benchmark estatisticamente rigoroso da qualidade das respostas.
  • Tudo roda em uma única máquina com GPU para embedding, reranking e geração. Não há uma proposta de deployment distribuído ou multi-tenant aqui. Foi construído para a documentação local de uma pessoa.

Stack

PythonFastAPIStreamlitQdrantOllamaLangChainBAAI/bge-m3BAAI/bge-reranker-v2-m3uvruffmypypytest
Ver código-fonte no GitHub