Pular para o conteúdo

Certificação Claude Developer Foundations · parte 13 de 20

Implementação de ferramentas com Claude

Como descrever uma ferramenta, devolver erros sem derrubar o laço, validar argumentos e tratar vários pedidos de uma vez com tool use.

Publicada em · 29 min de leitura

Tamanho da fonte:
Largura da coluna:

Ferramenta (tool use, ou function calling) é o mecanismo que deixa o modelo pedir uma ação ao seu código. O modelo não consulta um banco, não envia e-mail e não lê arquivo: ele só escreve um pedido estruturado dizendo qual função quer chamar e com quais argumentos. Quem executa é a sua aplicação, e o resultado volta para o modelo numa mensagem própria, para que ele consiga terminar a resposta com um dado real em mãos. Esta aula cobre o lado de quem programa: como descrever uma ferramenta, como o pedido e a resposta trafegam pela API, como devolver erros sem derrubar o laço, como validar argumentos, como lidar com vários pedidos numa vez e como desenhar ferramentas que o modelo consiga usar bem.

Os exemplos de código usam Python e o SDK oficial da Anthropic. A documentação consultada está listada no fim. Para o conceito de agente, veja Agents. Para servidores que expõem ferramentas por um protocolo padrão, veja MCP. Para a montagem da mensagem que leva as ferramentas, veja Prompt e contexto.

Mapa da aula

SeçãoO que responde
1. O que é uma ferramentaPor que o modelo precisa de ferramentas e quem executa cada coisa
2. O esquema da ferramentaQuais campos a definição tem e por que a descrição é o que decide a escolha
3. O ciclo tool_use e tool_resultComo o pedido sai do modelo, volta com o resultado e se liga pelo id
4. Erros sem derrubar o laçoQuando usar is_error em vez de deixar uma exceção escapar
5. Validação antes de executarComo conferir os argumentos antes de tocar no mundo real
6. Chamadas em paraleloO que muda no código quando o modelo pede duas ou mais ferramentas numa vez
7. Ferramentas pequenas e com propósitoComo dividir e nomear as ferramentas para o modelo escolher bem
8. Um laço mínimo rodandoCódigo completo de um laço de ferramenta, com a saída real da execução
9. Erros comuns e boas práticasArmadilhas que aparecem em produção e o motivo de cada regra
10. Prática, glossário e fontesExercícios, termos definidos e documentação consultada

1. O que é uma ferramenta

Pense num atendente de balcão que sabe muita coisa, mas não tem acesso ao estoque. Quando um cliente pergunta se o tênis azul número 42 existe, ele não adivinha. Ele pede para alguém da loja olhar o sistema e, com a resposta na mão, volta a falar com o cliente. O modelo funciona desse jeito. Ele tem muito conhecimento guardado do treinamento, mas não sabe o estado atual do seu pedido, do seu estoque ou do seu banco de dados. Sem uma ferramenta, ele responde com o que parece plausível, e é aí que nascem as respostas inventadas.

Uma ferramenta é uma função do seu programa que você descreve ao modelo. Você escreve o nome, o que a função faz, quais argumentos ela recebe e envia essa descrição junto com a mensagem. O modelo lê a descrição, decide se a função ajuda a responder e, quando ajuda, devolve um pedido com os argumentos preenchidos. A função continua sendo sua. Ela roda no seu processo, com as suas credenciais, e pode ser tão simples quanto uma consulta a um dicionário ou tão complexa quanto uma chamada a um sistema de terceiros.

Vale separar os dois papéis desde já. O modelo decide o que pedir. A aplicação decide se pode executar, executa e devolve o resultado. Essa divisão é a base da segurança do sistema, porque a última palavra sobre uma ação com efeito no mundo fica com o seu código, e não com o texto que o modelo gerou. Na documentação, as ferramentas que você escreve são chamadas de client tools, porque rodam no seu lado. Existem também server tools, como a busca na web, que a própria Anthropic executa. Esta aula trata só das primeiras.

Os usos mais comuns seguem o mesmo padrão. Uma ferramenta consulta um pedido, outra cria um chamado num sistema de suporte, outra converte uma moeda usando a cotação do dia. Em todos os casos o ganho é o mesmo: a resposta do modelo passa a depender de um dado que existe no momento da pergunta, e não de algo que ele aprendeu meses antes.

2. O esquema da ferramenta

Uma ferramenta é declarada no parâmetro tools da requisição. Cada item tem três campos obrigatórios. O primeiro é name, o identificador que aparece depois no pedido do modelo e que o seu código usa para escolher a função a executar. A documentação exige que o nome combine com a expressão ^[a-zA-Z0-9_-]{1,128}$, então letras, números, sublinhado e hífen, sem espaço e sem acento. O segundo é description, um texto em linguagem natural que explica o que a ferramenta faz, quando deve ser usada e como se comporta. O terceiro é input_schema, um objeto no formato JSON Schema que descreve os argumentos esperados, seus tipos e quais são obrigatórios.

