//Receita

Busca linha a linha dentro de um documento

Achar a cláusula certa num contrato é diferente de achar o contrato certo. Aqui o documento é um só e a resposta é uma linha.

Alguém do jurídico pergunta: “os termos dizem se posso ser suspenso sem aviso?”. O documento tem duzentas cláusulas. A resposta, se existir, é uma linha específica — e a pessoa precisa da linha, não de um resumo, porque vai citar.

Esse é um problema de busca dentro de um documento. Não há acervo para varrer: há um texto e uma pergunta em linguagem natural.

Por que a solução ingênua falha

A primeira tentativa costuma ser Ctrl+F. Falha porque a pergunta usa as palavras do usuário (“me expulsar da plataforma”) e o documento usa as dele (“suspender ou encerrar o acesso”).

A segunda tentativa é montar um índice vetorial por trecho. Funciona, mas é infraestrutura: fatiar, gerar embeddings, guardar, atualizar quando o documento muda. Para um contrato, é desproporcional.

A terceira é pedir ao LLM que devolva a cláusula. Aí aparece o problema que essa receita resolve de raiz: o modelo pode reescrever a cláusula em vez de citá-la, e uma citação levemente alterada num documento jurídico é pior que nenhuma. Além disso, quando a resposta não está no documento, o modelo tende a oferecer a linha mais parecida como se fosse resposta.

O desenho das perguntas

A receita tem três movimentos. Primeiro, marque cada linha com um id curto:

def id_da_linha(i: int) -> str:
    return f"L{i:03d}"

DOCUMENTO = "\n".join(f"{id_da_linha(i)}| {linha}" for i, linha in enumerate(LINHAS))

O documento passa a carregar seus próprios rótulos, assim:

L052| Você é dono do seu conteúdo. Se publicar conteúdo que não criou, é responsável por...
L053| Você concede a nós e a outros usuários as licenças das seções D.4 a D.8...
L054| 4. Concessão de licença

Segundo, use os ids como opções de um Choice. Escolher uma opção passa a significar apontar uma linha, e a distribuição de probabilidades vira a relevância de cada linha:

from typesafe_sdk import Choice, Noul, NoulCriteria, TypeSafeClient

def pergunta_onde(consulta: str) -> Choice:
    return Choice(
        instructions=f'Qual linha do documento contém a resposta para: "{consulta}"?',
        criteria={id_da_linha(i): None for i in range(len(LINHAS))},
    )

def pergunta_existe(consulta: str) -> Noul:
    return Noul(
        instructions=f'Alguma linha do documento trata ou responde: "{consulta}"?',
        criteria=NoulCriteria(
            true="Pelo menos uma linha afirma ou implica diretamente a resposta",
            false="Nenhuma linha do documento trata disso",
        ),
    )

with TypeSafeClient() as client:
    resposta = client.system_one(
        state=DOCUMENTO,
        questions={"onde": pergunta_onde(consulta), "existe": pergunta_existe(consulta)},
    )

relevancia = resposta.answers["onde"].probabilities
existe = resposta.answers["existe"].noul

Repare no None nas descrições das opções: o texto de cada linha já está no state, então descrever a opção seria repetição. E note que as duas perguntas vão na mesma requisição — o documento é enviado uma vez.

O terceiro movimento é o que separa esta receita de uma busca comum. As probabilidades de um Choice somam 1 por construção, então alguma linha sempre fica em primeiro lugar, inclusive quando o documento não responde nada. O Noul de existência resolve isso porque não depende das opções: ele pode ficar perto de zero enquanto a melhor linha tem relevância alta.

O que o experimento oficial mediu

O cookbook usa os termos de serviço do GitHub, em inglês: 218 linhas, 43.980 caracteres, tudo pontuado em uma requisição. Os limiares adotados foram 0,70 para “respondido” e 0,35 para “ausente”. Os resultados publicados pela TypeSafe:

