Uma ferramenta é uma promessa. Dê a ela um schema.
Uma ferramenta com uma interface frouxa é uma promessa que o modelo pode quebrar, um schema na camada de chamada transforma essa promessa num contrato que o sistema consegue impor, para que o modelo tente de novo em vez de alucinar uma forma.
Apollo Space Research
Apollo Space
Um agent é pedido para criar um evento de calendário. Ele produz uma tool call com a data escrita como “próxima quinta”, a duração como um amigável “umas hora” e o participante como um nome em vez de um endereço. Cada palavra é razoável. Cada palavra está errada para a função do outro lado, que queria um timestamp ISO, uma contagem inteira de minutos e um email. A chamada falha, ou pior, ela meio-funciona e marca o slot errado. Ninguém digitou uma instrução ruim. O modelo simplesmente inventou uma forma que a ferramenta nunca concordou em aceitar.
Esse gap, entre o que o modelo emite e o que a ferramenta de fato vai aceitar, é onde a maioria das falhas de agent vive. Não no raciocínio. No aperto de mão.
Uma ferramenta com uma interface frouxa é uma promessa que o modelo pode quebrar. O conserto é fazer da promessa algo que o sistema consegue checar.
Este post é sobre esse conserto: por que input e output estruturados, validados por schema, são a diferença entre uma ferramenta que você torce para o modelo usar corretamente e uma ferramenta que o modelo não consegue usar incorretamente sem ser mandado, na hora, tentar de novo.
A ferramenta ingênua: uma docstring e uma prece
O primeiro jeito que todo mundo constrói uma ferramenta é o jeito óbvio. Você escreve uma função, dá um nome a ela, escreve uma frase de documentação, e entrega ao modelo uma lista dessas frases. create_event(details). send_email(message). record_expense(info). O modelo lê a descrição, decide que essa é a ferramenta que quer, e escreve quaisquer argumentos que pareçam certos.
Funciona na demo. O prompt da demo é limpo, o modelo está fresco, o exemplo é o happy path. Então o modelo escreve um JSON blob arrumadinho, a função o aceita, e todo mundo balança a cabeça.
Aí ele encontra o mundo real. O usuário real escreve “marca 30 com o pessoal da Acme amanhã de tarde”. Agora o modelo tem que inventar a forma inteiramente por conta própria. attendees é uma lista ou uma string? Eu chamei a chave de start ou start_time ou when? A data é uma palavra ou um número? A descrição dizia details, details não é uma forma. Então o modelo escolhe uma, com confiança, e em cerca de um terço das vezes ele escolhe uma que a função nunca esperou.
Aqui está a parte que torna isso perigoso em vez de meramente irritante. A função frequentemente aceita a forma errada e faz a coisa errada silenciosamente. Um handler loosely-typed pega a string “amanhã”, falha em interpretá-la, recai para agora, e marca a reunião para este exato minuto. Sem erro. Sem retry. Só um resultado errado vestindo a fantasia de um bem-sucedido. O modelo acha que cumpriu sua promessa. O usuário descobre na quinta que não cumpriu.
O contrato da ferramenta ingênua vive num único lugar: uma frase em inglês que o modelo é livre para interpretar errado. Não há nada no sistema que consiga dizer a diferença entre uma chamada que honrou o contrato e uma chamada que o violou. Então nada diz. A violação navega direto até o usuário.
O schema é o contrato, escrito onde uma máquina consegue ler
A ideia central é simples. Pare de descrever o input da ferramenta em prosa que o modelo interpreta, e comece a declará-lo num schema que o sistema valida.
Um schema é só a promessa tornada explícita e checável por máquina. Não “passe os details”, mas: start é um timestamp ISO-8601 obrigatório; duration_minutes é um inteiro positivo obrigatório; attendees é uma lista obrigatória de strings, cada uma correspondendo a um padrão de email; title é uma string não-vazia obrigatória. Cada campo tem um tipo. Campos obrigatórios são marcados como obrigatórios. Strings que têm uma forma, emails, datas, códigos de moeda, carregam essa forma como uma restrição, não uma esperança.
Agora a chamada passa por um portão antes de jamais chegar à sua função. O modelo emite seus argumentos; o portão os checa contra o schema; só uma chamada que satisfaz cada restrição é permitida passar. Uma string onde um inteiro foi prometido não chega à função e silenciosamente corrompe uma reserva. Ela quica no portão.
Duas coisas mudam no instante em que você faz isso, e elas importam de jeitos diferentes.
A primeira é que o modelo é guiado antes mesmo de chamar. Um modelo capaz a quem se mostra um schema gera contra aquele schema, ele vê duration_minutes: integer e emite um número, vê start: date-time e emite um timestamp. A forma para de ser um chute porque a forma está escrita. Metade das chamadas malformadas nunca acontecem, porque a descrição nunca foi ambígua em primeiro lugar.
A segunda é a que de fato te salva: quando o modelo erra mesmo assim, e numa solicitação difícil, do mundo real, às vezes ele erra, a chamada errada é pega no portão, não em produção. E uma chamada pega pode ser devolvida.
Uma rejeição que o modelo consegue ler é um retry, não um crash
Este é o movimento que transforma validação de uma rede de segurança num loop de feedback, e é a parte que a maioria das pessoas erra.
O instinto ingênuo, uma vez que você adiciona um portão, é tratar uma chamada rejeitada como um erro, logá-la, falhar o turno, surgir um stack trace, desistir. Isso é estritamente melhor que o resultado-errado-silencioso, porque pelo menos é honesto. Mas desperdiça a coisa mais útil que o portão acabou de produzir: uma explicação precisa e estruturada de exatamente o que estava errado.
Quando o schema rejeita uma chamada, ele não diz “input inválido”. Ele diz duration_minutes: esperava integer, recebeu string "umas hora". Ele diz attendees[0]: "o pessoal da Acme" não corresponde ao formato de email. Essa mensagem não é para um log humano. É para o modelo.
Então você a devolve. A chamada rejeitada, com a razão do validador anexada, vai direto para o próximo turno do modelo: aquela chamada não satisfez o contrato, aqui está exatamente qual campo e por quê, tente de novo. E o modelo, que é genuinamente bom em consertar um erro específico e nomeado, conserta. Ele relê “amanhã de tarde”, consulta a restrição que acabou de violar, e emite um timestamp ISO real. A segunda chamada passa pelo portão.
O jeito ingênuo de imaginar uma fronteira de ferramenta é uma parede: a chamada ou passa ou se espatifa na parede. O schema transforma a parede numa catraca que responde. Uma chamada ruim não é o fim da tentativa, é um rascunho corrigido a caminho de um bom. O modelo não é punido pela forma malformada; ele é informado da forma, no único momento em que pode agir sobre a informação.
Um erro de validação que o modelo consegue ler não é uma falha. É a frase mais útil do loop.
É por isso que “deixe o modelo mais esperto” é o lugar errado para gastar. Um modelo mais esperto ainda tem que chutar uma forma não-declarada, e ainda chuta errado nos inputs difíceis. Uma ferramenta com schema e uma rejeição legível deixa um modelo atual se autocorrigir, porque a correção que o modelo precisa não é mais inteligência, é um fato específico sobre o que a ferramenta vai aceitar, entregue no exato momento em que ele está chamando.
O lado do output tem a mesma promessa, e a mesma quebra
Tudo até agora foi sobre o que entra na ferramenta. A metade que as pessoas esquecem é o que sai.
Uma ferramenta que retorna um blob não-estruturado é a mesma promessa quebrada, apontada na outra direção. A função roda, tem sucesso, e retorna “Criei o evento para você!”, uma frase amigável sem estrutura. Agora o próximo passo no plano do agent, que precisa do ID do evento para enviar o convite, tem que interpretar inglês para encontrá-lo. Às vezes não há ID nenhum na frase. O agent, precisando de um valor que não está ali, faz a coisa que modelos fazem quando um valor falta sob pressão: ele inventa um. Um ID de aparência plausível que aponta para nada. A cadeia segue em frente, com confiança, sobre um handle fabricado.
O conserto é simétrico. O retorno da ferramenta também tem schema: { event_id: string, start: timestamp, status: enum }. A função é contratada a produzir exatamente isso, e o valor é validado na saída do mesmo jeito que os argumentos foram validados na entrada. O próximo passo não interpreta prosa para encontrar o ID, ele lê um campo que tem garantia de estar lá, ou a ferramenta falhou honestamente e disse isso. Um output estruturado é uma promessa que a ferramenta faz ao resto do sistema, e validá-lo é como você impede um agent downstream de alucinar em volta de um campo faltante.
Input schema impede o modelo de mentir para a ferramenta. Output schema impede a ferramenta de mentir para o modelo. Uma ferramenta digna de confiança honra o contrato nas duas direções, e o contrato é imposto na fronteira, não assumido na prosa.
O que essa disciplina de fato compra
Vale ser claro sobre o trade, porque schemas não são de graça. Escrevê-los é mais trabalho que escrever uma docstring. Você tem que decidir, de antemão, exatamente o que cada campo é e que forma ele tem, o que significa que você tem que de fato saber o que a sua ferramenta aceita, em vez de descobrir em produção quando um valor errado navega através.
Esse custo antecipado é o ponto inteiro. A falha cara num sistema de agent nunca foi a chamada malformada em si. Foi a chamada malformada que teve sucesso em estar errada, a reunião marcada para o minuto errado, a despesa registrada contra o campo errado, o passo downstream rodando sobre um ID fabricado, tudo silencioso, tudo descoberto tarde, por um usuário, como um bug difícil de rastrear porque nada nunca lançou um erro.
Um schema move essa falha de produção para a fronteira de chamada, e de silenciosa para barulhenta-mas-recuperável. Suponha que uma ferramenta seja chamada mil vezes numa semana movimentada pelos agents de uma empresa. O punhado de chamadas malformadas que uma interface frouxa teria transformado em resultados errados silenciosos é em vez disso pego, nomeado, devolvido e corrigido dentro do mesmo turno, antes de qualquer coisa tocar um calendário, um livro-razão ou um cliente. Você não as vê como incidentes. Você não as vê de jeito nenhum. Esse é o sinal de que está funcionando.
A confiança não vive no modelo ser cuidadoso. Ela vive na fronteira ser checável. Esse é um lugar muito mais confiável para mantê-la, porque a fronteira não se cansa, não recebe um prompt confuso, e não tem um dia bom e um dia ruim. Ela só segura o contrato.
A virada: um contrato é uma gentileza, não uma restrição
Tire os agents disto e é uma ideia antiga sobre trabalhar com qualquer um, máquina ou pessoa.
O jeito mais rápido de tornar um colaborador não-confiável é ser vago sobre o que você precisa e então ficar irritado quando ele chuta errado. O jeito mais rápido de torná-lo confiável é o oposto: diga a ele exatamente como “certo” se parece, e diga a ele no instante em que ele errou, enquanto ele ainda pode consertar. Não pensamos numa spec clara como uma jaula. Pensamos nela como respeito, aqui está precisamente o que preciso, e aqui está um não rápido e específico quando ainda não é isso, para você chegar ao sim.
Um schema é isso, para uma ferramenta. Ele não está ali para pegar o modelo no flagra. Está ali para que o modelo possa ter sucesso de propósito em vez de por sorte, para que uma forma errada vire um rascunho corrigido em vez de um desastre silencioso, e uma ferramenta pare de ser uma promessa que você torce que se sustente e vire um contrato que o sistema mantém por você.
Essa é uma parte do que estamos construindo na Apollo: agents cujas ferramentas têm bordas de verdade, onde uma chamada ruim recebe um não preciso e uma segunda tentativa em vez de escapulir até morder alguém numa quinta-feira. Se você já lançou um agent que confiantemente fez a coisa errada e nunca lançou um erro, você já sabe que o lugar mais barato de pegar isso não era o modelo. Era o aperto de mão.
A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.
Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.
Entrar na lista de esperaO imposto oculto dos agents em paralelo é um diamante de migrations
Seis agents escrevendo para um schema conflitam no banco de dados, não no código, e a CI morre em "multiple heads".
EngenhariaUm orchestrator que não sobrevive ao próprio crash não é um
Um crash que apaga o raciocínio do orchestrator perde a única coisa que você não consegue reconstruir.
EngenhariaColoque um portão determinístico na frente do seu revisor mais esperto
A pega-defeito mais barata é um script burro que checa se duas branches mergeadas ainda sobem antes de qualquer julgamento.