Tool use no loop: histórico, tool_result e paralelismo
Objetivos de aprendizagem
- Anexar corretamente o turno do assistente ao histórico:
response.contentinteiro, incluindo os blocostool_use. - Construir mensagens
tool_resultcom otool_use_idcorrespondente. - Tratar chamadas paralelas de ferramentas: todos os resultados em uma única mensagem
user. - Sinalizar falhas de execução com
is_error: truepara que o modelo possa se recuperar. - Implementar um loop manual completo em Python, do jeito que a prova espera.
Por que o histórico importa tanto
A Messages API é stateless: cada requisição envia a conversa inteira. No agentic loop, é o histórico acumulado que permite ao modelo incorporar os resultados das ferramentas ao raciocínio da próxima iteração. Entre uma iteração e outra, você anexa dois itens ao histórico:
- O turno do assistente:
{"role": "assistant", "content": response.content}— oresponse.contentcompleto, com blocos de texto, thinking etool_use. Se você anexar só o texto, os blocostool_usese perdem e a API rejeita ostool_resultseguintes, que ficariam órfãos (cadatool_resultprecisa referenciar umtool_useexistente no histórico). - Os resultados: uma mensagem
{"role": "user", "content": [ ...blocos tool_result... ]}.
Erro clássico: reconstruir o turno do assistente só com o texto ({"role": "assistant", "content": texto}). Sem os blocos tool_use no histórico, o tool_use_id do seu tool_result não referencia nada e a requisição falha com erro 400. Sempre anexe response.content inteiro.
Anatomia do tool_result
Cada bloco tool_result tem três campos que caem na prova:
tool_use_id— obrigatório; deve ser exatamente oiddo blocotool_useque o originou. É assim que o modelo casa pedido e resposta.content— o resultado (string ou lista de blocos, inclusive imagens).is_error— opcional;truequando a execução falhou. O modelo então tenta outra abordagem ou explica o problema, em vez de tratar a mensagem de erro como dado válido.
Chamadas paralelas de ferramentas
Por padrão, o Claude pode pedir várias ferramentas no mesmo turno (vários blocos tool_use em um único response.content) quando as ações são independentes — por exemplo, consultar o cliente e o pedido ao mesmo tempo. As regras:
- Execute todas as chamadas pedidas (idealmente em paralelo, já que são independentes).
- Devolva todos os
tool_resultna mesma e única mensagemuser— um bloco portool_use, cada um com seutool_use_id. Enviar cada resultado em uma mensagemuserseparada é anti-padrão e pode quebrar o pareamento pedido/resposta. - Para forçar no máximo uma ferramenta por turno, use
disable_parallel_tool_use: truedentro detool_choice.
sequenceDiagram
participant U as Aplicação
participant C as Claude
U->>C: messages.create(tools, histórico)
C-->>U: stop_reason tool_use
2 blocos tool_use (get_customer, lookup_order)
U->>U: executa as 2 ferramentas (em paralelo)
U->>C: 1 mensagem user com 2 tool_result
(cada um com seu tool_use_id)
C-->>U: stop_reason end_turn (resposta final)
Loop manual completo
Este é o esqueleto de referência que junta tudo: histórico completo, resultados paralelos em uma única mensagem user e is_error para falhas:
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_customer",
"description": "Retorna dados do cliente a partir do e-mail verificado. "
"Use antes de qualquer operacao de pedido ou reembolso.",
"input_schema": {
"type": "object",
"properties": {"email": {"type": "string"}},
"required": ["email"],
},
},
{
"name": "lookup_order",
"description": "Busca detalhes de um pedido pelo numero. "
"Use quando o cliente mencionar um pedido especifico.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
]
def execute_tool(name, tool_input):
"""Executa a ferramenta e retorna (conteudo, houve_erro)."""
try:
if name == "get_customer":
return '{"customer_id": "C-981", "verified": true}', False
if name == "lookup_order":
return '{"order_id": "4412", "status": "shipped"}', False
return "Ferramenta desconhecida: " + name, True
except Exception as exc:
return "Erro ao executar %s: %s" % (name, exc), True
messages = [{
"role": "user",
"content": "Sou ana@exemplo.com; o pedido 4412 chegou danificado.",
}]
while True:
response = client.messages.create(
model="claude-opus-5",
max_tokens=8000,
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
break # end_turn (ou outro stop_reason tratado fora do exemplo)
# 1) anexa o turno do assistente INTEIRO (texto + tool_use)
messages.append({"role": "assistant", "content": response.content})
# 2) executa todas as chamadas do turno e junta os resultados
tool_results = []
for block in response.content:
if block.type == "tool_use":
content, failed = execute_tool(block.name, block.input)
result = {
"type": "tool_result",
"tool_use_id": block.id, # casa com o pedido
"content": content,
}
if failed:
result["is_error"] = True
tool_results.append(result)
# 3) TODOS os resultados numa unica mensagem user
messages.append({"role": "user", "content": tool_results})
final_text = next((b.text for b in response.content if b.type == "text"), "")
print(final_text)
Na prática (fora da prova): o SDK Python oferece o Tool Runner (client.beta.messages.tool_runner, beta), que automatiza esse loop. A lição 4 compara as abordagens. Mas a prova cobra que você saiba montar o loop manual — em especial o pareamento tool_use_id ↔ tool_result e a mensagem única com resultados paralelos.
Pegadinhas da prova
- Distrator típico: anexar ao histórico apenas o texto do assistente. Sem os blocos
tool_use, ostool_resultficam órfãos → erro 400. - Distrator típico: enviar cada
tool_resultde chamadas paralelas em mensagensuserseparadas. O correto é uma única mensagemusercom todos os blocos. - Distrator típico: em caso de falha da ferramenta, lançar exceção e abortar o loop, ou devolver o erro como se fosse resultado válido. O correto é
tool_resultcomis_error: truee mensagem informativa — o modelo se recupera sozinho. - Distrator típico: inventar um
tool_use_idou reutilizar o mesmo id para vários resultados. Cada resultado referencia exatamente o id do seu pedido. - Distrator típico: "responda ao tool_use com uma mensagem
assistant".tool_resultsempre vai em mensagem comrole: "user".
Resumo em 5 linhas
- A API é stateless: o loop reenvia o histórico completo, e é ele que deixa o modelo raciocinar sobre os resultados das ferramentas.
- Anexe o turno do assistente com
response.contentinteiro — texto, thinking e blocostool_use. - Cada
tool_resultvai numa mensagemusere referencia o pedido viatool_use_id. - Chamadas paralelas: execute todas e devolva todos os resultados numa única mensagem
user. - Falha de execução →
is_error: truecom mensagem útil; o modelo tenta outra abordagem em vez de abortar.
Documentação oficial
Questões de fixação
1. Ao continuar o loop após um stop_reason == "tool_use", o que deve ser anexado ao histórico como turno do assistente?
Gabarito: B. O histórico precisa do turno completo para que cada tool_result encontre seu tool_use correspondente. A e C mutilam o turno (A quebra o pareamento de ids; C perde o raciocínio em texto). D deixa os tool_result sem pedido correspondente — a API rejeita.
2. O Claude pediu três ferramentas independentes no mesmo turno (três blocos tool_use). Como devolver os resultados?
Gabarito: C. Resultados de chamadas paralelas vão todos numa única mensagem user. A fragmenta os resultados em turnos artificiais. B usa o role errado — tool_result é sempre conteúdo de mensagem user. D deixa pedidos sem resposta, o que invalida a continuação da conversa.
3. A execução de lookup_order lançou timeout no backend. Qual é a melhor forma de comunicar isso ao modelo?
Gabarito: B. is_error: true sinaliza a falha de forma estruturada e o modelo pode tentar outra abordagem ou explicar ao usuário. A aborta uma conversa recuperável. C faz o modelo tratar o erro como dado válido. D deixa um tool_use sem resposta, quebrando o protocolo.
4. Qual configuração impede o Claude de pedir mais de uma ferramenta por turno?
Gabarito: A. disable_parallel_tool_use: true em tool_choice é o mecanismo determinístico. B trunca respostas em vez de limitar chamadas — gera max_tokens, não controle. C é enforcement por prompt, probabilístico. D limita quais ferramentas existem, não quantas chamadas por turno o modelo faz da mesma ferramenta.