Objetivo da Aula
Separar com precisão o que é decisão não-determinística (qual tool, quais argumentos) do que precisa ser execução determinística (o efeito real no sistema)
Projetar schemas de tool que reduzem o espaço de erro do modelo antes de qualquer execução acontecer
Classificar tools por raio de impacto — leitura, escrita reversível, escrita irreversível — e aplicar gates de aprovação proporcionais
Especificar o pipeline de governança que o harness executa entre "o modelo decidiu chamar" e "a chamada de fato aconteceu"
Usar idempotência, credenciais de escopo limitado e dry-run para tornar chamadas de escrita seguras mesmo sob retry de um chamador probabilístico
Por que isso importa
O Capítulo 3.1 resolveu o problema de topologia: como o harness descobre e se conecta a tools de forma padronizada, e quem tem permissão de ver o quê. Este capítulo resolve o problema seguinte, mais difícil, que aparece assim que a resposta a "o agente pode ver esta tool" é sim: o que garante que, quando o modelo decide chamá-la, a chamada acontece do jeito certo, uma vez, com os argumentos certos, e deixa rastro de por que aconteceu?
Essa pergunta importa porque um LLM não é uma função determinística. O mesmo prompt, no mesmo agente, pode produzir uma chamada de tool com argumentos ligeiramente diferentes em duas execuções, pode tentar chamar uma tool duas vezes por engano após um timeout, pode preencher um campo numérico com um valor fora do intervalo esperado, pode até "alucinar" uma tool que não existe. Nada disso é hipotético — é o comportamento normal e esperado de um sistema probabilístico sob incerteza de contexto. A pergunta arquitetural não é "como impedimos o modelo de errar" — é impossível eliminar isso na origem. A pergunta é: onde exatamente termina o não-determinismo e começa a garantia?
Impacto direto no seu trabalho: toda vez que você aprova uma tool de escrita para um agente — emitir reembolso, cancelar assinatura, fazer merge em um repositório, disparar um pagamento — você está desenhando essa fronteira. Desenhar mal significa ou travar o agente com aprovação humana em tudo (perde-se o ganho de automação) ou deixar o modelo executar ação irreversível sem verificação (perde-se controle). O trabalho do arquiteto é projetar o meio de campo: o pipeline que deixa o modelo decidir livremente, mas nunca deixa a decisão virar efeito sem passar por uma camada determinística de validação.
Conceitos Fundamentais
Onde mora o não-determinismo — e onde ele deve parar
Todo tool call tem duas metades com naturezas diferentes. A primeira metade é a decisão: qual tool chamar e com quais argumentos, produzida pelo modelo com base em contexto, prompt e no inputSchema publicado pelo servidor MCP. Essa metade é, por natureza, probabilística — depende de amostragem, de como o contexto foi montado, de qual foi o histórico da conversa. A segunda metade é a execução: o código do servidor MCP recebendo argumentos já validados e produzindo um efeito no sistema real. Essa metade deve ser determinística — os mesmos argumentos válidos sempre produzem o mesmo comportamento.
| Etapa | Conteúdo | Observação |
|---|---|---|
| 1. Entrada | Contexto + prompt | — |
| 2. Decisão do LLM | Qual tool, quais argumentos | Não-determinístico — amostragem, depende de contexto |
| 3. Fronteira | Validação de schema + política | É aqui que o harness governa |
| 4. Execução da tool | Mesmos argumentos válidos → mesmo efeito | Determinístico — código do servidor, sem ambiguidade de interpretação |
O erro de projeto mais comum é tratar as duas metades como uma coisa só — confiar que, porque o modelo "geralmente" produz argumentos sensatos, a validação na fronteira pode ser leve ou inexistente. Isso funciona em demo e falha em produção, porque "geralmente" não é uma garantia arquitetural. A fronteira precisa ser um portão real: nada passa da decisão para a execução sem ser validado contra um contrato explícito.
Design de schema como contenção de erro
A ferramenta mais eficaz para reduzir a taxa de erro do modelo não é instrução em linguagem natural ("por favor, use apenas valores válidos") — é restrição estrutural no próprio inputSchema da tool. Um schema bem desenhado torna certas classes de erro estruturalmente impossíveis de produzir, em vez de apenas prováveis de evitar.
// Schema fraco — abre espaço de erro grande
{
"name": "emitir_estorno",
"inputSchema": {
"type": "object",
"properties": {
"pedido_id": { "type": "string" },
"valor": { "type": "string" },
"motivo": { "type": "string" }
}
}
}
// Problemas: "valor" como string livre aceita "cem reais",
// "R$100", "100.00" — três formatos, mesmo campo. "motivo"
// como texto livre não classifica o tipo de estorno.
// Schema forte — restringe o espaço de erro
{
"name": "emitir_estorno",
"inputSchema": {
"type": "object",
"properties": {
"pedido_id": {
"type": "string",
"pattern": "^PED-[0-9]{8}$"
},
"valor_centavos": {
"type": "integer",
"minimum": 1,
"maximum": 5000000
},
"motivo": {
"type": "string",
"enum": ["produto_defeituoso", "atraso_entrega",
"cancelamento_cliente", "erro_cobranca"]
},
"idempotency_key": {
"type": "string",
"pattern": "^[a-f0-9]{32}$"
}
},
"required": ["pedido_id", "valor_centavos", "motivo",
"idempotency_key"]
}
}Note o que mudou: valor virou inteiro em centavos com limite máximo (elimina ambiguidade de formato e impõe um teto estrutural), motivo virou enum fechado (elimina texto livre onde deveria haver categoria), e um idempotency_key obrigatório entrou no contrato — sobre isso, mais adiante nesta seção. Cada uma dessas restrições fecha uma classe inteira de erro antes de qualquer linha de código de validação ser escrita à mão.
Fundamento: Decodificação Restrita (Constrained Decoding)
Provedores modernos de LLM com suporte a tool calling não pedem, em prosa, que o modelo "gere um JSON válido" — eles usam decodificação restrita: o processo de amostragem de tokens é limitado, token a token, ao conjunto de continuações que mantêm a saída em conformidade com o inputSchema declarado. Na prática, isso significa que um enum com três valores torna estruturalmente impossível o modelo produzir um quarto valor — não é uma questão de o modelo "obedecer" a instrução, é uma restrição na própria amostragem. É por isso que o schema, não o texto da instrução, é a camada real de segurança da decisão. Um prompt bem escrito ajuda o modelo a decidir bem; um schema bem escrito impede que ele produza uma saída inválida, independentemente de quão bem decidiu.
Classificação por raio de impacto
Nem toda tool merece o mesmo nível de escrutínio. Uma consulta de leitura que não altera estado tem um perfil de risco completamente diferente de uma ação que move dinheiro ou apaga dados. A prática que sustenta um pipeline de governança proporcional — e não paranóico a ponto de travar o agente em toda chamada — é classificar cada tool numa escala de raio de impacto, decisão que se conecta diretamente com o RBAC do Agent Loop descrito na Parte 1 desta série.
| Nível | Exemplos | Efeito colateral | Governança |
|---|---|---|---|
| 0 — Leitura pura | consultar_status_pedido, buscar_cliente | Nenhum | Execução automática, log padrão |
| 1 — Escrita reversível, baixo impacto | criar_ticket, atualizar_endereco_entrega | Sim, mas desfazível sem custo relevante | Execução automática + auditoria detalhada |
| 2 — Escrita reversível, alto impacto ou valor | emitir_estorno (acima de um limite), aplicar_desconto | Sim, desfazível mas com custo/atrito | Aprovação humana (HITL) acima de limiar definido |
| 3 — Escrita irreversível | excluir_cliente, cancelar_assinatura definitivamente, fazer_merge_em_producao | Irreversível ou de reversão muito custosa | Aprovação humana obrigatória, sempre, sem exceção por limiar de valor |
O detalhe que faz essa escala funcionar de verdade é que ela não é uma propriedade estática só da tool — pode depender também dos argumentos da chamada específica. emitir_estorno de R$30 e emitir_estorno de R$8.000 são, do ponto de vista de schema, a mesma tool; do ponto de vista de governança, pertencem a níveis de risco diferentes. Isso significa que a classificação de raio de impacto precisa ser avaliada pelo harness em tempo de chamada, não apenas registrada uma vez no catálogo de tools.
O pipeline de governança da chamada de ferramenta
Juntando schema, classificação de risco e a política de acesso do capítulo anterior, o harness aplica uma sequência fixa de etapas entre "o modelo propôs uma chamada" e "a chamada de fato executou". Essa sequência é o núcleo determinístico que este capítulo defende — cada etapa é código convencional, sem ambiguidade, mesmo que a decisão que a disparou tenha sido probabilística:
function processar_chamada_de_tool(agente, chamada_proposta_pelo_llm):
// 1. A tool proposta existe e está no conjunto que este
// agente tem permissão de ver? (Capítulo 3.1)
tool = resolver_tool(chamada_proposta_pelo_llm.nome)
se tool == null ou !agente.pode_ver(tool):
retornar erro_estruturado("tool_desconhecida_ou_nao_permitida")
// 2. Os argumentos batem com o inputSchema publicado?
se !validar_schema(tool.inputSchema, chamada_proposta_pelo_llm.args):
retornar erro_estruturado("argumentos_invalidos", detalhes)
// 3. Classificar risco desta chamada específica (tool + args)
nivel_risco = classificar_risco(tool, chamada_proposta_pelo_llm.args)
// 4. Checar permissão de execução para este agente e este nível
se !agente.pode_executar(tool, nivel_risco):
retornar erro_estruturado("permissao_negada")
// 5. Gate de aprovação humana, se o nível de risco exigir
se nivel_risco >= LIMIAR_HITL:
aprovacao = solicitar_aprovacao_humana(agente, tool, args)
se aprovacao.negada:
retornar erro_estruturado("aprovacao_negada", aprovacao.motivo)
// 6. Executar com credencial de escopo mínimo para esta chamada
credencial = emitir_credencial_escopada(tool, ttl_curto=true)
resultado = executar(tool, chamada_proposta_pelo_llm.args, credencial)
// 7. Registrar auditoria — sempre, independentemente do resultado
registrar_auditoria(agente, tool, chamada_proposta_pelo_llm.args,
nivel_risco, aprovacao, resultado)
retornar resultadoCada uma dessas sete etapas é determinística e testável isoladamente — o único ponto não-determinístico de todo o fluxo é o valor de entrada (chamada_proposta_pelo_llm), que vem do modelo. É essa contenção estrutural que permite auditar, depois do fato, exatamente por que uma ação aconteceu: não porque "o modelo decidiu", mas porque uma sequência específica e reconstruível de validações passou.
Aprofundamento Técnico
Idempotência sob retry de um chamador probabilístico
Um LLM pode, dentro do mesmo loop de agente, tentar chamar a mesma tool duas vezes — porque não recebeu confirmação a tempo, porque interpretou um timeout como falha, ou porque o loop de raciocínio simplesmente perdeu o rastro de que já havia tentado. Em uma tool de leitura, isso é inofensivo: chamar duas vezes devolve o mesmo dado. Em uma tool de escrita sem proteção, isso duplica o efeito:
Sem idempotency key:
Tentativa 1: emitir_estorno(pedido=PED-00012345, valor=5000)
→ timeout na resposta (mas o estorno JÁ FOI processado)
Modelo interpreta timeout como falha, tenta de novo:
Tentativa 2: emitir_estorno(pedido=PED-00012345, valor=5000)
→ processado com sucesso
Resultado: cliente recebeu DOIS estornos de R$50,00
Com idempotency key:
Tentativa 1: emitir_estorno(..., idempotency_key="a1b2c3...")
→ timeout na resposta (mas processado no servidor)
Tentativa 2: emitir_estorno(..., idempotency_key="a1b2c3...")
→ servidor reconhece a chave já processada,
devolve o MESMO resultado da tentativa 1, sem reexecutar
Resultado: cliente recebeu UM estorno de R$50,00A regra prática: toda tool com efeito colateral não-trivial (nível 1 em diante na escala de raio de impacto) deveria exigir, no próprio inputSchema, um campo de idempotência — e o harness, não o modelo, é responsável por gerar essa chave de forma estável por tentativa lógica (a mesma intenção de chamada gera a mesma chave em retries, uma nova intenção gera uma chave nova). Isso tira do modelo a responsabilidade de "lembrar" que já tentou — responsabilidade que um sistema probabilístico não deveria carregar sozinho.
O custo de pular essa proteção é mensurável, não teórico. Considere um agente de Atendimento com autonomia para emitir estornos até R$200, processando um volume moderado de 50 chamadas por dia a essa tool. Sem idempotência, uma taxa de duplicação de apenas 2% — plausível em ambientes com timeout de rede intermitente — gera em torno de 1 chamada duplicada por dia. A um valor médio de estorno de R$80, isso representa um vazamento de aproximadamente R$2.400 por mês, silencioso, distribuído em centenas de transações pequenas demais para acionar qualquer alerta de fraude tradicional. Uma chave de idempotência obrigatória no schema custa uma linha a mais no inputSchema e elimina o problema na origem, estruturalmente, em vez de depender de reconciliação contábil posterior para descobrir o vazamento.
Credenciais de escopo por chamada, não chave estática de longa duração
A prática mais comum — e mais arriscada — em integrações apressadas é embutir uma credencial de API de escopo amplo (às vezes de admin) direto no servidor MCP, usada para todas as chamadas de todos os agentes. O problema não é hipotético: se essa credencial vazar, ou se uma chamada mal governada escapar do pipeline descrito acima, o raio de impacto é o escopo inteiro da credencial, não o escopo da chamada específica.
A alternativa é o harness emitir, a cada chamada aprovada, uma credencial de curta duração e escopo mínimo — um token que autoriza exatamente aquela operação, com aquele identificador de recurso, por uma janela curta de tempo (segundos a poucos minutos), e nada além disso:
// Em vez de: usar sempre a mesma API_KEY_ADMIN_BILLING
// O harness emite, por chamada aprovada:
credencial = emitir_credencial_escopada({
operacao: "emitir_estorno",
recurso: "PED-00012345",
ttl_segundos: 60,
emitida_para: agente.id,
aprovacao_ref: aprovacao.id // rastreável até quem aprovou
})Se essa credencial vazar ou for reusada indevidamente, o dano está contido: expira em sessenta segundos e só vale para uma operação específica sobre um recurso específico. Esse padrão é o análogo, na camada de tool calling, de credenciais de sessão de curta duração já comuns em arquiteturas de microsserviços — aplicado aqui à fronteira entre decisão não-determinística e execução determinística.
Erros que o modelo corrige sozinho vs. erros que exigem humano
Quando a validação de schema rejeita uma chamada (etapa 2 do pipeline), a forma como o erro é comunicado de volta ao modelo determina se o agente se autocorrige no próximo turno ou entra em loop de tentativa e erro caro. Erro estruturado e específico permite autocorreção; stack trace ou mensagem genérica não:
// Erro ruim — não dá ao modelo informação acionável
{ "error": "ValidationError: schema mismatch" }
// Erro bom — específico o suficiente para o modelo corrigir
{
"error": "argumentos_invalidos",
"campo": "motivo",
"problema": "valor 'cliente_insatisfeito' não está entre os "
"valores aceitos",
"valores_aceitos": ["produto_defeituoso", "atraso_entrega",
"cancelamento_cliente", "erro_cobranca"]
}Mesmo com erro bem estruturado, o loop de correção precisa de limite. Um agente que recebe o mesmo tipo de erro de validação três vezes seguidas na mesma tool provavelmente não vai convergir na quarta tentativa — o problema não é de argumento, é de entendimento da tarefa. Um circuit breaker no harness — interromper o loop de retry de tool call após N falhas consecutivas do mesmo tipo e escalar para revisão humana ou para o passo de planejamento do agente — evita que o sistema queime tokens e tempo em um loop que não vai se resolver sozinho. Essa é a mesma lógica de contenção discutida no Ralph Loop, na Parte 1: autonomia tem limite de tentativa, não é ilimitada por padrão.
Granularidade de tools: atômica e composável vs. composta e opaca
Uma última decisão de design atravessa tudo isso: uma tool deveria fazer uma coisa só (debitar_saldo, creditar_saldo, registrar_transacao como três tools separadas), ou deveria compor várias operações internas em uma única chamada (transferir_valor fazendo as três coisas por trás, de uma vez)?
| Aspecto | Tools atômicas | Tool composta |
|---|---|---|
| Exemplo | debitar_saldo(conta, valor), creditar_saldo(conta, valor), registrar_transacao(...) | transferir(origem, destino, valor) |
| Vantagens | Cada chamada é auditável isoladamente; reutilizável em outros fluxos | Menos decisões para o modelo tomar em sequência; menor chance de o modelo parar no meio (débito feito, crédito esquecido) |
| Desvantagens | Modelo precisa orquestrar as 3 chamadas na ordem certa — mais chances de erro de sequenciamento; se o modelo parar no meio, sistema fica em estado inconsistente | Efeito colateral real fica "escondido" atrás de uma interface simples — menos transparente para auditoria granular |
Não há vencedor universal. A regra prática que orienta a decisão: operações que precisam ser atômicas do ponto de vista de negócio (débito e crédito de uma transferência não podem ficar "meio feitos") devem ser uma única tool composta no servidor MCP, com a atomicidade garantida por transação de banco de dados do lado do servidor — nunca depender do modelo para "lembrar" de completar os dois passos. Operações genuinamente independentes, que fazem sentido acontecer isoladamente em fluxos diferentes, ganham mais reuso e mais transparência de auditoria como tools atômicas separadas. A pergunta que decide não é "o que é mais simples para o modelo chamar" — é "o que quebra o sistema se o modelo parar no meio".
Dry-run e verificação pós-execução
Para tools de nível 2 e 3 na escala de raio de impacto, uma última camada de segurança compensa o fato de que nenhuma validação de schema garante que os argumentos, mesmo válidos no formato, produzem o efeito que o operador humano realmente pretendia. Expor um modo de simulação (dry_run: true) que retorna o efeito previsto sem executá-lo de fato permite que o gate de aprovação humana (etapa 5 do pipeline) mostre a um revisor exatamente o que vai acontecer, antes que aconteça — em vez de pedir aprovação de uma descrição textual gerada pelo modelo, que pode não corresponder fielmente aos argumentos reais da chamada.
Depois da execução, a mesma disciplina se aplica de volta: verificar que o efeito esperado de fato ocorreu (consultar o estado do recurso após a chamada, não apenas confiar em um código de retorno 200) fecha o ciclo de confiança. Um retorno bem-sucedido do servidor MCP indica que a chamada foi aceita — não necessariamente que o efeito de negócio pretendido foi alcançado. Essa verificação faz parte do mesmo pipeline determinístico descrito neste capítulo: a última etapa da governança não é "a chamada retornou", é "o efeito esperado está confirmado e auditado".
Testes de contrato para um pipeline com entrada não-determinística
Uma consequência prática de tudo o que este capítulo defende: se a fronteira entre decisão e execução é código determinístico, ela pode — e deve — ser testada como qualquer outro código determinístico, sem precisar envolver o modelo em cada execução de teste. O pipeline de sete etapas descrito na seção anterior aceita como entrada exatamente uma estrutura de dados (a chamada proposta: nome da tool e argumentos), independentemente de essa estrutura ter sido produzida por um LLM em produção ou por um caso de teste escrito à mão.
// Suíte de contrato do pipeline — nenhuma chamada ao LLM aqui
casos_de_teste = [
{ nome: "argumento fora do enum é rejeitado",
chamada: { tool: "emitir_estorno",
args: { motivo: "cliente_bravo", ... } },
esperado: erro("argumentos_invalidos") },
{ nome: "valor acima do limiar exige aprovação humana",
chamada: { tool: "emitir_estorno",
args: { valor_centavos: 800000, ... } },
esperado: aprovacao_pendente() },
{ nome: "chamada repetida com mesma idempotency_key não duplica",
chamada: repetir_duas_vezes(chamada_valida),
esperado: efeito_aplicado_uma_unica_vez() },
{ nome: "agente sem permissão nem enxerga a tool",
agente: agente_sem_acesso_a_billing,
chamada: { tool: "emitir_estorno", args: validos },
esperado: erro("tool_desconhecida_ou_nao_permitida") },
]Essa suíte roda em segundos, sem custo de inferência, e cobre exatamente a superfície que este capítulo tratou como fronteira de governança: validação de schema, classificação de risco, permissão, idempotência e visibilidade. O que ela deliberadamente não testa — e não deveria tentar testar de forma determinística — é se o modelo vai escolher a tool certa e preencher os argumentos certos a partir de um contexto ambíguo; essa é a metade probabilística, avaliada por outros meios (métricas de acerto de tool-call, avaliação por amostragem, revisão humana). Separar as duas suítes de teste é o reflexo direto, na prática de engenharia, da mesma fronteira que todo o capítulo defende: o que é determinístico se testa como determinístico, o que é probabilístico se avalia como probabilístico — e confundir os dois é a forma mais comum de uma suíte de testes de agente virar frágil e cara de manter.