04-06-03 — Desafio: arquitetura RAG para 10k PDFs jurídicos

⏱ 10 minFontes validadas em: 2026-04-29

TL;DR

Cenário real: escritório de advocacia com 10.000 contratos em PDF precisa buscar cláusulas específicas, identificar riscos e citar exatamente onde está a informação (página e parágrafo). Não é um RAG genérico — exige OCR de qualidade, chunking que respeite estrutura jurídica, metadata rico (partes, data, tipo de contrato) e compliance LGPD para dados de clientes. Este tópico apresenta o problema, as decisões de arquitetura e um template para você preencher com sua solução.

O cenário

Contexto do desafio: Um escritório de advocacia corporativo tem 10.000 contratos em PDF (contratos de prestação de serviços, NDAs, contratos de compra e venda, acordos societários). Os advogados precisam: (1) buscar cláusulas específicas por tema, (2) comparar como a empresa trata determinado assunto em contratos diferentes, (3) identificar contratos que não têm cláusula de LGPD, (4) citar exatamente a página e parágrafo da fonte. Tempo de resposta esperado: < 10 segundos.

Requisitos não-triviais

  • OCR: 40% dos PDFs são escaneados (imagens), não texto searchable. Precisam de OCR antes de qualquer processamento.
  • Tabelas: Contratos têm tabelas (cronogramas, valores, partes). Extração de tabelas precisa preservar estrutura.
  • Citação com localização: Advogado precisa saber "página 7, cláusula 3.2" — não apenas o texto recuperado.
  • Compliance LGPD: Contratos contêm dados pessoais (CPF, endereço das partes). O sistema não pode vazar esses dados para usuários sem autorização.
  • Multi-tenant: Advogados diferentes só podem ver contratos dos seus clientes.

Decisões de arquitetura

1. Extração de texto — Azure Document Intelligence

Use o modelo prebuilt-layout do Azure Document Intelligence (ex-Form Recognizer). Ele extrai texto com coordenadas de página, identifica tabelas como objetos estruturados e funciona em PDFs escaneados (OCR nativo).

from azure.ai.formrecognizer import DocumentAnalysisClient
from azure.core.credentials import AzureKeyCredential

client = DocumentAnalysisClient(
    endpoint="https://meu-doc-intelligence.cognitiveservices.azure.com",
    credential=AzureKeyCredential("SUA_CHAVE"),
)

with open("contrato.pdf", "rb") as f:
    poller = client.begin_analyze_document("prebuilt-layout", f)
    result = poller.result()

# Extrair texto por parágrafo com número de página
paragrafos = []
for para in result.paragraphs:
    paragrafos.append({
        "texto": para.content,
        "pagina": para.bounding_regions[0].page_number if para.bounding_regions else None,
        "role": para.role,  # "title", "sectionHeading", "footnote", etc.
    })

# Extrair tabelas estruturadas
for table in result.tables:
    print(f"Tabela na página {table.bounding_regions[0].page_number}: {table.row_count}x{table.column_count}")
    for cell in table.cells:
        print(f"  [{cell.row_index},{cell.column_index}]: {cell.content}")

2. Chunking para documentos jurídicos

O chunking padrão (tamanho fixo de tokens) quebra cláusulas ao meio — inaceitável. Use chunking semântico baseado na estrutura do documento:

def chunk_contrato_juridico(paragrafos, max_tokens=400):
    """
    Chunking que respeita a estrutura de contratos:
    - Mantém cláusulas inteiras (identifica por role=sectionHeading)
    - Preserva metadata de localização (página, número da cláusula)
    - Não quebra cláusulas no meio
    """
    chunks = []
    chunk_atual = []
    tokens_atuais = 0
    clausula_atual = "Preâmbulo"
    pagina_inicio = 1

    for para in paragrafos:
        # Nova cláusula detectada
        if para["role"] == "sectionHeading":
            # Salvar chunk anterior se não vazio
            if chunk_atual:
                chunks.append({
                    "texto": " ".join([p["texto"] for p in chunk_atual]),
                    "clausula": clausula_atual,
                    "pagina_inicio": pagina_inicio,
                    "pagina_fim": chunk_atual[-1]["pagina"],
                })
                chunk_atual = []
                tokens_atuais = 0
            clausula_atual = para["texto"]
            pagina_inicio = para["pagina"]

        tokens_para = len(para["texto"].split()) * 1.3  # Estimativa tokens
        
        # Se chunk ficaria grande demais, fechar e começar novo
        if tokens_atuais + tokens_para > max_tokens and chunk_atual:
            chunks.append({
                "texto": " ".join([p["texto"] for p in chunk_atual]),
                "clausula": clausula_atual,
                "pagina_inicio": pagina_inicio,
                "pagina_fim": chunk_atual[-1]["pagina"],
            })
            chunk_atual = []
            tokens_atuais = 0
            pagina_inicio = para["pagina"]

        chunk_atual.append(para)
        tokens_atuais += tokens_para

    # Último chunk
    if chunk_atual:
        chunks.append({
            "texto": " ".join([p["texto"] for p in chunk_atual]),
            "clausula": clausula_atual,
            "pagina_inicio": pagina_inicio,
            "pagina_fim": chunk_atual[-1]["pagina"],
        })

    return chunks

3. Schema do índice AI Search com metadata jurídico