Há ainda campos opcionais. input_examples aceita exemplos de entradas válidas, úteis quando a ferramenta tem objetos aninhados ou formatos delicados, e cada exemplo precisa obedecer ao próprio schema. strict, quando ligado, faz a API garantir que os argumentos respeitem o schema, o que reduz pedidos malformados. Esse recurso merece estudo próprio, então aqui ele fica apenas citado.

Segue a definição da ferramenta usada no laço da seção 8:

FERRAMENTAS = [
    {
        "name": "buscar_pedido",
        "description": (
            "Consulta o status de um pedido de compra pelo número. Use quando o "
            "cliente perguntar onde está o pedido ou quando ele chega. Não altera, "
            "cancela nem reembolsa nada. Retorna o status atual e a previsão de "
            "entrega, quando existir."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "numero": {
                    "type": "string",
                    "description": "Número do pedido no formato A-1234.",
                }
            },
            "required": ["numero"],
            "additionalProperties": False,
        },
    }
]

Repare em três escolhas. O input_schema declara additionalProperties: False, o que sinaliza que nenhum campo fora de numero é esperado. O campo numero tem uma descrição com o formato exato, porque o modelo usa essa frase para preencher o argumento. E a descrição da ferramenta diz o que ela não faz. Isso parece detalhe, mas evita que o modelo tente cancelar um pedido usando uma função de consulta.

A pergunta que mais importa aqui é por que a descrição pesa tanto. A resposta é que o modelo não enxerga o seu código. Ele não sabe o que buscar_pedido faz por dentro, não sabe se consulta um cache ou um banco de dados, e não lê a implementação. Tudo o que ele tem para decidir é o nome, a descrição e o schema. Quando ele escolhe uma ferramenta errada ou ignora uma que deveria usar, a causa quase sempre está nesse texto, e não num bug de código. A documentação da Anthropic é direta sobre isso: a descrição é o fator de maior peso para o desempenho da ferramenta, e uma boa descrição costuma ter pelo menos três ou quatro frases, explicando o que a ferramenta faz, quando usá-la, o que cada parâmetro significa e quais são suas limitações.

A comparação entre uma descrição boa e uma ruim ajuda a fixar o ponto. Uma descrição como “Gets the stock price for a ticker.” deixa o modelo sem saber se o preço é atual, em que bolsa, em que moeda e o que fazer quando o ticker não existe. Uma descrição que diz que o dado é o último preço negociado em dólar, que vale para ações negociadas nas bolsas americanas, e que a ferramenta não traz outras informações sobre a empresa, dá ao modelo o que ele precisa para decidir bem. A diferença custa algumas frases a mais e evita dezenas de chamadas erradas.

A figura abaixo separa as duas leituras da mesma definição. O modelo lê name, description e input_schema para escolher e preencher o pedido. O seu código lê o campo name da resposta para despachar a função certa e valida os argumentos antes de executar. As setas laranja e verdes mostram o caminho feliz, e a seta tracejada vermelha mostra o que acontece com um nome desconhecido, que também precisa de resposta.

1 02 definicao e despacho
figura 1 · 1 02 definicao e despacho

Na figura, o losango é o ponto em que o seu código decide. Ele confere se o nome existe no seu despacho. Se existir, os argumentos seguem para validação e depois para a função real. Se não existir, a resposta de erro volta para o modelo, e o laço continua vivo. Esse ponto é o que a seção 4 aprofunda.

3. O ciclo tool_use e tool_result

O uso de uma ferramenta é uma conversa de idas e voltas em que cada passo tem um formato próprio. Ele começa com a sua requisição, que leva as mensagens e a lista tools. Se o modelo entender que precisa de uma ferramenta, a resposta vem com stop_reason igual a "tool_use", e o conteúdo traz um ou mais blocos do tipo tool_use. Cada bloco tem três campos que importam: id, um identificador gerado pela API no formato parecido com toolu_01A09q90qw90lq917835lq9, name, que diz qual ferramenta foi pedida, e input, um objeto com os argumentos. Antes desses blocos o modelo pode ter escrito um bloco de texto explicando o que vai fazer, e o seu código deve tratar esse texto como qualquer saída do modelo, sem depender de um formato fixo.

Ao receber o pedido, o seu código localiza a função pelo name, executa com o input e monta a resposta. A resposta é uma nova mensagem com role igual a "user", cujo conteúdo traz um bloco tool_result. Esse bloco tem dois campos centrais: tool_use_id, que precisa ser exatamente o id do pedido que está sendo respondido, e content, que pode ser uma string ou uma lista de blocos de texto, imagem ou documento. Com a mensagem enviada, o modelo continua de onde parou. Se ele ainda precisar de outra ferramenta, pede de novo. Se já tiver o que precisa, responde ao usuário com texto, e a resposta vem com um stop_reason diferente de "tool_use", como "end_turn".

