TL;DR: Um modelo LLM é como Don Quijote — você não pode curá-lo, ele é estocástico por natureza. A solução não é corrigir o louco, mas colocar um Sancho Panza determinista ao lado. MDD tem duas camadas: primeiro você estuda os erros que ele comete para projetar ferramentas que os absorvam, e depois o solta com essas ferramentas para garantir que nenhuma brecha foi deixada. Projete para a loucura, não contra ela.


Passei semanas auditando logs. 165 sessões de um agente de IA interagindo com um CLI para gerenciar tarefas. Mais de 500 erros. 370 reprocessamentos. Padrões se repetindo como um disco arranhado: o agente usava --status quando a flag correta era --state. Escrevia Todo quando a API exigia unstarted. Passava urgent como prioridade quando o sistema só aceitava números.

E o fascinante é que todo erro fazia sentido. Eles não eram erros aleatórios. Eram plausíveis. Exatamente os tipos de erros que você cometeria se conhecesse o domínio “mais ou menos”, mas nunca tivesse lido a documentação com atenção.

Em algum momento, analisando o milionésimo --status Done que deveria ser --state completed, percebi que estava diante de um padrão literário. Um padrão com mais de 400 anos.

Don Quijote é um LLM

Pense por um momento. Don Quijote vê moinhos de vento e diz “são gigantes”. Ele não é burro — é um sujeito educado, leu muito, conhece todas as histórias de cavalaria. O problema é que seu modelo mental está contaminado por dados de treinamento fictícios. Ele leu tantas novelas de cavalaria que, ao ver algo ambíguo, interpreta segundo seu training data. Moinhos → gigantes. Rebanhos → exércitos. Estalagens → castelos.

Um LLM faz exatamente isso. Ele viu milhares de APIs no treino. Quando você pede que use uma que ele não conhece bem, ele não responde “não sei”. Ele adivinha. E adivinha bem. Quase sempre. O suficiente para você confiar. Mas quando erra, o erro é plausível.

--status em vez de --state. Porque em 60% dos CLIs que ele já viu, a flag se chama --status.

Todo em vez de unstarted. Porque na interface gráfica da ferramenta, a coluna diz “Todo”. O LLM viu capturas de tela na documentação. Ele leu blogs. Inferiu que, se a UI diz “Todo”, a API aceita “Todo”. Faz sentido. Só que está errado.

urgent em vez de 1. Porque na maioria dos sistemas de prioridade, urgent é um valor válido. Quem projeta uma API onde a prioridade é um número entre 1 e 4 sem usar labels?

Cada alucinação é uma inferência razoável sobre dados incompletos. Don Quijote não é burro. Ele está insano. E você não consegue curar um insano.

O que Cervantes já sabia

Cervantes não tenta curar Don Quijote. O que ele faz é colocar Sancho Panza ao lado dele.

Sancho não é brilhante. Ele não leu os livros. Não tem visões grandiosas. Mas é determinista. Quando Don Quijote diz “olha aqueles gigantes”, Sancho diz “senhor, são moinhos”. Nem sempre Don Quijote o ouve, mas a informação está lá. O sistema tem duas camadas: uma estocástica que gera hipóteses (Don Quijote) e uma determinista que as contrasta com a realidade (Sancho).

Essa é a arquitetura de que você precisa ao trabalhar com um LLM. Você não vai conseguir fazer que ele deixe de alucinar. É sua natureza. Mas pode adicionar camadas deterministas para capturar suas alucinações antes que elas causem problemas.

E é aqui que entra a metodologia.

MDD: Madness Driven Design

MDD tem duas camadas, e a ordem importa.

Camada 1: Arqueologia a priori

Antes de escrever uma linha de código, você estuda a loucura. Não imagina — observa. Coleta dados reais de como o LLM interage com ferramentas existentes e classifica os erros.

No meu caso, analisei 165 sessões de um agente de IA utilizando um CLI para gerenciar as tarefas de uma equipe de desenvolvimento. Os números:

Categoria de erroOcorrênciasReprocessamentos gerados
Flags inventados ou proibidos275~150
Escape quebrado de JSON/GraphQL2580+
Confusão de nomenclatura40+50+
Operações impossíveis na CLI60+90+
Output verbose que consome tokensN/AN/A

