
Construí uma demo para reproduzir um erro famoso de IA. Meu baseline se recusou a cometê-lo.
O erro que eu não conseguia fazer acontecer
Comecei esta construção querendo recriar uma falha específica e bem documentada. A IA de correspondência de pacientes lê uma anotação clínica como texto, então confunde palavras que se parecem, mas significam coisas diferentes na medicina. O exemplo canônico é limpo: um ensaio de anticoagulante de Fase III exclui pacientes com cateterismo cardíaco prévio, a anotação de um paciente diz colocação de cateter venoso central, um matcher de similaridade vê dois procedimentos de cateter cardiovascular, pontua-os como próximos e exclui um paciente que na verdade era elegível. Avaliações publicadas confirmam que modelos reais cometem exatamente este erro (Fierce Biotech, 2025). Eu queria que minha demo mostrasse isso acontecendo e, em seguida, mostrasse meu motor capturando-o.
Então escrevi um baseline justo para fazer o papel de vilão. Similaridade de cosseno TF-IDF no nível de entidade, palavras mais 3 a 5-gramas de caracteres, um método real de similaridade vetorial. Até lhe dei uma configuração generosa e fiz validação cruzada do limiar de decisão em benefício dele (ROC estratificada em 3 folds, J de Youden, semente 13, fixando em t = 0.6932), porque um espantalho não prova nada. Depois rodei o caso do cateterismo cardíaco e esperei a exclusão injusta.
Ela não veio. O baseline pontuou as duas frases de cateter confortavelmente abaixo do seu próprio limiar com validação cruzada e retornou elegível. O erro em torno do qual eu havia construído toda a demo simplesmente não se reproduzia.
Eu havia me proposto a encenar uma falha famosa e descobri que meu vilão honesto era fraco demais para cometê-la.
O motivo acabou sendo instrutivo, e quero ser preciso sobre ele porque é fácil exagerar. Um baseline lexical esparso não produz aquela exclusão falsa em particular. É preciso embeddings semânticos densos para aproximar essas duas frases o suficiente para disparar o limiar. Adicionar um modelo de embedding pesado teria explodido a demo em algo que você não consegue rodar offline com um único comando, então tomei uma decisão: manter o baseline honesto e esparso, e parar de fingir que ele comete um crime que não consegue cometer. Essa decisão reorganizou toda a peça que estou construindo. Se quiser rodar você mesmo, ela está em veriprajna.com/pt-BR/demos/motor-de-raciocinio-de-elegibilidade-para-ensaios-clinicos.
O que é, de fato, uma linha central?
Ainda mantive o caso do cateterismo cardíaco, porque acabou provando algo melhor do que um erro capturado. Ele prova por que a resposta do meu motor é confiável de fato. Ambos os conceitos aqui têm identificadores SNOMED-CT reais e verificáveis. Cateterismo venoso central é 392230005. Cateterismo cardíaco é 41976001. Você pode colar qualquer um em qualquer navegador público SNOMED e confirmar que eles ficam em ramos diferentes da hierarquia. Não há caminho is-a de um para o outro. Uma linha central não é um cateterismo cardíaco, e só uma hierarquia sabe disso.
Essa é a tese inteira em uma aresta de um grafo. Uma pontuação de similaridade não consegue representar "is-a." Ela só consegue representar "essas strings se parecem," e parecer-se não é o mesmo que significar a mesma coisa. Quando meu motor avalia a exclusão "sem cateterismo cardíaco prévio," ele não pontua nada. Ele faz uma pergunta estrutural: o fato verificado do paciente está subsumido pelo conceito proibido? Ele percorre a ontologia, não encontra caminho de subsunção e retorna elegível com um rastro de três passos nomeando ambos os IDs de conceito e a aresta do grafo que verificou.

Quando vi aquele rastro renderizar pela primeira vez, o que me atingiu não foi o veredito. Foi o recibo por baixo dele. A caixa do baseline na mesma tela mostra um número de similaridade sem proveniência e sem nada reproduzível. A caixa do meu motor nomeia os dois SCTIDs e a pergunta exata is-a que fez. Um destes, um regulador pode arquivar. O outro é um número com um dar de ombros anexado. Esse contraste, não um erro capturado, é o que o caso do cateterismo cardíaco de fato conquista.
O paciente que o matcher de fato descartou
Ainda precisava de um paciente perdido de verdade, então fui procurar onde meu baseline honesto realmente falha, e encontrei em uma palavra: não. O prontuário herói sintético P-074 traz a linha de anotação "Sem evidência de diabetes." Uma das exclusões do protocolo de oncologia é "sem diagnóstico de diabetes mellitus." O baseline vetorial vê o token "diabetes" sentado bem ao lado do "diabetes" do critério e os corresponde com similaridade 1.0. Uma pontuação perfeita. Ele não tem modelo de negação, então lê uma frase que deixa o diabetes fora como se incluísse o diabetes dentro, e exclui um paciente que era elegível.
Este é o paciente que o matcher descarta, e é o momento que eu originalmente esperava que o caso do cateterismo cardíaco carregasse. A negação é onde um baseline esparso quebra honestamente, no seu próprio melhor limiar, sem manipulação.