O id é o que costura o ciclo. Quando um laço tem várias chamadas em andamento, ou quando uma conversa é longa, é o id que diz qual resultado corresponde a qual pedido. Um tool_result com id errado, ou sem id, não tem como ser associado ao pedido, e a API recusa a requisição. Na prática, o id é uma chave estrangeira que você não escolhe e nunca deve inventar. Guarde-o como veio.

Há duas regras de posição que a documentação deixa explícitas. A primeira é que o tool_result precisa vir logo depois do tool_use correspondente na história da conversa, sem nenhuma mensagem no meio. A segunda é que, dentro da mensagem do usuário que contém os resultados, os blocos tool_result precisam vir primeiro na lista de conteúdo, e qualquer texto deve vir depois deles. Um texto colocado antes dos resultados causa erro 400. Essas regras existem porque o modelo lê a conversa como uma sequência, e um resultado deslocado quebraria a correspondência entre pedido e resposta.

A figura abaixo mostra o ciclo com os números dos passos. O laço envia as mensagens e as ferramentas, recebe o pedido da API, executa a função no seu processo e envia o resultado de volta com o mesmo id. A seta de retorno ao modelo é a que mais vale observar, porque é ali que o id precisa bater.

1 01 ciclo tool use
figura 2 · 1 01 ciclo tool use

O laço completo tem uma forma previsível, que a seção 8 implementa. Ele repete o envio enquanto o stop_reason for "tool_use", acumula as mensagens do assistente e do usuário, e encerra quando o modelo devolve texto final. O limite de voltas é uma proteção que você mesmo escreve, porque nada na API impede um modelo de pedir a mesma ferramenta indefinidamente.

4. Erros sem derrubar o laço

Imagine que a consulta ao sistema de pedidos falhou com um erro de rede. A primeira reação de quem programa é deixar a exceção subir, como em qualquer função. Esse é o erro que mais derruba sistemas com ferramentas. Quando a exceção escapa do laço, o programa para antes de devolver o tool_result, a conversa fica com um tool_use sem resposta e o usuário recebe uma falha, e não uma explicação. O modelo nunca chega a saber que houve um problema, porque para ele nada aconteceu. É como um garçom que cai na cozinha e derruba a bandeja: a cozinha não sabe que o pedido se perdeu, e o cliente fica esperando.

O caminho certo é capturar a falha dentro da execução da ferramenta e devolver o erro como um tool_result com is_error igual a true. O conteúdo continua sendo uma string, mas agora ela carrega o motivo. Com isso o modelo lê a falha como parte da conversa e decide o que fazer: pode pedir de novo com outros argumentos, tentar outra ferramenta, ou explicar ao usuário que o sistema está indisponível. A documentação descreve esse comportamento e dá um exemplo típico, em que o modelo responde algo como dizer que não conseguiu consultar a previsão porque o serviço estava fora do ar.

A qualidade da mensagem de erro importa tanto quanto o uso da flag. Uma mensagem como “falhou” não dá ao modelo nada para agir. Uma mensagem como “limite de consultas excedido, tente novamente em 60 segundos” permite que ele espere, ou que avise o usuário com precisão. O mesmo vale para argumentos inválidos, em que dizer qual campo está errado e qual formato é esperado faz o modelo corrigir o pedido na volta seguinte. A documentação observa que, em pedidos inválidos, o modelo costuma tentar de novo duas ou três vezes com correções antes de desistir e pedir desculpas ao usuário, o que mostra que o erro devolvido funciona como instrução.

Existe uma distinção importante. O is_error é para falhas de execução da sua ferramenta e para argumentos que você recusou. Ele não substitui a validação de schema feita pela API, e ele não se aplica a server tools, como a busca na web, cujos erros a Anthropic trata do lado dela. Do seu lado, toda ferramenta client precisa responder com um tool_result para cada tool_use, mesmo quando decide não executar. Se o laço executa as chamadas em sequência e uma falha, as chamadas que sobraram ainda precisam de resposta, com is_error e uma explicação curta, como “não executada: a chamada anterior falhou”.

O código da seção 8 segue essa regra em dois níveis. Entradas erradas e consultas a pedidos inexistentes voltam como is_error, com uma mensagem que diz o que corrigir. Uma exceção inesperada também não derruba o laço: ela é registrada no erro padrão para você investigar e devolvida ao modelo como uma falha genérica. Esse segundo caso exige cuidado, porque capturar tudo pode esconder um bug de programação. Por isso ele fica registrado no erro padrão, com a exceção completa, e a mensagem devolvida ao modelo não expõe detalhes internos.

