Construcción de un Bucle de Uso de Herramientas de Múltiples Turnos
Repite los intercambios de tool_use y tool_result hasta que Claude devuelva un end_turn.
Resumen
La API de Mensajes no tiene estado: cada llamada es una solicitud nueva sin memoria de turnos anteriores, excepto lo que envías en messages. Cuando Claude decide que se necesita una herramienta, no ejecuta la herramienta él mismo. Deja de generar, devuelve un bloque tool_use que describe lo que quiere llamar y devuelve el control a tu código.
Tu trabajo es ejecutar la herramienta, empaquetar el resultado como un bloque tool_result y enviar toda la conversación de vuelta. Claude luego pide otra llamada a la herramienta o produce su respuesta final. Muchas tareas reales (buscar algo, luego actuar sobre lo encontrado, luego verificar el resultado) requieren varios de estos viajes de ida y vuelta antes de que Claude termine.
Esta ida y vuelta es el "bucle agéntico": llama a la API, inspecciona stop_reason, ejecuta herramientas si se le solicita, anexa resultados, llama de nuevo. Termina cuando response.stop_reason == "end_turn". Si te equivocas en el bucle (pierdes contenido, olvidas una stop_reason o omites el guardia de iteración), obtendrás un error en la siguiente llamada o un agente que se ejecutará indefinidamente.
Esta página construye ese bucle manualmente en Python con el SDK oficial de anthropic, y luego muestra el atajo de nivel superior Tool Runner una vez que se entiende la versión manual.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
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")Cuándo usar esto:
- Claude necesita llamar a una o más herramientas antes de poder dar una respuesta final (búsquedas, cálculos, efectos secundarios).
- La tarea puede requerir varios pasos dependientes: por ejemplo, buscar, luego obtener un registro específico, luego resumir.
- Estás construyendo un agente que se ejecuta sin supervisión y debe continuar hasta que Claude haya terminado realmente, no solo después de una llamada a la herramienta.
- Quieres control total sobre cómo se ejecutan las herramientas (concurrencia, reintentos, sandboxing) en lugar de delegar eso a un framework.
Ejemplo de Trabajo
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Obtener el clima actual de una ciudad.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Nombre de la ciudad, p. ej. 'Denver'"},
},
"required": ["city"],
},
},
{
"name": "convert_temperature",
"description": "Convertir una temperatura entre Celsius y 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:
# Sustituto de una llamada real a la API del clima.
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"herramienta desconocida: {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("Respuesta truncada en max_tokens; aumenta el límite.")
if response.stop_reason == "refusal":
raise RuntimeError(f"Claude rechazó la solicitud: {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 hay llamadas a herramientas ni end_turn - nada más que hacer de forma segura.
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("Se excedió max_iterations sin alcanzar end_turn.")
return "".join(b.text for b in response.content if b.type == "text")
if __name__ == "__main__":
answer = run_agent(
"¿Cuál es el clima en Denver ahora mismo, en Fahrenheit?"
)
print(answer)Lo que esto demuestra:
- Un bucle acotado (
for _ in range(max_iterations)) que siempre termina, con unRuntimeErrorlanzado a través de la cláusulafor...elsesi Claude nunca alcanzaend_turn. - Se anexa el
response.contentcompleto, no solo el texto extraído, como el turno del asistente para que los bloquestool_useque Claude emitió sean visibles en la próxima llamada. - Un
tool_resultemparejado portool_use_idpor cada bloquetool_use, agrupado en un solo mensajeuserpor turno (requerido cuando Claude solicita llamadas a herramientas en paralelo). - Manejo explícito para
pause_turn,max_tokensyrefusaljunto con las dos rutas de "seguir en bucle" (tool_useyend_turn). - Un único despachador
execute_toolque mantiene el enrutamiento de herramientas en un solo lugar en lugar de cadenasif/elifdispersas cerca de la llamada a la API.
Inmersión Profunda
Cómo Funciona
- Cada llamada a
client.messages.create()es independiente. La API no tiene estado de sesión: todo lo que Claude "recuerda" es lo que incluiste enmessagesen esa llamada. - Cuando Claude quiere una herramienta, deja de generar salida y devuelve bloques de contenido: algunos
text(raro a mitad de bucle, pero posible), uno o más bloquestool_use, y unastop_reason. - Tu código ejecuta la(s) herramienta(s) solicitada(s) localmente (o a través de un conector) e informa de vuelta con bloques
tool_result, emparejados con la llamada original a través detool_use_id. - La siguiente llamada a
messages.create()reenvía toda la conversación creciente: el turno original del usuario, el turno del asistente de Claude con bloquestool_use, tu turnousercon bloquestool_result, para que Claude tenga el contexto completo para continuar o concluir. - El bucle no es una llamada única de "llamar a la herramienta, obtener respuesta"; Claude puede encadenar varias llamadas a herramientas (buscar algo, luego actuar sobre ello, luego verificar) antes de tener suficiente información para responder, por lo que el bucle, no una sola solicitud/respuesta, es el modelo mental correcto.
Valores de stop_reason y Qué Hacer
stop_reason | Significado | Acción del bucle |
|---|---|---|
end_turn | Claude ha terminado; no hay más llamadas a herramientas pendientes | Salir del bucle, devolver el texto |
tool_use (implícito siempre que haya bloques tool_use y no haya otra razón terminal) | Claude quiere ejecutar una o más herramientas | Ejecutar cada herramienta, anexar bloques tool_result, continuar |
pause_turn | Una herramienta del lado del servidor (p. ej., búsqueda web, ejecución de código) alcanzó su propio límite de iteración interno a mitad de turno | Anexar response.content tal cual y reenviar; la API se reanuda automáticamente - no agregar un mensaje de usuario "Continuar" adicional |
max_tokens | La salida fue truncada en el límite de tokens | Lanzar/registrar un error; considerar aumentar max_tokens o usar streaming en lugar de continuar silenciosamente |
refusal | Claude se negó a continuar por razones de seguridad | Inspeccionar stop_details, mostrarlo al llamador; no reintentar ciegamente el mismo prompt |
Forma del Mensaje en Cada Paso
# Después del turno 1 (Claude solicita una llamada a herramienta):
messages == [
{"role": "user", "content": "What's the weather in Denver?"},
{"role": "assistant", "content": [ToolUseBlock(id="toolu_01...", name="get_weather", input={"city": "Denver"})]},
]
# Después de que ejecutas la herramienta y anexas el resultado:
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 de Python
# Prefiere un diccionario de distribución sobre largas cadenas if/elif a medida que aumenta el número de herramientas:
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"herramienta desconocida: {name}"})
try:
return handler(tool_input)
except Exception as exc:
# Informar fallos a Claude como datos, no como un proceso caído.
return json.dumps({"error": str(exc)})Parámetros y Valores de Retorno
| Campo | Tipo | Descripción |
|---|---|---|
response.stop_reason | str | Por qué se detuvo la generación: end_turn, tool_use-implícito (bloques presentes), pause_turn, max_tokens, refusal, etc. |
response.content | list[ContentBlock] | Bloques ordenados que Claude devolvió en este turno - text y/o tool_use. Anexar verbatim como el mensaje del asistente. |
block.id | str | ID único en un bloque tool_use; devuélvelo como tool_use_id en el tool_result correspondiente. |
block.input | dict | Argumentos analizados para la llamada a la herramienta, validados contra el input_schema de la herramienta. |
tool_result["content"] | str | list | La salida de la herramienta, como una cadena o lista de bloques de contenido; mantenlo pequeño y estructurado (p. ej., JSON). |
Trampas
- Anexar solo el texto extraído, no
response.content. Si envías{"role": "assistant", "content": final_text}en lugar de los bloques de contenido crudos, la próxima solicitud de Claude pierde los bloquestool_useque tu mensajetool_resultdebe responder, y la API rechazará el mensajetool_resultde seguimiento porque no coincide con una llamada a herramienta anterior. Solución: siempre anexaresponse.contentsin modificar como el turno del asistente. - Sin límite de iteración. Un bucle que solo sale en
end_turnse ejecutará indefinidamente si una herramienta sigue devolviendo datos ambiguos o el modelo se queda atascado solicitando la misma llamada. Solución: envuelve el bucle enfor _ in range(max_iterations)(10-15 es un valor predeterminado razonable) y lanza/registra un tiempo de espera cuando se agote. - Tratar
pause_turncomo un turno normal detool_use. Enviar un nuevo mensaje de usuario "Continuar" después depause_turn(en lugar de reenviar el turno del asistente pausado tal cual) confunde el estado de la herramienta del lado del servidor reanudada. Solución: anexaresponse.contenty llama a la API nuevamente de inmediato, sin mensaje de usuario adicional. tool_use_idfaltante o incorrecto. Cadatool_resultdebe hacer referencia aliddel bloquetool_useal que responde; omitir un bloque (p. ej., cuando Claude solicita dos llamadas paralelas pero solo manejas una) deja a la API esperando una llamada a herramienta no resuelta. Solución: itera sobre todos los bloquestool_useen la respuesta y produce exactamente untool_resultpor bloque, agrupado en un solo mensajeuser.- Dejar que una excepción de herramienta bloquee el bucle. Una excepción no capturada dentro de
execute_toolmata toda la solicitud en lugar de darle a Claude la oportunidad de recuperarse (p. ej., reintentar con argumentos diferentes). Solución: captura las excepciones por herramienta y devuélvelas como contenido detool_result(p. ej.,{"error": "..."}) para que Claude pueda reaccionar. - Ignorar
max_tokenscomo razón de parada. Tratarmax_tokenscomoend_turnenvía silenciosamente una respuesta truncada al usuario. Solución: compruébalo explícitamente y lanzamax_tokensen la siguiente llamada o cambia a streaming. - Reintentar ciegamente en
refusal. Repetir el mismo prompt después de una negativa de seguridad desperdicia llamadas y no cambiará el resultado. Solución: inspeccionastop_details, ajusta la solicitud o escala a un humano en lugar de reintentar textualmente.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
client.beta.messages.tool_runner(...) con funciones decoradas con @beta_tool, luego runner.until_done() | Quieres que el ciclo de llamada -> ejecución -> retroalimentación -> repetición esté automatizado y no necesitas control detallado sobre la ejecución (concurrencia, sandboxing, lógica de reintento personalizada) | Necesitas interceptar cada turno, p. ej., transmitir resultados parciales a una interfaz de usuario, registro personalizado por llamada a herramienta o formato de resultado no estándar |
Solicitud de un solo disparo con tool_choice forzando exactamente una llamada a herramienta, sin bucle | Sabes de antemano que la respuesta necesita exactamente una llamada a herramienta y nada más | La tarea puede requerir llamadas de seguimiento basadas en lo que devuelve la primera herramienta |
| Un framework/orquestador (p. ej., LangChain, un runtime de agente personalizado) que envuelve su propio bucle | Ya estás estandarizado en ese framework en toda la base de código y quieres consistencia sobre el minimalismo | Quieres entender y controlar exactamente lo que se envía a la API en cada turno, o quieres cero dependencias adicionales |
Preguntas Frecuentes
¿Por qué no puedo simplemente hacer una llamada a la API y esperar la respuesta final?
Porque la API no tiene estado y Claude no puede ejecutar herramientas por sí mismo. Si Claude necesita datos externos, devuelve un bloque tool_use y se detiene; tu código debe ejecutar la herramienta, enviar un tool_result y llamar a la API nuevamente para que Claude continúe.
¿Cómo sé cuándo ha terminado el bucle?
Comprueba response.stop_reason == "end_turn". Esa es la única señal confiable de que Claude no tiene más llamadas a herramientas pendientes y ha producido su respuesta final.
¿Qué sucede si solo anexo el texto que generó Claude en lugar de los bloques de contenido completos?
La siguiente solicitud carecerá de los bloques tool_use a los que se supone que debe responder tu mensaje tool_result, y la API la rechazará. Siempre anexa response.content tal cual.
¿Necesito un mensaje `tool_result` separado para cada llamada a herramienta?
No, agrupa todos los bloques tool_result para un turno dado en un solo mensaje user, un bloque por cada tool_use_id de los bloques tool_use de ese turno.
¿Cuál es la diferencia entre `tool_use` y `pause_turn`?
tool_use: Claude está pidiendo a tu código que ejecute una herramienta; la ejecutas y envías untool_result.pause_turn: una herramienta del lado del servidor (como la búsqueda web) alcanzó su propio límite de iteración interno; simplemente reenvías la conversación sin cambios y la API reanuda esa herramienta por sí misma.
¿Por qué necesito un guardia `max_iterations` si el bucle ya sale en `end_turn`?
Porque end_turn no está garantizado que llegue: una herramienta que se porta mal, datos ambiguos o un modelo atascado solicitando la misma llamada pueden mantener el bucle en marcha indefinidamente. El guardia convierte un bloqueo infinito en un fallo acotado que puedes detectar e informar.
¿Qué debería devolver `execute_tool` cuando la herramienta falla?
Una cadena (a menudo JSON) que describe el error, p. ej., json.dumps({"error": str(exc)}), devuelta como contenido normal de tool_result. Captura la excepción tú mismo; no dejes que bloquee el bucle, y no te quedes en silencio: Claude necesita ver que la llamada falló para reaccionar adecuadamente.
¿Debo reintentar automáticamente cuando `stop_reason` es `refusal`?
No. Un refusal significa que Claude se negó por razones de seguridad; reintentar el prompt idéntico no cambiará eso. Inspecciona stop_details, ajusta la solicitud o dirígete a un revisor humano en lugar de reintentar.
¿Qué significa `stop_reason == "max_tokens"` para mi bucle?
La respuesta fue cortada porque alcanzó el límite de max_tokens; no es una respuesta completa ni una solicitud de herramienta. Trátalo como una condición de error: lanza max_tokens, cambia a streaming o manéjalo explícitamente en lugar de tratarlo como end_turn.
¿Puede Claude solicitar más de una llamada a herramienta en un solo turno?
Sí. response.content puede contener múltiples bloques tool_use en un turno (llamadas a herramientas paralelas). Itera sobre todos ellos y devuelve un tool_result por bloque en el mismo mensaje user de seguimiento.
¿Cuándo debería usar el Tool Runner en lugar de un bucle manual?
Una vez que entiendas el bucle manual y no necesites control de bajo nivel sobre la ejecución: el Tool Runner (client.beta.messages.tool_runner(...) con funciones decoradas con @beta_tool y runner.until_done()) automatiza el mismo ciclo de llamada -> ejecución -> retroalimentación -> repetición con menos código repetitivo.
¿A qué modelo deben dirigirse los ejemplos de código?
claude-sonnet-5 - el modelo predeterminado actual en la línea de modelos de Claude (junto con Claude Fable 5, Claude Opus 4.8 y Claude Haiku 4.5) - es un buen predeterminado para bucles de uso de herramientas que equilibra capacidad y costo.
Relacionado
- Manejo de Solicitudes
tool_usey Devolución de Bloquestool_result- la mecánica de un solo turno que este bucle repite en cada iteración. - Ejecución de Llamadas a Herramientas Paralelas en un Solo Turno - cómo manejar múltiples bloques
tool_usedevueltos en un turno. - Conceptos Básicos de Uso de Herramientas - los conceptos fundamentales (herramientas,
tool_choice, esquemas) sobre los que se basa esta página. - Mejores Prácticas - guía más amplia para un uso de herramientas confiable y listo para producción.
Versiones de la Pila: Escrito contra la línea de modelos de Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado), y Claude Haiku 4.5 - y el SDK oficial de Python
anthropic(última versión 0.x). Los nombres de los modelos, las versiones del SDK y los precios cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs antes de confiar en ellos.