Fico pensando em como essa falha é silenciosa. Não há mensagem de erro, nenhum sinalizador de baixa confiança, nenhum sinal de que algo deu errado. A pontuação é 1.0, a mais alta possível, a mais confiante que o sistema pode ser. O baseline nunca tem mais certeza do que no exato momento em que está mais errado. Um coordenador revisando uma fila dessas não tem como saber que essa correspondência perfeita em particular é um paciente que deveria ter sido incluído. Multiplique isso por um protocolo e você entende por que 80% dos ensaios perdem seus prazos de recrutamento (consenso do setor, 2025), e por que cada falha de triagem custa em média cerca de US$ 1.200 (Antidote.me, 2025).
O baseline nunca esteve mais confiante do que no exato momento em que estava mais errado. Isso não é um bug que se possa ajustar. É um erro de categoria.
Por que o veredito mora fora do modelo?
Tomei cedo uma decisão arquitetural que agora acho que foi a única que importou, e foi manter o modelo de linguagem longe do veredito por completo. Há exatamente um passo probabilístico em todo o pipeline. Um modelo intercambiável por provedor, apenas consultivo, lê a prosa bagunçada e propõe fatos candidatos, cada um carregando o trecho literal de onde leu o fato e um ID de conceito candidato tirado de um vocabulário fechado pequeno. Essa é a única coisa em que um modelo é genuinamente bom: ler. Ele não tem voto sobre quem é elegível.
Tudo depois disso é código determinístico que posso auditar. Antes que qualquer fato proposto chegue a uma decisão, um verificador adversarial o desafia contra a anotação literal com três checagens: o trecho está de fato presente, está negado, e o sujeito é o paciente e não um familiar. O fato "Sem evidência de diabetes" falha na checagem de negação e nunca chega ao motor. No mesmo prontuário, "Histórico familiar de câncer de mama" falha na checagem de sujeito, porque aquele histórico pertence a um familiar e não ao paciente, e é sinalizado como rejeitado com a checagem falha nomeada.

No conjunto gold completo, este verificador rejeitou 7 instâncias de fato, 3 fatos ruins distintos (uma menção de diabetes negada, uma atribuição de câncer de mama por histórico familiar e um medicamento alucinado plantado sem trecho de suporte), espalhados por 4 das 13 execuções de caso pontuadas, todos antes de poderem tocar um veredito. Quando as pessoas me perguntam "como confio no que o agente extraiu das minhas anotações," este painel é a resposta inteira. Não peço que você confie. Mostro o que ele propôs e o que foi descartado e por quê.
Depois o veredito em si é Python puro sentado fora do framework de agentes: um motor de lógica deôntica que avalia proibições, exceções temporais e requisitos sobre a ontologia e um pouco de matemática de datas. Um modelo não pode sobrescrever este portão, porque o modelo não está na sala quando o portão roda. Isso também é o que torna o motor reproduzível. Quando a lógica é código determinístico sobre uma ontologia fixa, rodar de novo o mesmo prontuário produz a mesma resposta, byte a byte, todas as vezes.
O modelo lê. Ele não vota. Essa única fronteira é o que torna uma reexecução idêntica em bytes.
O único número com que meu leitor de operações clínicas se importava
Passei semanas otimizando métricas que, acabei admitindo para mim mesmo, o comprador não perde o sono. A acurácia de decisão é um número de ranking. A pessoa que cuida da viabilidade em um patrocinador ou CRO não está comparando pontuações de ranking. Está assistindo um cronograma de recrutamento escorregar, e cada dia de atraso é caro. O Tufts CSDD Impact Report (2024) coloca o custo de um atraso de recrutamento em cerca de US$ 800 mil por dia em vendas de prescrição perdidas, e mais alto nas áreas terapêuticas que esta demo toca: cerca de US$ 840 mil por dia em oncologia e US$ 1,4 milhão por dia em cardiovascular. A complexidade de protocolo subiu 139% em procedimentos de ensaio desde 2005 (IQVIA, 2026), o que significa mais critérios, mais cláusulas e mais lugares para um matcher de texto errar um.
Então parei de liderar com acurácia e comecei a liderar com o número que de fato mapeia essa dor: pacientes elegíveis que você não perdeu. Em um conjunto gold rotulado fixo de 13 casos tirados de 7 pacientes sintéticos em 2 protocolos sintéticos, meu motor perde 0 pacientes elegíveis. O baseline justo perde 3. O mesmo conjunto, o mesmo limiar com validação cruzada em benefício do próprio baseline.