A figura abaixo coloca os dois caminhos lado a lado. Pelo caminho da exceção, o laço cai e nenhum resultado chega ao modelo. Pelo caminho do is_error, o mesmo erro vira um tool_result com o mesmo id, e a API responde ao usuário explicando o problema. Repare que a seta da exceção sai tracejada e termina numa caixa de laço caído, enquanto a seta do erro devolvido segue até o modelo.

1 03 erro no laco
figura 3 · 1 03 erro no laco

5. Validação antes de executar

O input_schema descreve o que o modelo deveria enviar, mas uma descrição não é uma garantia. Sem strict, o modelo segue o schema na maior parte das vezes e erra em outras: manda um número onde o campo é texto, omite um campo obrigatório, inventa um campo que não existe ou escreve o formato de um identificador de um jeito que o seu sistema não reconhece. Como o argumento é texto gerado por um modelo, ele deve ser tratado como qualquer entrada externa, com validação na fronteira.

A razão para validar vai além da qualidade. Os argumentos chegam ao seu código a partir de um texto que pode ter sido influenciado por conteúdo externo, como uma página web lida numa busca ou um e-mail resumido, e um documento malicioso pode induzir o modelo a pedir um valor que você nunca autorizaria. A documentação lembra que resultados de ferramentas carregam conteúdo fora do seu controle, e que esse conteúdo pode conter instruções disfarçadas. Validar o argumento antes de executar é a última linha de defesa que não depende do modelo se comportar. A parte de segurança operacional, com permissões e confirmações humanas, é tema de 7.2 Guardrails e implantação segura.

A validação deve conferir o tipo de cada campo, a presença dos obrigatórios, a ausência de campos extras e o formato de valores sensíveis. O código da seção 8 faz isso na função validar. Ela recusa qualquer objeto que não seja um dicionário, recusa campos fora de numero, e exige que numero seja uma string no padrão A-1234. Quando a entrada falha, a função levanta ValueError com uma mensagem que diz o que está errado, e essa mensagem vai para o modelo como is_error. A execução da função real nunca começa com um argumento que você não aceitaria.

Um cuidado adicional é que validar o formato não basta para validar a permissão. Um número de pedido pode estar no formato certo e pertencer a outro cliente. Essa checagem depende da identidade de quem está usando a aplicação, e deve acontecer dentro da função, com o usuário autenticado vindo do seu sistema e não do texto do modelo. Nunca confie num campo como usuario_id vindo do input, porque ele também é escrito pelo modelo.

A figura da seção 2 mostra essa validação em ação. Antes de chegar à função, o pedido passa pelo despacho e pela checagem contra o schema. Quando alguma etapa recusa a entrada, a saída é um tool_result de erro e não uma execução. A seta de erro é tracejada de propósito, porque nada é gravado nem consultado nesse caminho.

6. Chamadas em paralelo

Um modelo pode pedir mais de uma ferramenta numa mesma resposta. Quem pergunta “como estão os pedidos A-1001 e A-1002?” tem duas consultas independentes, e o modelo pode devolver dois blocos tool_use no mesmo conteúdo do assistente. Isso é o que a documentação chama de uso paralelo de ferramentas, e é um comportamento padrão. O que muda no seu código é a obrigação de tratar a resposta como uma lista de pedidos, e não como um pedido único, e de devolver todos os resultados juntos.

A ordem de execução é decisão sua. A API não determina se os pedidos rodam em paralelo, em série ou numa combinação, e a documentação sugere pensar no efeito de cada ferramenta. Consultas de leitura, que não alteram nada, podem rodar ao mesmo tempo para reduzir a espera. Ferramentas que escrevem, como uma que reembolsa um pedido, ou que dependem umas das outras, devem rodar em série, na ordem em que aparecem, porque a segunda pode depender do efeito da primeira. O exemplo reembolsar_pedido na figura é hipotético e não existe no código da seção 8.

A figura abaixo mostra o caso de três chamadas numa única resposta. Duas são leituras que rodam ao mesmo tempo, e a terceira, que escreve, roda sozinha. Os três resultados convergem para uma mensagem só, e é essa mensagem que volta à API.

1 04 chamadas paralelas
figura 4 · 1 04 chamadas paralelas

A regra de devolução é a mais importante. Todos os tool_result correspondentes aos blocos da resposta devem vir numa única mensagem do usuário, cada um com o seu tool_use_id, e sempre antes de qualquer texto. Se você mandar cada resultado numa mensagem separada, o modelo aprende, dentro da própria conversa, que não deve pedir várias ferramentas de uma vez, e o paralelismo deixa de acontecer. A documentação chama isso de problema de formatação, e ele é um dos mais comuns quando alguém migra um laço de ferramenta única para um de várias.