ConsultaexistsMelhor linhaLeitura
Quem é dono do código que eu envio?0,98L052 com 0,95Respondido
O GitHub pode me suspender sem aviso?0,97L168 com 0,97Respondido
Sou obrigado a levar disputas à arbitragem?0,140,86Não está no documento
Menor de idade pode usar com autorização dos pais?0,46L029 com 0,90Parcialmente abordado

As duas últimas linhas são a razão de existir do Noul. Na pergunta sobre arbitragem, a melhor linha tem relevância 0,86 — alta o bastante para um sistema ingênuo entregar como resposta — enquanto o exists de 0,14 diz que a resposta não está ali. Na pergunta sobre menores, a regra de idade aparece com 0,90, mas ela não diz se autorização dos pais muda a regra: parcialmente abordado.

A lição do cookbook cabe em uma frase: a ordenação diz onde olhar, o exists diz se aquilo responde. O experimento declara rodar no jev-1.12, e o próprio texto pede para calibrar os limiares nos seus documentos antes de produção.

O que muda em português

O documento do experimento é em inglês. A receita não tem nenhuma dependência de idioma: ids são ids, e a consulta vai nas instructions em português se o seu documento é português.

Há um ponto prático que o Brasil sente mais: contrato e termo de uso em português costumam ter cláusulas longas e numeração própria (“Cláusula 7.2.1”). Vale usar a numeração do próprio documento como id em vez de inventar L000, porque assim a resposta já sai citável. E, como a TypeSafe declara que o inglês é a língua principal de treino, teste com as suas cláusulas antes de confiar no limiar.

Onde isso quebra

  • Acima de 255 linhas. É o teto de opções de um Choice. O cookbook sugere duas passagens: uma escolhe a janela, outra ordena dentro dela.
  • Documento grande. Mesmo abaixo de 255 linhas, um documento inflado com anexos irrelevantes cai no modo de falha de state grande descrito em limites do jev-1.13.
  • Linha errada por leitura literal. Se a consulta é ambígua, a resposta segue o que está escrito, não a intenção. Reescrever a consulta em termos do documento ajuda mais que insistir no mesmo texto.
  • Datas e prazos. “A cláusula de rescisão vale depois de 90 dias?” mistura achar a linha com fazer conta de data. Ache a linha com esta receita e faça a conta no código.

Para achar o documento certo dentro de um acervo antes de entrar aqui, veja reranking de busca. O índice das receitas fica em receitas.

Perguntas frequentes

Como o modelo aponta uma linha específica?

Você numera as linhas com um id curto no início de cada uma e usa esses ids como as opções de uma pergunta Choice. Escolher uma opção passa a ser apontar uma linha, e as probabilidades do Choice dão a relevância de cada linha.

Por que preciso de uma segunda pergunta?

Porque as probabilidades de um Choice sempre somam 1, então alguma linha fica em primeiro lugar mesmo quando o documento não responde nada. Um Noul separado responde se existe resposta no documento, e esse valor não depende das opções.

Quantas linhas cabem por requisição?

Uma pergunta Choice aceita até 255 opções, então a receita cobre documentos de até 255 linhas por requisição. Acima disso, o cookbook sugere duas passagens: um Choice escolhe a janela de linhas e um segundo ordena dentro dela.

Que limiares o experimento usou?

No cookbook, exists a partir de 0,70 conta como respondido e abaixo de 0,35 como ausente; entre os dois, parcialmente abordado. O próprio texto recomenda calibrar esses cortes nos seus documentos antes de usar em produção.

Isso substitui busca vetorial?

Para um documento, resolve sem índice nenhum. Para um acervo grande, não: você continua precisando de uma etapa de busca rápida para achar o documento, e aí a receita de reranking é a companheira natural.

Quer dominar decisões com IA em português? O curso da comunidade está em pré-venda.

Garantir pré-venda por R$ 499,00

Pré-venda: R$ 499,00 · Após o lançamento: R$ 799,00