Com esses dados, você projeta uma nova ferramenta para absorver esses erros, em vez de rejeitá-los. Em linguagem simples: o sensato se adapta ao louco, não o contrário.

Exemplos concretos de absorção:

Erro do LLM              → Design da ferramenta
─────────────────────────────────────────────────────
--status Done            → --status serve como alias para --state
                           "Done" é normalizado para "completed"

--priority urgent        → "urgent" é normalizado para 1
                           "high" → 2, "medium" → 3, "low" → 4

--no-pager               → Flag silenciosamente ignorado
                           (a ferramenta nunca usa pager)

Escape quebrado          → Input via arquivo ou stdin
em descrições              Nunca inline. Serde resolve tudo.

Cada linha dessa tabela é uma decisão de design baseada em erros reais observados. Não em especulações sobre “o que poderia falhar”, mas em logs claros que mostram “isto falhou 40 vezes em 165 sessões”.

A diferença com o design convencional é sutil, mas importante. No design tradicional, você define a interface correta e rejeita o que não se encaixa. No MDD, você define a interface correta e todas as interfaces incorretas que seu usuário vai tentar, e adapta para absorvê-las.

É como projetar uma porta que abre tanto ao empurrar quanto ao puxar. A porta “correta” abre apenas em uma direção. A porta boa abre nas duas, porque você observou que 40% das pessoas empurram quando deveriam puxar.

Camada 2: Verificação a posteriori

Você constrói a ferramenta com as defesas da Camada 1 e depois a solta. Coloca a nova ferramenta à disposição do LLM e observa quais erros novos ele comete.

Se a Camada 1 foi bem feita, os erros novos deveriam ser mínimos. Caso novos erros surjam, você descobriu brechas no seu design. Cada erro novo é um teste de penetração involuntário.

Quando fiz isso com meu CLI, o LLM inventou coisas que não apareceram na auditoria inicial:

  • Um enum de ordenação inexistente. A API permite ordenar por createdAt e updatedAt. O LLM inventou um valor priority para ordenação. Faz todo sentido — por que não ordenar por prioridade? Mas esse valor não existe no schema GraphQL.

  • Um operador de filtragem impossível. Para filtrar por estado, a API aceita state.type.in. O LLM gerou state.id.or. Sintaxe coerente, padrão lógico, completamente inventado.

  • Uma função de file locking de outra linguagem. Em um projeto Rust, o LLM sugeriu fcntl.flock para bloquear arquivos. Isso é Python. Em Rust você usa o crate fs2.

Cada um desses erros foi plausível. Nenhum foi absurdo. E cada um revelou uma brecha: a ferramenta não validava os valores do enum de ordenação, não rejeitava operadores de filtragem inventados, e a documentação sobre o crate de file locking não estava no contexto do agente.

A Camada 2 completa o ciclo. Não confie que o design é perfeito — verifique soltando o agente mais criativo (e mais propenso a alucinar) que você tem.

A Stack Sancho Panza

A metáfora de Don Quijote e Sancho Panza não é apenas uma analogia bonita. É uma arquitetura. Na prática, o “Sancho Panza” não é uma peça única — é uma stack de camadas deterministas, cada uma capturando um tipo específico de loucura:

┌──────────────────────────────────────┐
│         LLM (Don Quijote)            │  Gera comandos plausíveis
│         Estocástico, criativo        │  mas potencialmente falsos
└──────────────┬───────────────────────┘
               │ "--status Done --priority urgent"
┌──────────────▼───────────────────────┐
│  1. CLI Parser (clap)                │  Rejeita flags inexistentes
│     Aceita aliases: --status→--state │  nem como aliases
└──────────────┬───────────────────────┘
               │ "--state Done --priority urgent"
┌──────────────▼───────────────────────┐
│  2. Normalização                     │  "Done"→"completed"
│     state-aliases, priority-aliases  │  "urgent"→1
└──────────────┬───────────────────────┘
               │ "--state completed --priority 1"
┌──────────────▼───────────────────────┐
│  3. Validação                        │  "completed" é estado válido?
│     Contra enums conhecidos          │  1 está dentro do range?
└──────────────┬───────────────────────┘
               │ state=completed, priority=1