Se você decidir não executar uma das chamadas, ainda precisa devolver um tool_result para ela. O caso típico é uma execução em série em que uma falha interrompe o restante. Nesse caso, a chamada que sobrou recebe is_error igual a true e uma explicação curta, como “não executada: a chamada anterior falhou”. Deixar um tool_use sem resposta quebra a conversa da mesma forma que qualquer falha de correspondência.

Para forçar o modelo a pedir uma única ferramenta por resposta, o parâmetro vai dentro do objeto tool_choice, e não na raiz da requisição. A forma documentada é "tool_choice": {"type": "auto", "disable_parallel_tool_use": true}. Com type igual a "auto", o modelo pode responder em texto sem chamar ferramenta, mas no máximo uma chamada por resposta. Forçar uma ferramenta específica com tool_choice do tipo "tool" ou "any" nem sempre é aceito: a documentação diz que, em alguns modelos mais recentes, essas escolhas devolvem erro 400, e recomenda auto combinado com strict nesses casos. Verifique a tabela de modelos na documentação antes de depender de uma escolha forçada.

O código da seção 8 já está preparado para paralelismo, porque percorre todos os blocos tool_use da resposta, executa cada um e devolve todos os resultados numa mensagem só. A execução, porém, acontece em sequência, num laço simples. Para rodar leituras em paralelo de verdade, a versão de produção usaria algo como asyncio.gather em Python ou Promise.all em TypeScript, mantendo a mesma montagem da mensagem de resultados. Essa variação não foi executada nesta aula, e o cenário 3 da saída da seção 8 mostra só a ordem das mensagens.

7. Ferramentas pequenas e com propósito

Uma tentação comum é criar uma ferramenta para cada ação possível sobre um recurso. Para pull requests, isso daria criar_pr, revisar_pr, aprovar_pr e fazer_merge_pr, cada uma com seu schema e sua descrição. O resultado costuma ser ruim. O modelo passa a ter um catálogo longo, as descrições começam a se parecer umas com as outras, e a escolha entre ferramentas vizinhas vira uma fonte de erro. A documentação sugere o caminho oposto em alguns casos: agrupar operações relacionadas numa ferramenta só, com um parâmetro action que escolhe a operação. Menos ferramentas reduzem a ambiguidade de seleção e deixam a superfície mais fácil de navegar.

Isso não contradiz a ideia de ferramenta pequena, desde que o critério seja claro. Uma ferramenta pequena é aquela que faz uma coisa bem definida, cujo nome e descrição dizem exatamente o que ela faz, e cujo efeito é fácil de prever. Agrupar faz sentido quando as operações são sempre usadas juntas e têm o mesmo nível de risco. Separar faz sentido quando elas têm riscos diferentes, como uma ferramenta de leitura de pedidos e outra que altera um pedido. Misturar leitura e escrita numa mesma ferramenta obriga o modelo a decidir, a cada chamada, se está lendo ou modificando, e um erro aí tem custo real.

O nome também faz parte do desenho. Quando o sistema cresce e passa a ter ferramentas de vários serviços, vale prefixar o nome com o serviço, como github_listar_prs ou slack_enviar_mensagem. Isso torna a escolha inequívoca e ajuda bastante quando a busca de ferramentas entra em cena, algo que a documentação recomenda para catálogos grandes.

A resposta da ferramenta também é desenho. Devolva apenas o que o modelo precisa para o próximo passo, e prefira identificadores estáveis, como slugs ou UUIDs, em vez de referências internas que mudam. Um resultado enorme ocupa a janela de contexto, custa dinheiro e dilui a informação útil. Um resultado enxuto, com o status, a previsão e o número do pedido, é mais fácil de usar do que um dump completo do registro do banco.

Por fim, a descrição precisa dizer o que a ferramenta não faz. O modelo tende a usar uma ferramenta de consulta para qualquer pergunta que pareça relacionada, e a frase “não altera nem cancela nada” evita que ele tente usar a consulta para uma operação que ela não suporta.

8. Um laço mínimo rodando

O código abaixo implementa o ciclo completo com a ferramenta da seção 2. Ele tem quatro partes: a definição da ferramenta, a validação, a execução que transforma qualquer falha em tool_result, e o laço que repete o envio. O cliente que aparece no final é simulado, com respostas roteirizadas, para que o script rode sem chave de API e sem rede. Na aplicação real, a linha comentada no bloco principal, cliente = anthropic.Anthropic(), substitui o simulado, e o restante do código é o mesmo.

"""Laço mínimo de ferramenta. Roda com um cliente simulado, sem chamar a API."""
import json
import re
import sys
from types import SimpleNamespace

MODELO = "claude-haiku-5-5"  # confira o identificador na documentação antes de usar

PEDIDOS = {
    "A-1001": {"status": "enviado", "previsao": "2026-10-20"},
    "A-1002": {"status": "entregue", "previsao": None},
}

