Domínio 1 — Agentic Architecture & Orchestration · Lição 2 de 7

Tool use no loop: histórico, tool_result e paralelismo

Objetivos de aprendizagem

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:

  1. O turno do assistente: {"role": "assistant", "content": response.content} — o response.content completo, com blocos de texto, thinking e tool_use. Se você anexar só o texto, os blocos tool_use se perdem e a API rejeita os tool_result seguintes, que ficariam órfãos (cada tool_result precisa referenciar um tool_use existente no histórico).
  2. 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:

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:

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

Resumo em 5 linhas

  1. A API é stateless: o loop reenvia o histórico completo, e é ele que deixa o modelo raciocinar sobre os resultados das ferramentas.
  2. Anexe o turno do assistente com response.content inteiro — texto, thinking e blocos tool_use.
  3. Cada tool_result vai numa mensagem user e referencia o pedido via tool_use_id.
  4. Chamadas paralelas: execute todas e devolva todos os resultados numa única mensagem user.
  5. Falha de execução → is_error: true com 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?

2. O Claude pediu três ferramentas independentes no mesmo turno (três blocos tool_use). Como devolver os resultados?

3. A execução de lookup_order lançou timeout no backend. Qual é a melhor forma de comunicar isso ao modelo?

4. Qual configuração impede o Claude de pedir mais de uma ferramenta por turno?