RAG multi-tenant: metadados, filtros e permissões

Você está construindo um produto SaaS com RAG, ou um sistema interno com múltiplos departamentos, e a pergunta inevitável aparece: como garantir que o Cliente A nunca veja dados do Cliente B? Um filtro mal implementado e o LLM responde uma pergunta de RH com dados do jurídico. Este artigo mostra as 3 estratégias de isolamento, seus trade-offs e como implementar RBAC de verdade.

O problema real: vazamento entre tenants

Em um RAG simples (single-tenant), você indexa todos os documentos e busca em tudo. Em multi-tenant, a busca vetorial — por natureza — retorna os chunks mais similares do corpus inteiro. Sem isolamento adequado, uma pergunta do Cliente A pode trazer contexto do Cliente B para o LLM, que vai usá-lo sem hesitar.

O problema é traiçoeiro porque não gera erro — gera resposta errada com aparência de correta. O usuário do Cliente A recebe uma resposta fundamentada em políticas comerciais do Cliente B sem saber. Em contextos B2B com dados sensíveis (precificação, contratos, estratégia), isso é uma falha grave de segurança e um risco de compliance com a LGPD.

Veja o que é RAG e como funciona o retrieval para entender por que o problema acontece na camada de busca, não na geração.

Arquitetura geral de um RAG multi-tenant

Antes das estratégias específicas, o fluxo geral de um RAG multi-tenant seguro é:

┌─────────────────────────────────────────────────────────┐
│                    CLIENTE / USUÁRIO                     │
│  (autenticado com JWT contendo tenant_id + roles)        │
└──────────────────────────┬──────────────────────────────┘
                           │ query
                           ▼
┌─────────────────────────────────────────────────────────┐
│                    API / BACKEND                         │
│  1. Valida JWT e extrai tenant_id                        │
│  2. Constrói filtro de isolamento                        │
│  3. Executa busca com filtro injetado                    │
│  4. Valida que chunks retornados pertencem ao tenant     │
│  5. Monta prompt com contexto filtrado                   │
│  6. Chama LLM                                            │
└──────────────────────────┬──────────────────────────────┘
                           │ busca filtrada
                           ▼
┌─────────────────────────────────────────────────────────┐
│                  VECTOR STORE                            │
│  ┌───────────────┐  ┌───────────────┐  ┌─────────────┐ │
│  │  tenant_A     │  │  tenant_B     │  │  tenant_C   │ │
│  │  [chunks...]  │  │  [chunks...]  │  │  [chunks..] │ │
│  └───────────────┘  └───────────────┘  └─────────────┘ │
└─────────────────────────────────────────────────────────┘
                           │ ingestão
                           ▲
┌─────────────────────────────────────────────────────────┐
│               PIPELINE DE INGESTÃO                       │
│  Documento → Chunking → Embedding → Metadados           │
│  (tenant_id, departamento, nível_acesso, data, tipo)    │
└─────────────────────────────────────────────────────────┘

O ponto crítico: o filtro de isolamento é construído no backend com base no token de autenticação, não no frontend. O usuário nunca pode informar seu próprio tenant_id.

As 3 estratégias de isolamento: comparação

Estratégias de isolamento em RAG multi-tenant: prós, contras e indicação
Estratégia Como funciona Prós Contras Quando usar
Namespace por tenant Cada tenant recebe um namespace lógico dentro da mesma collection. A busca é restrita ao namespace via parâmetro. Simples de implementar; único vector store; baixo custo operacional Isolamento lógico, não físico — um bug no código pode vazar. Nem todos os vector stores suportam namespaces. Aplicações internas com nível moderado de sensibilidade; provas de conceito
Collection separada por tenant Cada tenant tem sua própria collection/index no vector store. Impossível cruzar dados por design. Isolamento forte; nenhum bug de filtro pode vazar dados; backup e exclusão por tenant simples Custo de recursos escala com número de tenants; overhead operacional alto para 100+ tenants SaaS B2B com dados críticos; conformidade regulatória rigorosa; até ~50 tenants
Filtro de metadados (campo tenant_id) Todos os chunks numa collection única, com campo tenant_id. Todo query inclui filtro obrigatório where tenant_id == X. Escala para milhares de tenants; custo fixo de infraestrutura; simples de adicionar novos tenants Depende de código correto; requer validação dupla no backend; menos auditável sem logs Plataformas com muitos tenants pequenos; quando custo de infra é restrição