FERRAMENTAS = [
    {
        "name": "buscar_pedido",
        "description": (
            "Consulta o status de um pedido de compra pelo número. Use quando o "
            "cliente perguntar onde está o pedido ou quando ele chega. Não altera, "
            "cancela nem reembolsa nada. Retorna o status atual e a previsão de "
            "entrega, quando existir."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "numero": {
                    "type": "string",
                    "description": "Número do pedido no formato A-1234.",
                }
            },
            "required": ["numero"],
            "additionalProperties": False,
        },
    }
]


def buscar_pedido(numero):
    if numero not in PEDIDOS:
        raise LookupError(f"pedido {numero} não encontrado")
    return PEDIDOS[numero]


EXECUTORES = {"buscar_pedido": lambda args: buscar_pedido(**args)}


def validar(entrada):
    """Confere a entrada antes de executar. Devolve o dicionário limpo ou levanta ValueError."""
    if not isinstance(entrada, dict):
        raise ValueError("a entrada precisa ser um objeto JSON")
    extras = set(entrada) - {"numero"}
    if extras:
        raise ValueError(f"campos desconhecidos: {sorted(extras)}")
    numero = entrada.get("numero")
    if not isinstance(numero, str) or not re.fullmatch(r"A-\d{4}", numero):
        raise ValueError(f"numero inválido: {numero!r}. Use o formato A-1234")
    return {"numero": numero}


def executar(bloco):
    """Roda uma tool_use e devolve o tool_result correspondente. Nenhuma exceção sai daqui."""
    try:
        if bloco.name not in EXECUTORES:
            raise LookupError(f"ferramenta desconhecida: {bloco.name}")
        args = validar(bloco.input)
        resultado = EXECUTORES[bloco.name](args)
        return {
            "type": "tool_result",
            "tool_use_id": bloco.id,
            "content": json.dumps(resultado, ensure_ascii=False),
        }
    except (ValueError, LookupError) as erro:
        return {
            "type": "tool_result",
            "tool_use_id": bloco.id,
            "content": f"Erro: {erro}. Corrija a entrada e tente de novo.",
            "is_error": True,
        }
    except Exception as erro:  # falha inesperada: o laço segue e o modelo é avisado
        print(f"  falha interna em {bloco.name}: {erro!r}", file=sys.stderr)
        return {
            "type": "tool_result",
            "tool_use_id": bloco.id,
            "content": "Falha interna ao consultar o sistema de pedidos. Tente de novo mais tarde.",
            "is_error": True,
        }


def rodar(cliente, pergunta, limite=5):
    mensagens = [{"role": "user", "content": pergunta}]
    for volta in range(1, limite + 1):
        resposta = cliente.messages.create(
            model=MODELO, max_tokens=1024, tools=FERRAMENTAS, messages=mensagens
        )
        mensagens.append({"role": "assistant", "content": resposta.content})
        if resposta.stop_reason != "tool_use":
            return "".join(b.text for b in resposta.content if b.type == "text")
        pedidos = [b for b in resposta.content if b.type == "tool_use"]
        for b in pedidos:
            print(f"  volta {volta}: modelo pediu {b.name} com {b.input}")
        # Todos os tool_result de uma vez, numa só mensagem do usuário.
        mensagens.append({"role": "user", "content": [executar(b) for b in pedidos]})
    raise RuntimeError(f"o laço passou de {limite} voltas sem resposta final")


class ClienteSimulado:
    """Imita client.messages.create com respostas roteirizadas. Não toca na rede."""

    def __init__(self, roteiro):
        self._roteiro = list(roteiro)
        self.ultimo = None
        self.messages = self

    def create(self, **kwargs):
        self.ultimo = kwargs["messages"]
        return self._roteiro.pop(0)


def texto(t):
    return SimpleNamespace(type="text", text=t)


def pedido(id_, numero):
    return SimpleNamespace(type="tool_use", id=id_, name="buscar_pedido", input={"numero": numero})


def resposta(stop, *blocos):
    return SimpleNamespace(stop_reason=stop, content=list(blocos))


def resumo(mensagens):
    for m in mensagens:
        c = m["content"]
        if isinstance(c, str):
            print(f"  {m['role']}: texto")
            continue
        partes = []
        for b in c:
            if isinstance(b, dict):
                if b["type"] == "tool_result":
                    marca = ", is_error" if b.get("is_error") else ""
                    partes.append(f"tool_result({b['tool_use_id']}{marca}) -> {b['content'][:60]}")
            else:
                partes.append(f"{b.type}({b.id})" if b.type == "tool_use" else b.type)
        print(f"  {m['role']}: " + " | ".join(partes))