┌──────────────▼───────────────────────┐
│  4. Serialização (serde)             │  Escape correto por princípio.
│     Variáveis GraphQL, não strings   │  Evita aspas quebradas e
│     interpoladas                     │  injeção.
└──────────────┬───────────────────────┘
               │ {"state":"completed","priority":1}
┌──────────────▼───────────────────────┐
│  5. API + tratamento de erros        │  Se a API rejeita algo,
│     Retry com backoff, mensagens     │  o erro é compreensível
│     acionáveis                       │  pelo usuário.
└──────────────────────────────────────┘

Cinco camadas. Cada uma determinista. Cada uma captura um tipo específico de erro esperado. O LLM não precisa estar certo — só precisa estar aproximadamente certo, e a stack cuida do resto.

MDD vs. fuzz testing: A diferença crucial

Se você já conhece fuzz testing, pode estar pensando “isso é igual”. Não é.

Fuzz testingMDD
InputAleatório, quebradoPlausível, coerente, bem escrito
ObjetivoDetectar crashes ou bugs sériosIdentificar erros semânticos
Input parece válido?NãoSim — esse é o problema
Exemplo\x00\xff\xfe como nome--priority urgent como flag

Um fuzzer gera lixo para ver se seu programa falha. MDD cria entradas que parecem válidas, mas são factualmente incorretas. --priority urgent não é lixo — é exatamente o que um humano que conhece o domínio mas não a API escreveria. Um fuzzer nunca geraria isso porque exige coerência demais.

A ideia prática

Você não precisa criar um CLI em Rust para aplicar MDD. O padrão funciona em qualquer ferramenta que um LLM vá utilizar:

Passo 1: Observe a loucura. Antes de projetar (ou reprojetar) uma ferramenta, deixe o LLM usar a versão atual e registre cada erro. Não 5 sessões — 50. Padrões precisam de volume para aparecer.

Passo 2: Classifique os erros. Eles são de nomenclatura? Formato? Semântica? Cada categoria exige uma defesa diferente.

Passo 3: Projete para absorver. Não rejeite --status com um erro confuso. Aceite --status como alias de --state. Não rejeite urgent como prioridade. Normalize para 1. O usuário que vai te usar mais é um agente que conhece 80% do domínio. Projete para esses 80%.

Passo 4: Teste e verifique. Solte a nova ferramenta para o LLM sem instruções especiais. Cada novo erro que ele comete é uma falha em sua Camada 1. Corrija e repita.

Se sua ferramenta for para humanos e LLMs, as defesas MDD melhoram a experiência para ambos. Humanos cometem os mesmos erros que LLMs — só que com menos frequência e mais vergonha.

Quem projeta o Sancho é o arquiteto

Existe um equívoco comum que eu quero esclarecer. O LLM não projeta o Stack Sancho Panza. Ele é Don Quijote. Você é Cervantes.

Você observa os padrões de loucura. Você decide o que deve ser normalizado ou rejeitado. Você constrói as camadas deterministas. O LLM pode ajudar a implementar — ele é bom em escrever código — mas o design é seu papel.

Nem a pau confie que o LLM corrigirá seus próprios erros. Sua natureza estocástica garante que ele repetirá os mesmos erros com variações criativas. Você não precisa de um LLM melhor — precisa de um Sancho melhor.

O que realmente importa

MDD não é uma metodologia de teste. É uma metodologia de design de ferramentas. A pergunta não é “como detecto que o LLM errou?” e sim “como projeto para que os erros não gerem consequências?”.

É a mesma filosofia dos guardrails em curvas de estradas perigosas. Você não impede que as pessoas façam curvas ruins — coloca uma barreira para que curvas ruins não sejam fatais. Você não corrige o motorista — protege o caminho.

Cervantes entendeu isso há quatro séculos. Ele não tentou curar Don Quijote. Ele colocou um Sancho ao lado e viu a história funcionar.

Seu CLI, sua API, seu SDK — o que quer que um LLM vá usar — precisa de um Sancho. Determinista, persistente, incapaz de alucinar. Nada brilhante ou criativo. Apenas correto.

Projete para a loucura. O sensato se adapta ao louco.