fields = [
    SimpleField("id", SearchFieldDataType.String, key=True),
    SearchableField("texto", SearchFieldDataType.String, analyzer_name="pt.lucene"),
    SimpleField("contrato_id", SearchFieldDataType.String, filterable=True, facetable=True),
    SimpleField("cliente_id", SearchFieldDataType.String, filterable=True),  # Multi-tenant
    SimpleField("tipo_contrato", SearchFieldDataType.String, filterable=True, facetable=True),
    # "NDA", "Prestacao_Servicos", "Compra_Venda", "Societario"
    SimpleField("clausula", SearchFieldDataType.String, filterable=True),
    SimpleField("pagina_inicio", SearchFieldDataType.Int32, filterable=True, sortable=True),
    SimpleField("pagina_fim", SearchFieldDataType.Int32, filterable=True),
    SimpleField("data_assinatura", SearchFieldDataType.DateTimeOffset, filterable=True, sortable=True),
    SimpleField("partes", SearchFieldDataType.Collection(SearchFieldDataType.String), filterable=True),
    SimpleField("tem_clausula_lgpd", SearchFieldDataType.Boolean, filterable=True, facetable=True),
    SearchField(
        "texto_vector",
        SearchFieldDataType.Collection(SearchFieldDataType.Single),
        searchable=True,
        vector_search_dimensions=1536,
        vector_search_profile_name="hnswProfile",
    ),
]
Multi-tenant com security filters: Para garantir que advogado A não veja contratos do cliente B, use security filters do Azure AI Search: passe filter=f"cliente_id eq '{cliente_id_do_usuario}'" em toda query. Nunca confie apenas no controle de acesso da interface — filtre no índice.

Template de arquitetura para preencher

Use este template para documentar sua solução antes de implementar:

ComponenteServiço escolhidoJustificativaCusto estimado/mês
Armazenamento dos PDFsAzure Blob Storage / OneLake
Extração de texto / OCRAzure Document Intelligence
Geração de embeddingstext-embedding-3-small / large
Vector store / índiceAzure AI Search (SKU?)
LLM para geraçãoGPT-4o / GPT-4o-mini
OrquestraçãoSemantic Kernel / LangChain / custom
Autenticação / multi-tenantAzure AD + security filters
Avaliação de qualidadeRAGAS / DeepEval / Azure Eval

Estimativa de custo de referência (10k PDFs)

ItemVolumeCusto estimado
Document Intelligence (prebuilt-layout)10k docs × ~20 páginas = 200k páginas~US$ 300 (one-time)
Embeddings (text-embedding-3-small)~50M tokens (10k docs × 5k tokens)~US$ 10 (one-time)
Azure AI Search (Standard S1)1 réplica, 1 partição, ~500k docs~US$ 250/mês
Azure OpenAI GPT-4o~10k queries/mês × 2k tokens avg~US$ 100-300/mês
Total estimado~US$ 350-600/mês + setup ~US$ 310
LGPD na prática: Contratos têm CPF e endereço das partes. Implemente: (1) classificação automática de PII no momento da indexação com Azure AI Content Safety ou Presidio, (2) campo tem_pii no índice, (3) mascaramento de PII na resposta final para usuários sem perfil de compliance. Documente o fluxo de dados para o DPO.

Fluxo de consulta com citação precisa

def buscar_clausula(query: str, cliente_id: str, top_k: int = 5):
    """
    Busca cláusulas com citação precisa (contrato, cláusula, página)
    """
    # 1. Gerar embedding da query
    embedding = openai_client.embeddings.create(
        input=query, model="text-embedding-3-small"
    ).data[0].embedding

    # 2. Hybrid search com security filter (multi-tenant)
    results = search_client.search(
        search_text=query,
        vector_queries=[VectorizedQuery(
            vector=embedding,
            k_nearest_neighbors=top_k,
            fields="texto_vector",
        )],
        filter=f"cliente_id eq '{cliente_id}'",  # CRÍTICO: isolamento por cliente
        query_type=QueryType.SEMANTIC,
        semantic_configuration_name="default",
        top=top_k,
        select=["texto", "contrato_id", "clausula", "pagina_inicio", "tipo_contrato"],
    )

    # 3. Montar contexto com localização
    contexto_com_citacoes = []
    for r in results:
        contexto_com_citacoes.append(
            f"[Contrato: {r['contrato_id']} | Tipo: {r['tipo_contrato']} | "
            f"Cláusula: {r['clausula']} | Página: {r['pagina_inicio']}]\n"
            f"{r['texto']}"
        )

    # 4. Gerar resposta com instrução de citar fonte
    prompt = f"""Responda à pergunta abaixo com base APENAS nos trechos de contratos fornecidos.
Para cada informação, cite o contrato e a página exata no formato [Contrato X, p.Y, Cláusula Z].
Não invente informações que não estejam nos trechos.

PERGUNTA: {query}

TRECHOS:
{chr(10).join(contexto_com_citacoes)}"""

    response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,  # Determinístico para contexto jurídico
    )

    return response.choices[0].message.content

Como isso se conecta

  • 04-03-01 Chunking: o chunking por cláusula jurídica é uma especialização do parent-document retriever
  • 04-03-03 Hybrid search: essencial para contratos — termos jurídicos específicos (BM25) + semântica (vetorial)
  • 04-04-01 Re-ranking: o semantic ranker do AI Search é crítico para precisão em queries jurídicas ambíguas
  • 04-05-02 RAGAS, DeepEval: use faithfulness e context_precision para validar que o sistema cita corretamente
  • 05-01 Agentes: a próxima evolução é um agente jurídico que compara cláusulas entre múltiplos contratos automaticamente

Fontes

  1. Microsoft Learn — Document Intelligence prebuilt-layout model — microsoft.com
  2. Microsoft Learn — Security trimming in Azure AI Search (multi-tenant) — microsoft.com
  3. Microsoft Presidio — PII Detection and Anonymization — microsoft.github.io
  4. Azure AI Search — Pricing Model — microsoft.com