if __name__ == "__main__":
    # Na aplicação real: cliente = anthropic.Anthropic() (lê ANTHROPIC_API_KEY).
    print("== cenário 1: uma ferramenta, uma volta")
    c1 = ClienteSimulado([
        resposta("tool_use", texto("Vou consultar o pedido."), pedido("toolu_01", "A-1001")),
        resposta("end_turn", texto("O pedido A-1001 foi enviado e chega até 20/10/2026.")),
    ])
    print("resposta final:", rodar(c1, "Onde está o pedido A-1001?"))
    resumo(c1.ultimo)

    print("\n== cenário 2: entrada inválida, erro devolvido ao modelo e nova tentativa")
    c2 = ClienteSimulado([
        resposta("tool_use", pedido("toolu_02", "1002")),
        resposta("tool_use", pedido("toolu_03", "A-1002")),
        resposta("end_turn", texto("O pedido A-1002 já foi entregue.")),
    ])
    print("resposta final:", rodar(c2, "O pedido 1002 chegou?"))
    resumo(c2.ultimo)

    print("\n== cenário 3: duas chamadas em paralelo na mesma resposta")
    c3 = ClienteSimulado([
        resposta("tool_use", pedido("toolu_04a", "A-1001"), pedido("toolu_04b", "A-1002")),
        resposta("end_turn", texto("O A-1001 foi enviado e o A-1002 já foi entregue.")),
    ])
    print("resposta final:", rodar(c3, "Como estão os pedidos A-1001 e A-1002?"))
    resumo(c3.ultimo)

Para rodar, basta python3 laco.py. Não há dependências além da biblioteca padrão, porque o cliente é simulado. A saída real dessa execução, sem edição, é a seguinte:

== cenário 1: uma ferramenta, uma volta
  volta 1: modelo pediu buscar_pedido com {'numero': 'A-1001'}
resposta final: O pedido A-1001 foi enviado e chega até 20/10/2026.
  user: texto
  assistant: text | tool_use(toolu_01)
  user: tool_result(toolu_01) -> {"status": "enviado", "previsao": "2026-10-20"}
  assistant: text

== cenário 2: entrada inválida, erro devolvido ao modelo e nova tentativa
  volta 1: modelo pediu buscar_pedido com {'numero': '1002'}
  volta 2: modelo pediu buscar_pedido com {'numero': 'A-1002'}
resposta final: O pedido A-1002 já foi entregue.
  user: texto
  assistant: tool_use(toolu_02)
  user: tool_result(toolu_02, is_error) -> Erro: numero inválido: '1002'. Use o formato A-1234. Corrija
  assistant: tool_use(toolu_03)
  user: tool_result(toolu_03) -> {"status": "entregue", "previsao": null}
  assistant: text

== cenário 3: duas chamadas em paralelo na mesma resposta
  volta 1: modelo pediu buscar_pedido com {'numero': 'A-1001'}
  volta 1: modelo pediu buscar_pedido com {'numero': 'A-1002'}
resposta final: O A-1001 foi enviado e o A-1002 já foi entregue.
  user: texto
  assistant: tool_use(toolu_04a) | tool_use(toolu_04b)
  user: tool_result(toolu_04a) -> {"status": "enviado", "previsao": "2026-10-20"} | tool_result(toolu_04b) -> {"status": "entregue", "previsao": null}
  assistant: text

A saída mostra três coisas que a aula defendeu. No cenário 1, o tool_result tem o mesmo id do tool_use que o gerou, e a resposta final só aparece depois. No cenário 2, o número 1002 é recusado pela validação antes de qualquer consulta, a recusa volta como is_error, e o modelo corrige o pedido na volta seguinte. No cenário 3, os dois tool_result aparecem numa única mensagem do usuário, na mesma ordem dos tool_use.

Vale deixar claro o que essa execução não prova. O modelo do roteiro não decidiu nada: cada resposta foi escrita por mim e entregue na ordem certa, então a saída mostra que o laço reage corretamente às respostas, e não que um modelo real faria as mesmas escolhas. Também não houve chamada à API real, então o identificador do modelo, a aceitação dos campos pelo servidor e o comportamento de correção do modelo continuam por verificar. Os nomes de campos foram conferidos contra os tipos do SDK Python instalado (versão 1.15.0) e contra a documentação, e o código depende apenas do método messages.create e dos atributos stop_reason, content, type, id, name e input, que existem nos objetos reais do SDK. Na aplicação real, convém começar com um cenário de uma só ferramenta e registrar cada requisição antes de ampliar.

9. Erros comuns e boas práticas

O erro que mais derruba laços de ferramenta é deixar uma exceção escapar da execução. Já vimos o motivo na seção 4: o modelo nunca fica sabendo da falha, e a conversa fica com um pedido sem resposta. A regra prática é que toda execução de ferramenta termine com um tool_result, mesmo que ele diga que algo deu errado. Quem programa o laço deve tratar a exceção como dado, e não como interrupção.

