Pular para o conteúdo
ZYNQORALABS
AI · Demonstração interna

LedgerlineInteligência contratual com recuperação avaliada

Arquitetura de referência para responder perguntas sobre milhares de contratos com citação obrigatória da cláusula de origem e recusa explícita quando a evidência é fraca.

Tecnologias
Python · PostgreSQL · pgvector · LLM API · TypeScript
Demonstra
  • Recuperação híbrida
  • Respostas com citação verificada
  • Avaliação contínua de RAG
  • Privacidade antes do modelo

O desafio

  1. 01

    Contratos têm numeração de cláusulas, nomes de partes e valores exatos. Busca puramente semântica erra justamente nesses termos.

  2. 02

    Uma resposta sem fonte é inútil para um jurídico: cada afirmação precisa apontar para o trecho exato do documento.

  3. 03

    Trocar de modelo ou de estratégia de chunking não pode ser decisão no escuro. A qualidade da recuperação precisa ser medida antes da geração.

Restrições

  • Documentos com dados pessoais
  • Respostas auditáveis
  • Custo por consulta previsível

Arquitetura

Fluxo de ingestão (acima) e de consulta (abaixo), com o conjunto de avaliação como portão de qualidade.Fig. 01 · Ledgerline

Componentes

Contracts
PDFs e DOCX, incluindo digitalizados.
Parse + OCR
Extrai texto preservando a hierarquia de cláusulas.
PII redaction
Mascara dados pessoais antes de qualquer chamada externa.
Clause chunker
Um trecho por cláusula, com o caminho hierárquico como metadado.
Hybrid index
PostgreSQL com pgvector para embeddings e busca textual para termos exatos.
Question
Pergunta em linguagem natural do usuário.
Retrieve + RRF
Busca vetorial e textual em paralelo, combinadas por Reciprocal Rank Fusion.
Rerank
Reordena os candidatos e aplica um limiar mínimo de relevância.
Cited answer
O modelo só pode afirmar o que está nos trechos citados. Sem evidência, recusa.
Eval set
Perguntas de referência com as cláusulas corretas: mede recall@k e fidelidade.

Decisões de engenharia

D1Busca híbrida em vez de apenas vetorial

Escolha
Vetorial + full-text, combinadas por Reciprocal Rank Fusion.
Alternativa descartada
Apenas embeddings.
Por quê
“Cláusula 14.2” e “multa de 2%” são termos exatos. Embeddings aproximam significado, mas confundem números e identificadores.

D2Chunking pela estrutura do documento

Escolha
Um trecho por cláusula, com o caminho (Seção 4 › 4.3) como metadado.
Alternativa descartada
Janelas fixas de N tokens com sobreposição.
Por quê
Janelas fixas cortam cláusulas ao meio e misturam obrigações diferentes no mesmo trecho, o que gera citações enganosas.

D3Recusar é um resultado válido

Escolha
Abaixo do limiar de relevância, o sistema responde que não encontrou base no documento.
Alternativa descartada
Sempre gerar uma resposta.
Por quê
Para um time jurídico, uma resposta confiante e errada custa mais do que nenhuma resposta.

D4Avaliação antes de trocar qualquer peça

Escolha
Conjunto de perguntas de referência rodando no CI a cada mudança de modelo, prompt ou chunking.
Alternativa descartada
Avaliação manual pontual.
Por quê
Sem medida, melhorias de prompt viram regressões silenciosas na recuperação.

Detalhes de implementação

A ingestão roda como job assíncrono: cada documento passa por extração, mascaramento de dados pessoais e segmentação por cláusula antes de ser indexado. Reprocessar um documento é idempotente, porque o identificador do trecho deriva do documento e do caminho da cláusula.

Na consulta, as buscas vetorial e textual rodam em paralelo e são combinadas por posição (RRF), o que dispensa calibrar escalas de score diferentes. O reranker aplica o limiar que decide entre responder e recusar.

O modelo recebe apenas os trechos aprovados, cada um com um identificador. A resposta é validada: toda citação precisa existir entre os trechos enviados, ou a resposta é descartada.

retrieval/fusion.pyTrecho representativo
def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """Merge ranked lists by position, so incompatible score scales never matter."""
    scores: dict[str, float] = {}
    for ranking in rankings:
        for position, chunk_id in enumerate(ranking, start=1):
            scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + position)
    return sorted(scores.items(), key=lambda item: item[1], reverse=True)


fused = reciprocal_rank_fusion([vector_hits, lexical_hits])
evidence = [c for c, _ in rerank(question, fused[:40]) if c.relevance >= MIN_RELEVANCE]
if not evidence:
    return Answer.refusal("No supporting clause found in the indexed contracts.")

Resultados

Não aplicável. Por ser uma demonstração interna, não há métricas de produção verificadas a relatar, e não publicamos números estimados como se fossem resultados.

Tem um problema parecido?

Conte o contexto. Discutimos a arquitetura antes de qualquer proposta comercial.

Próximo estudoQuorum IndexerIndexador EVM resistente a reorganizações
01Quem é você

Usamos apenas para responder a esta solicitação.

02O projeto

O problema, o que já existe hoje e o que seria sucesso. Não inclua senhas ou dados pessoais de terceiros.

03Escopo

Usamos estes dados apenas para avaliar e responder à sua solicitação. Não vendemos nem compartilhamos com terceiros para marketing. Detalhes na política de privacidade.