Construindo um Loop de Uso de Ferramentas Multi-Turn
Repita trocas de tool_use e tool_result até que Claude retorne um end_turn.
Resumo
A API de Mensagens é sem estado - cada chamada é uma nova requisição sem memória de turnos anteriores, exceto o que você envia em messages. Quando Claude decide que uma ferramenta é necessária, ele não executa a ferramenta. Ele para de gerar, retorna um bloco tool_use descrevendo o que ele quer chamar e devolve o controle para o seu código.
Seu trabalho é executar a ferramenta, empacotar o resultado como um bloco tool_result e enviar a conversa inteira de volta. Claude então pede outra chamada de ferramenta ou produz sua resposta final. Muitas tarefas reais (pesquisar algo, agir com base no que foi encontrado, verificar o resultado) levam várias idas e vindas antes que Claude termine.
Essa troca é o "loop agêntico": chame a API, inspecione stop_reason, execute ferramentas se solicitado, anexe resultados, chame novamente. Ele termina quando response.stop_reason == "end_turn". Se você errar o loop - perder conteúdo, esquecer um stop_reason ou pular a guarda de iteração - você terá um crash na próxima chamada ou um agente que gira infinitamente.
Esta página constrói esse loop manualmente em Python com o SDK oficial anthropic, e depois mostra o atalho de nível superior Tool Runner assim que a versão manual for compreendida.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
import anthropic
client = anthropic.Anthropic()
messages = [{"role": "user", "content": user_input}]
while True:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
if response.stop_reason == "pause_turn":
messages.append({"role": "assistant", "content": response.content})
continue
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in tool_use_blocks:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
final_text = "".join(b.text for b in response.content if b.type == "text")Quando usar isso:
- Claude precisa chamar uma ou mais ferramentas antes de poder dar uma resposta final (pesquisas, cálculos, efeitos colaterais).
- A tarefa pode exigir várias etapas dependentes - por exemplo, pesquisar, buscar um registro específico e depois resumir.
- Você está construindo um agente que roda sem supervisão e deve continuar funcionando até que Claude realmente termine, não apenas após uma chamada de ferramenta.
- Você deseja controle total sobre como as ferramentas são executadas (concorrência, retentativas, sandboxing) em vez de delegar isso a um framework.
Exemplo de Trabalho
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Denver'"},
},
"required": ["city"],
},
},
{
"name": "convert_temperature",
"description": "Convert a temperature between Celsius and Fahrenheit.",
"input_schema": {
"type": "object",
"properties": {
"value": {"type": "number"},
"from_unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["value", "from_unit"],
},
},
]
def get_weather(city: str) -> str:
# Stand-in for a real weather API call.
fake_data = {"Denver": 8.0, "Miami": 29.0}
celsius = fake_data.get(city, 20.0)
return json.dumps({"city": city, "celsius": celsius})
def convert_temperature(value: float, from_unit: str) -> str:
if from_unit == "celsius":
result = value * 9 / 5 + 32
return json.dumps({"fahrenheit": round(result, 1)})
result = (value - 32) * 5 / 9
return json.dumps({"celsius": round(result, 1)})
def execute_tool(name: str, tool_input: dict) -> str:
if name == "get_weather":
return get_weather(tool_input["city"])
if name == "convert_temperature":
return convert_temperature(tool_input["value"], tool_input["from_unit"])
return json.dumps({"error": f"unknown tool: {name}"})
def run_agent(user_input: str, max_iterations: int = 12) -> str:
messages = [{"role": "user", "content": user_input}]
response = None
for _ in range(max_iterations):
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
if response.stop_reason == "pause_turn":
messages.append({"role": "assistant", "content": response.content})
continue
if response.stop_reason == "max_tokens":
raise RuntimeError("Response truncated at max_tokens; increase the limit.")
if response.stop_reason == "refusal":
raise RuntimeError(f"Claude declined the request: {response.stop_reason}")
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
if not tool_use_blocks:
# No tool calls and no end_turn - nothing left to do safely.
break
tool_results = []
for block in tool_use_blocks:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
else:
raise RuntimeError("Exceeded max_iterations without reaching end_turn.")
return "".join(b.text for b in response.content if b.type == "text")
if __name__ == "__main__":
answer = run_agent(
"What's the weather in Denver right now, in Fahrenheit?"
)
print(answer)O que isso demonstra:
- Um loop limitado (
for _ in range(max_iterations)) que sempre termina, com umRuntimeErrorlevantado pela cláusulafor...elsese Claude nunca atingirend_turn. - O
response.contentcompleto - não apenas texto extraído - anexado como o turno do assistente para que os blocostool_useque Claude emitiu sejam visíveis na próxima chamada. - Um
tool_use_idcorrespondente para cada blocotool_use, agrupado em uma única mensagemuserpor turno (necessário quando Claude solicita chamadas de ferramentas paralelas). - Tratamento explícito para
pause_turn,max_tokenserefusal, juntamente com os dois caminhos "continuar loop" (tool_useeend_turn). - Um único despachante
execute_toolque mantém o roteamento de ferramentas em um só lugar, em vez de cadeiasif/elifespalhadas perto da chamada da API.
Mergulho Profundo
Como Funciona
- Cada chamada
client.messages.create()é independente. A API não tem estado de sessão - tudo o que Claude "lembra" é o que você incluiu emmessagesnaquela chamada. - Quando Claude quer uma ferramenta, ele para de gerar saída e retorna blocos de conteúdo - algum
text(raro no meio do loop, mas possível), um ou mais blocostool_use, e umstop_reason. - Seu código executa a(s) ferramenta(s) solicitada(s) localmente (ou via um conector) e relata de volta com blocos
tool_result, correspondendo à chamada original viatool_use_id. - A próxima chamada
messages.create()reenvia toda a conversa crescente - turno original do usuário, turno do assistente de Claude com blocostool_use, seu turnousercom blocostool_result- para que Claude tenha o contexto completo para continuar ou concluir. - O loop não é um "chame a ferramenta, obtenha a resposta" de tiro único; Claude pode encadear várias chamadas de ferramentas (pesquisar algo, agir sobre isso, verificar) antes de ter informações suficientes para responder, por isso o looping - não uma única requisição/resposta - é o modelo mental correto.
Valores de stop_reason e O Que Fazer
stop_reason | Significado | Ação do Loop |
|---|---|---|
end_turn | Claude terminou; nenhuma outra chamada de ferramenta pendente | Saia do loop, retorne o texto |
tool_use (implícito sempre que blocos tool_use estão presentes e nenhuma outra razão terminal se aplica) | Claude quer que uma ou mais ferramentas sejam executadas | Execute cada ferramenta, anexe blocos tool_result, continue |
pause_turn | Uma ferramenta do lado do servidor (ex: pesquisa na web, execução de código) atingiu seu próprio limite de iteração interno no meio do turno | Anexe response.content como está e reenvie; a API retoma automaticamente - não adicione uma mensagem extra de usuário "Continuar" |
max_tokens | A saída foi truncada no limite de tokens | Levante/registre um erro; considere aumentar max_tokens ou fazer streaming em vez de continuar silenciosamente |
refusal | Claude recusou continuar por motivos de segurança | Inspecione stop_details, apresente ao chamador; não retente cegamente o mesmo prompt |
Formato da Mensagem em Cada Etapa
# After turn 1 (Claude requests a tool call):
messages == [
{"role": "user", "content": "What's the weather in Denver?"},
{"role": "assistant", "content": [ToolUseBlock(id="toolu_01...", name="get_weather", input={"city": "Denver"})]},
]
# After you execute the tool and append the result:
messages == [
{"role": "user", "content": "What's the weather in Denver?"},
{"role": "assistant", "content": [ToolUseBlock(...)]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01...", "content": "..."}]},
]Notas Python
# Prefer a dispatch dict over long if/elif chains as the tool count grows:
TOOL_HANDLERS = {
"get_weather": lambda i: get_weather(i["city"]),
"convert_temperature": lambda i: convert_temperature(i["value"], i["from_unit"]),
}
def execute_tool(name: str, tool_input: dict) -> str:
handler = TOOL_HANDLERS.get(name)
if handler is None:
return json.dumps({"error": f"unknown tool: {name}"})
try:
return handler(tool_input)
except Exception as exc:
# Report failures back to Claude as data, not a crashed process.
return json.dumps({"error": str(exc)})Parâmetros e Valores de Retorno
| Campo | Tipo | Descrição |
|---|---|---|
response.stop_reason | str | Por que a geração parou: end_turn, tool_use-implicando (blocos presentes), pause_turn, max_tokens, refusal, etc. |
response.content | list[ContentBlock] | Blocos ordenados que Claude retornou neste turno - text e/ou tool_use. Anexe verbatim como a mensagem do assistente. |
block.id | str | ID único em um bloco tool_use; ecoe-o de volta como tool_use_id no tool_result correspondente. |
block.input | dict | Argumentos analisados para a chamada da ferramenta, validados contra o input_schema da ferramenta. |
tool_result["content"] | str | list | A saída da ferramenta, como uma string ou lista de blocos de conteúdo; mantenha-a pequena e estruturada (ex: JSON). |
Armadilhas
- Anexar apenas o texto extraído, não
response.content. Se você enviar{"role": "assistant", "content": final_text}em vez dos blocos de conteúdo brutos, a próxima solicitação de Claude perderá os blocostool_useque sua mensagemtool_resultdeveria responder, e a API rejeitará a mensagemtool_resultde acompanhamento porque ela não corresponde a uma chamada de ferramenta anterior. Correção: sempre anexeresponse.contentinalterado como o turno do assistente. - Sem limite de iteração. Um loop que só sai em
end_turngirará para sempre se uma ferramenta continuar retornando dados ambíguos ou se o modelo ficar preso solicitando a mesma chamada repetidamente. Correção: envolva o loop emfor _ in range(max_iterations)(10-15 é um padrão razoável) e levante/registre um timeout quando ele estiver esgotado. - Tratar
pause_turncomo um turno normal detool_use. Enviar uma nova mensagem de usuário "Continuar" apóspause_turn(em vez de reenviar o turno pausado como está) confunde o estado da ferramenta do lado do servidor retomada. Correção: anexeresponse.contente chame a API novamente imediatamente, sem mensagem de usuário adicional. tool_use_idincompatível ou ausente. Cadatool_resultdeve referenciar oiddo blocotool_useao qual responde; pular um bloco (por exemplo, quando Claude solicita duas chamadas paralelas, mas você lida apenas com uma) deixa a API esperando por uma chamada de ferramenta não resolvida. Correção: itere sobre todos os blocostool_usena resposta e produza exatamente umtool_resultpor bloco, agrupado em uma única mensagemuser.- Deixar uma exceção de ferramenta travar o loop. Uma exceção não capturada dentro de
execute_toolmata toda a requisição em vez de dar a Claude a chance de se recuperar (por exemplo, retentar com argumentos diferentes). Correção: capture exceções por ferramenta e retorne-as como conteúdotool_result(por exemplo,{"error": "..."}) para que Claude possa reagir. - Ignorar
max_tokenscomo um motivo de parada. Tratarmax_tokenscomoend_turnenvia silenciosamente uma resposta truncada ao usuário. Correção: verifique explicitamente e levantemax_tokensna próxima chamada ou mude para streaming. - Retentar cegamente em
refusal. Repetir o mesmo prompt após uma recusa de segurança desperdiça chamadas e não mudará o resultado. Correção: inspecionestop_details, ajuste a solicitação ou escale para um humano em vez de retentar verbatim.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
client.beta.messages.tool_runner(...) com funções decoradas com @beta_tool, então runner.until_done() | Você quer que o ciclo de chamada -> execução -> feedback -> repetição seja automatizado e não precisa de controle granular sobre a execução (concorrência, sandboxing, lógica de retentativa personalizada) | Você precisa interceptar cada turno - por exemplo, transmitir resultados parciais para uma interface de usuário, registrar chamadas de ferramentas personalizadas ou formatação de resultados não padrão |
Requisição de tiro único com tool_choice forçando exatamente uma chamada de ferramenta, sem loop | Você sabe de antemão que a resposta precisa de exatamente uma chamada de ferramenta e nada mais | A tarefa pode precisar de chamadas de acompanhamento com base no que a primeira ferramenta retorna |
| Um framework/orquestrador (ex: LangChain, um runtime de agente personalizado) que envolve seu próprio loop | Você já padronizou esse framework em toda a base de código e quer consistência em vez de minimalismo | Você quer entender e controlar exatamente o que é enviado para a API em cada turno, ou você quer zero dependências extras |
FAQs
Por que não posso simplesmente fazer uma chamada de API e esperar a resposta final?
Porque a API é sem estado e Claude não pode executar ferramentas sozinho. Se Claude precisar de dados externos, ele retorna um bloco tool_use e para - seu código deve executar a ferramenta, enviar de volta um tool_result e chamar a API novamente para Claude continuar.
Como sei quando o loop está finished?
Verifique response.stop_reason == "end_turn". Esse é o único sinal confiável de que Claude não tem mais chamadas de ferramentas pendentes e produziu sua resposta final.
O que acontece se eu anexar apenas o texto que Claude gerou em vez dos blocos de conteúdo completos?
A próxima solicitação estará faltando os blocos tool_use que sua mensagem tool_result deveria responder, e a API rejeitará a mensagem. Sempre anexe response.content como está.
Preciso de uma mensagem `tool_result` separada para cada chamada de ferramenta?
Não - agrupe todos os blocos tool_result para um determinado turno em uma única mensagem user, um bloco por tool_use_id dos blocos tool_use desse turno.
Qual a diferença entre `tool_use` e `pause_turn`?
tool_use: Claude está pedindo ao seu código para executar uma ferramenta; você a executa e envia de volta umtool_result.pause_turn: uma ferramenta do lado do servidor (como pesquisa na web) atingiu seu próprio limite de iteração; você apenas reenvia a conversa inalterada e a API retoma essa ferramenta por conta própria.
Por que preciso de uma guarda `max_iterations` se o loop já sai em `end_turn`?
Porque end_turn não é garantido de chegar - uma ferramenta mal comportada, dados ambíguos ou um modelo preso solicitando a mesma chamada repetidamente podem manter o loop rodando indefinidamente. A guarda transforma um travamento infinito em uma falha limitada que você pode detectar e relatar.
O que `execute_tool` deve retornar quando a própria ferramenta falha?
Uma string (geralmente JSON) descrevendo o erro, por exemplo, json.dumps({"error": str(exc)}), enviada de volta como conteúdo normal de tool_result. Capture a exceção você mesmo; não a deixe travar o loop, e não fique em silêncio - Claude precisa ver que a chamada falhou para reagir apropriadamente.
Devo tentar novamente automaticamente quando `stop_reason` for `refusal`?
Não. Um refusal significa que Claude recusou por motivos de segurança - retentar o prompt idêntico não mudará isso. Inspecione stop_details, ajuste a solicitação ou direcione para um revisor humano em vez de retentar verbatim.
O que `stop_reason == "max_tokens"` significa para o meu loop?
A resposta foi cortada porque atingiu o limite de max_tokens - não é uma resposta completa nem uma solicitação de ferramenta. Trate isso como uma condição de erro: levante max_tokens, mude para streaming ou lide explicitamente com isso em vez de tratar como end_turn.
Claude pode solicitar mais de uma chamada de ferramenta em um único turno?
Sim. response.content pode conter múltiplos blocos tool_use em um turno (chamadas de ferramentas paralelas). Itere sobre todos eles e retorne um tool_result por bloco na mesma mensagem user de acompanhamento.
Quando devo usar o Tool Runner em vez de um loop manual?
Assim que você entender o loop manual e não precisar de controle de baixo nível sobre a execução - o Tool Runner (client.beta.messages.tool_runner(...) com funções decoradas com @beta_tool e runner.until_done()) automatiza o mesmo ciclo de chamada -> execução -> feedback -> repetição com menos código repetitivo.
Qual modelo o código de exemplo deve ter como alvo?
claude-sonnet-5 - o modelo padrão atual na linha Claude (juntamente com Claude Fable 5, Claude Opus 4.8 e Claude Haiku 4.5) - é um padrão sólido para loops de uso de ferramentas que equilibra capacidade e custo.
Relacionados
- Lidando com Requisições
tool_usee Retornando Blocostool_result- a mecânica de turno único que este loop repete em cada iteração. - Executando Chamadas de Ferramentas Paralelas em um Único Turno - como lidar com múltiplos blocos
tool_useretornados em um único turno. - Noções Básicas de Uso de Ferramentas - os conceitos fundamentais (ferramentas,
tool_choice, esquemas) nos quais esta página se baseia. - Melhores Práticas - orientação mais ampla para uso de ferramentas confiável e pronto para produção.
Versões da Pilha: Escrito contra a linha de modelos Claude atualizada em ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão) e Claude Haiku 4.5 - e o SDK oficial
anthropicpara Python (última versão 0.x). Nomes de modelos, versões de SDK e preços mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs antes de confiar neles.