O segundo erro é esquecer um tool_use sem resposta, especialmente quando a resposta do modelo tem vários blocos. Um laço que trata só o primeiro bloco quebra a conversa na segunda volta. O código da seção 8 evita isso ao percorrer a lista inteira, e a regra vale também para chamadas que você decidiu não executar.

O terceiro erro é colocar texto antes dos tool_result na mensagem do usuário. A documentação mostra que isso gera erro 400, e a origem costuma ser um sistema que quer acrescentar um lembrete junto dos resultados. O lembrete pode ir depois, nunca antes.

O quarto erro é tratar a descrição como documentação para humanos. Ela é a única coisa que o modelo usa para escolher a ferramenta. Uma descrição de uma linha costuma ser a causa de chamadas erradas que parecem bugs de código. Vale revisar a descrição sempre que o modelo errar a escolha, antes de mexer no código.

O quinto erro é validar só o formato e esquecer a permissão. Um argumento bem formado pode pedir um recurso que o usuário não deveria ver. A checagem de quem pode o quê deve acontecer no seu código, com a identidade vinda da sessão, e nunca a partir de um campo do input.

O sexto erro é o laço sem limite de voltas. Um modelo confuso, ou um resultado que sempre pede mais uma consulta, pode girar indefinidamente e consumir tokens e tempo. O limite da seção 8 é simples, mas em produção vale somar orçamento de tokens e um tempo máximo. Quando o limite estoura, a resposta ao usuário deve dizer que a operação não foi concluída, em vez de simplesmente falhar.

O sétimo erro é devolver resultados grandes demais. Um dump completo de um registro ocupa contexto, encarece a chamada e pode esconder a informação útil. A prática recomendada é filtrar no servidor da ferramenta e devolver só o necessário para o próximo passo.

Por fim, resultados de ferramentas são conteúdo não confiável. Uma página web, um e-mail ou um documento enviado pelo usuário podem conter instruções escritas para enganar o modelo. A documentação recomenda manter esse conteúdo dentro de blocos tool_result, e não em instruções de sistema ou em blocos de texto comuns, e tratar qualquer argumento sugerido por ele como entrada a ser validada. A defesa completa envolve outras camadas que 7.2 Guardrails e implantação segura detalha.

10. Prática, glossário e fontes

Os exercícios abaixo usam o código da seção 8. Faça-os na ordem, porque cada um pressupõe o anterior.

O primeiro exercício é acrescentar uma segunda ferramenta, cancelar_pedido, que aceite o mesmo número e devolva uma confirmação. Escreva uma descrição que diga claramente quando ela deve ser usada e quando não deve. Depois, pense em qual das duas ferramentas deveria ter validação adicional de permissão e explique por quê.

O segundo exercício é trocar o laço sequencial por uma versão que execute as chamadas de leitura em paralelo com concurrent.futures ou asyncio, mantendo a montagem da mensagem de resultados igual. Verifique que a ordem dos tool_result continua igual à ordem dos tool_use, mesmo que as execuções terminem em ordens diferentes.

O terceiro exercício é forçar uma falha de rede dentro de buscar_pedido e observar o que aparece no resumo. Depois, troque o except genérico por um que deixe a exceção subir, e descreva, com suas palavras, o que o modelo passaria a ver na conversa.

Glossário.

tool use: mecanismo em que o modelo pede a execução de uma função definida pela aplicação, em vez de responder só com texto.

client tool: ferramenta cuja execução acontece no seu código. As server tools, como a busca na web, são executadas pela Anthropic.

name: identificador da ferramenta, com letras, números, sublinhado e hífen, usado no pedido do modelo para escolher a função.

description: texto que explica o que a ferramenta faz, quando usar e quais são suas limitações. É o principal fator de escolha do modelo.

input_schema: objeto JSON Schema que descreve os argumentos esperados, seus tipos e os campos obrigatórios.

tool_use: bloco da resposta do modelo com id, name e input, que representa um pedido de execução.

tool_result: bloco da mensagem do usuário que devolve o resultado de uma execução, ligado ao pedido pelo tool_use_id.

is_error: campo booleano do tool_result que sinaliza ao modelo que a execução falhou, para que ele leia o motivo e reaja.

stop_reason: campo da resposta que indica por que o modelo parou de gerar. O valor "tool_use" significa que ele está esperando um resultado de ferramenta.

strict: opção da definição da ferramenta que faz a API garantir que os argumentos sigam o schema. Será tratada com mais profundidade em aula própria.

Relacionados

Fontes

Consultadas em 18 de outubro de 2026, na documentação oficial da Anthropic:

Os nomes de campos do código foram conferidos também contra os tipos do SDK Python anthropic versão 1.15.0, instalado em 18 de outubro de 2026. A documentação mostra exemplos com outros identificadores de modelo, e o identificador usado no código deve ser confirmado antes do uso real.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?