Implementando filtro RBAC por metadado

A estratégia de filtro de metadados é a mais comum em produção por escalar bem. O segredo está em tornar o filtro impossível de ser bypassado pelo usuário.

Estrutura de metadados recomendada

Na ingestão, cada chunk deve receber metadados suficientes para filtrar com granularidade:

# Metadados que acompanham cada chunk no vector store
metadados = {
    "tenant_id": "empresa-abc-001",          # obrigatório, imutável
    "departamento": "juridico",               # opcional, para RBAC interno
    "nivel_acesso": "confidencial",           # público, interno, confidencial, restrito
    "tipo_documento": "contrato",             # para filtros por tipo
    "data_vigencia": "2026-12-31",           # para filtros temporais
    "documento_id": "contrato-2024-0891",    # rastreabilidade
    "pagina": 3,                              # localização no original
    "anonimizado": False                      # controle LGPD
}

Injeção de filtro obrigatório no backend

def buscar_com_isolamento(query: str, token_jwt: str) -> list:
    # Extração de claims do token — nunca confiar em parâmetro do cliente
    claims = jwt.decode(token_jwt, chave_publica, algorithms=["RS256"])
    tenant_id = claims["tenant_id"]
    roles = claims["roles"]
    nivel_acesso = mapear_nivel_por_role(roles)

    # Filtro construído no backend, nunca enviado pelo cliente
    filtro = {
        "must": [
            {"key": "tenant_id", "match": {"value": tenant_id}},
            {"key": "nivel_acesso", "match": {"any": nivel_acesso}}
        ]
    }

    # Busca vetorial com filtro injetado
    resultados = vector_store.buscar(
        vetor=embedder.encode(query),
        filtro=filtro,
        top_k=20
    )

    # Validação defensiva: rejeitar qualquer chunk fora do tenant
    resultados_validados = [
        r for r in resultados
        if r.metadados["tenant_id"] == tenant_id
    ]

    return resultados_validados

A "validação defensiva" no final pode parecer redundante, mas funciona como segurança em profundidade (defense in depth): se um bug no filtro do vector store passar um chunk errado, ele é eliminado antes de chegar ao LLM.

RBAC interno: permissões por departamento dentro do mesmo tenant

Além do isolamento entre tenants, muitos cenários exigem isolamento interno: um funcionário de vendas não deve acessar documentos do RH, mesmo que ambos sejam do mesmo tenant.

A abordagem é adicionar camadas ao filtro de metadados:

Para casos de LGPD — dados pessoais de funcionários, informações de saúde — veja como tratar dados pessoais em aplicações LLM com LGPD.

Custo operacional: números reais

Antes de escolher a estratégia, estime o custo de cada uma para o seu cenário:

Para um SaaS com 200+ tenants pequenos, filtro de metadados é quase sempre a escolha certa. Para 10 tenants com dados altamente sensíveis, collection separada paga o custo extra em segurança e auditabilidade.

Auditoria: quem perguntou o quê, e o que o sistema entregou

Multi-tenant sem auditoria é um risco de compliance. Para cada query, registre:

Esses logs são essenciais para responder a uma auditoria de LGPD ("quais dados de fulano foram acessados por quem?") e para detectar tentativas de prompt injection que buscam vazar dados de outros tenants. Veja mais em segurança em aplicações LLM com allowlist e sandbox.

Erros comuns que vi em implementações reais