Quero ser exato sobre o que esses números são e não são. Eles são a saída do próprio harness naquele único conjunto fixo de 13 casos, não uma promessa de mundo aberto. Os 100% são "100% neste conjunto gold," nunca "sempre certo." Não vou dizer que o TrialProof nunca erra, porque não tenho os dados para dizer isso e não acreditaria em quem dissesse. O que posso dizer é mais estreito e, acho, mais útil: neste conjunto o motor perde zero pacientes elegíveis, cada decisão carrega um rastro reproduzível, duas decisões se abstiveram com segurança com NEEDS-REVIEW quando um laboratório ou sinal vital obrigatório faltava em vez de adivinhar, e reexecutar o conjunto inteiro foi idêntico em bytes, 13 de 13. Todos os pacientes, anotações e protocolos são fixtures sintéticos, nenhum registro real em lugar nenhum. Você pode assistir a cada uma dessas execuções em veriprajna.com/pt-BR/demos/motor-de-raciocinio-de-elegibilidade-para-ensaios-clinicos.
O número com que me importo não é a acurácia. São os pacientes elegíveis que eu não descartei. Neste conjunto, isso é zero perdidos contra os três do baseline.
Há também uma forma regulatória nisso, e vou nomeá-la com cuidado. A orientação de Clinical Decision Support da FDA de janeiro de 2026 é o framework relevante para um auxílio de correspondência com humano no loop como este. Cada decisão que o motor emite pode ser exportada como um registro CDISC SDTM IE, uma linha por paciente e critério, carregando o veredito, o rastro de raciocínio, os IDs de conceito e a operação deôntica. Isso não é um clearance e não estou reivindicando um. É alinhamento e direção. Mas significa que o rastro não é uma conveniência de depuração. É um artefato arquivável, e existe por construção em cada decisão em vez de como um pensamento posterior.
Para onde continuo voltando
Continuo voltando ao momento em que meu vilão se recusou a cumprir o seu papel, porque isso mudou a pergunta que eu estava fazendo. Por três anos o campo tem perguntado como tornar o modelo melhor em decidir quem é elegível. Melhores prompts, contexto maior, mais recuperação, tudo visando tornar um sistema probabilístico confiável o bastante para decidir sobre a inclusão de um paciente. Passei o primeiro trecho desta construção dentro desse enquadramento também, tentando pegar um modelo em um erro para poder consertar o modelo.
O que finalmente encaixou é que era a camada errada. Uma pontuação de similaridade não consegue representar "is-a," não consegue representar "não," e não consegue representar "a menos que a terapia tenha sido concluída há mais de doze meses antes da randomização." Nenhuma quantidade de prompting acrescenta isso, porque não são problemas de linguagem. São problemas de lógica. Então o movimento sênior não é tornar o modelo confiável. É tornar a confiança desnecessária. Deixe o modelo fazer a única coisa em que é bom: ler prosa e propor fatos com o trecho de onde os leu. Depois tenha um verificador que descarte o que a anotação não sustenta, e tenha código simples e auditável sobre uma ontologia médica computar o veredito.
A elegibilidade deve ser computada, não prevista. O que eu não esperava, ao entrar, era que o retorno não pareceria um benchmark de forma alguma. Parece um recibo. O mesmo prontuário dá a mesma resposta todas as vezes, a resposta nomeia o ID do conceito e a aresta do grafo que a decidiu, e o número com que um líder de viabilidade de fato perde o sono vai a zero pacientes elegíveis descartados.
E se você preferir assistir a isso do que me ler descrevendo, aqui está a coisa toda rodando de ponta a ponta.
Então aqui está a pergunta que não parei de revolver, e eu genuinamente gostaria de saber como você responde. Quando o que está em jogo é a chance real de uma pessoa em um ensaio, onde você quer que sua confiança more: em um modelo em que você tem de acreditar, ou em código que você pode ler?


