Manejo de Solicitudes tool_use y Devolución de Bloques tool_result
Ejecuta la herramienta solicitada y devuelve un bloque de contenido tool_result coincidente.
Resumen
Cuando stop_reason en un Message regresa como "tool_use", Claude se ha pausado a mitad de turno para pedirle a tu código que ejecute algo. Aún no ha respondido al usuario; está esperando por ti.
Tu trabajo es un traspaso fijo de cinco pasos: encontrar los bloques tool_use en response.content, ejecutar la función Python correspondiente con block.input, añadir response.content del asistente a messages de forma literal, envolver la salida de cada herramienta en un bloque tool_result indexado por tool_use_id, y llamar a messages.create nuevamente.
Esta página trata sobre cómo realizar ese traspaso de forma correcta y defensiva: leer block.input como el diccionario analizado que te da el SDK (nunca volviendo a analizar o buscando coincidencias de cadenas en JSON crudo), manejar cada bloque tool_use en un turno (no solo el primero), y reportar fallos con is_error en lugar de dejar que una excepción mate la solicitud.
Si logras este bucle correctamente una vez, se convertirá en una función pequeña y reutilizable que cada solicitud que use herramientas en tu aplicación podrá llamar.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)Cuándo recurrir a esto:
- Cualquier solicitud en la que hayas pasado una lista
toolsno vacía y necesites honrar realmente una respuestatool_use. - Turnos en los que Claude podría solicitar más de una herramienta a la vez; el bucle anterior las maneja todas antes de responder.
- En cualquier lugar donde una llamada a herramienta pueda fallar plausiblemente (una API inestable, una búsqueda incorrecta) y quieras que Claude vea el fallo en lugar de que tu proceso se bloquee.
- Construir una utilidad general de "ejecutar esta conversación hasta que Claude deje de pedir herramientas" que reutilizarás en todos los puntos de conexión.
Ejemplo de Trabajo
import anthropic
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Obtener el clima actual para una ubicación. Llama a esto cuando el usuario pregunte sobre las condiciones actuales.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Ciudad y estado, por ejemplo, San Francisco, CA",
}
},
"required": ["location"],
},
}
stock_price_tool = {
"name": "get_stock_price",
"description": "Obtener el último precio para un símbolo de ticker de acciones.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Símbolo de ticker de acciones, por ejemplo, AAPL",
}
},
"required": ["ticker"],
},
}
tools = [weather_tool, stock_price_tool]
def get_weather(location: str) -> str:
# Reemplaza con una llamada real a la API del clima.
return f"72F y soleado en {location}"
def get_stock_price(ticker: str) -> str:
# Reemplaza con una llamada real a la API de datos del mercado.
if not ticker.isalpha():
raise ValueError(f"'{ticker}' no es un símbolo de ticker válido")
return f"{ticker.upper()} cotiza a $184.32"
TOOL_FUNCTIONS = {
"get_weather": get_weather,
"get_stock_price": get_stock_price,
}
def execute_tool(name: str, tool_input: dict) -> str:
if name not in TOOL_FUNCTIONS:
raise ValueError(f"Herramienta desconocida: {name}")
return TOOL_FUNCTIONS[name](**tool_input)
def run_turn(messages: list) -> list:
"""Envía mensajes, maneja cualquier ronda de tool_use, devuelve la lista actualizada."""
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
return messages
if __name__ == "__main__":
conversation = [
{
"role": "user",
"content": "¿Cuál es el clima en Austin, TX, y a cuánto cotiza AAPL?",
}
]
conversation = run_turn(conversation)
final_text = next(
block.text for block in conversation[-1]["content"] if block.type == "text"
)
print(final_text)Lo que esto demuestra:
- Un bucle
while response.stop_reason == "tool_use":, para que el manejador siga resolviendo llamadas a herramientas hasta que Claude produzca una respuesta de texto final, no solo una ronda. - Una tabla de despacho (
TOOL_FUNCTIONS) que mapeablock.namea una función Python real, conexecute_toollanzando un error limpio para cualquier nombre que no reconozca. - Dos herramientas solicitadas en la misma pregunta del usuario, ambas resueltas y devueltas juntas en un único mensaje de
userque contienetool_result. - Un
try/exceptalrededor de cada ejecución de herramienta, para que un símbolo de cotización incorrecto (que hace queget_stock_pricelanceValueError) no bloquee la búsqueda del clima que se ejecuta junto a él.
Análisis Profundo
Cómo Funciona
response.stop_reason == "tool_use"significa que Claude dejó de generar específicamente para devolverte el control; no es un estado de error ni la respuesta final.response.contentes una lista de bloques de contenido. Un turnotool_usepuede contener un bloquetextinicial (Claude "pensando en voz alta") más uno o más bloquestool_use; siempre filtra porblock.type == "tool_use"en lugar de asumir una forma fija.- Cada bloque
tool_uselleva.id(un identificador único para esta llamada específica),.name(qué herramienta), y.input(los argumentos, ya analizados en un diccionario Python que coincide con tuinput_schema). - El mensaje del asistente que añades a
messagesdebe serresponse.contentexactamente como se devolvió, no una reconstrucción; es lo que preserva eliddel bloquetool_usepara que eltool_resultde seguimiento pueda referenciarlo correctamente. - Los bloques
tool_resultsiempre viven dentro de un mensaje de roluser, nunca de un rolassistant, aunque nada de ellos fue escrito por un humano.
Estructuración del Contenido de tool_result
Valor de content | Cuándo usarlo | Ejemplo |
|---|---|---|
| Una cadena simple | La mayoría de las salidas de herramientas: texto, JSON como cadena, resúmenes cortos | "72F y soleado en Austin, TX" |
| Una lista de bloques de contenido | La salida de la herramienta incluye múltiples partes, como texto más una imagen | [{"type": "text", "text": "..."}, {"type": "image", "source": {...}}] |
Una cadena de error con is_error: True | La herramienta lanzó una excepción o la llamada subyacente falló | {"content": "Ticker no encontrado", "is_error": True} |
Notas de Python
# Nunca busques coincidencias de cadenas en JSON crudo para averiguar qué pidió Claude.
# Los modelos de la familia Claude 4 pueden escapar Unicode o barras diagonales de manera diferente
# entre respuestas, por lo que la coincidencia de texto crudo con block.input es frágil.
# Usa siempre el diccionario ya analizado del SDK:
for block in response.content:
if block.type == "tool_use":
# block.input es un diccionario; indexalo directamente, no uses json.loads()
location = block.input["location"]Parámetros y Valores de Retorno
| Campo | Tipo | Descripción |
|---|---|---|
type | str | Siempre "tool_result" para este bloque. |
tool_use_id | str | Debe ser igual al .id del bloque tool_use al que se responde. |
content | str o list[dict] | La salida de la herramienta: una cadena o una lista de bloques de contenido. |
is_error | bool (opcional) | Establecer en True cuando la herramienta lanzó una excepción o falló; omitir o establecer en False en caso de éxito. |
Errores Comunes
- Coincidir con texto JSON crudo en lugar de
block.input. Algunos equipos intentan usar expresiones regulares o coincidencia de cadenas de la llamada a la herramienta antes de analizarla. Los modelos de la familia Claude 4 pueden variar cómo escapan el Unicode o las barras diagonales en el JSON generado, por lo que esto falla intermitentemente. Solución: siempre leeblock.input; el SDK ya lo ha analizado en un diccionario Python para ti. - Añadir solo el texto, no el
response.contentcompleto, como turno del asistente. Si extraesresponse.content[0].texty lo insertas como cadena enmessages, eliddel bloquetool_usese pierde y la siguiente solicitud falla la validación. Solución: añaderesponse.contentliteralmente:messages.append({"role": "assistant", "content": response.content}). - Dividir múltiples bloques
tool_resulten mensajes de usuario separados. Cuando Claude solicita dos herramientas en un turno, enviar dos mensajes deuserde seguimiento (uno por resultado) en lugar de un mensaje con dos bloques produce una conversación mal formada. Solución: recopila cadatool_resulten una sola lista y envíalos juntos:messages.append({"role": "user", "content": tool_results}). - Dejar un bloque
tool_usesin respuesta. Si Claude solicita tres herramientas y tú solo devuelves dos bloquestool_result(por ejemplo, porque el tercero falló silenciosamente y tragaste la excepción), la siguiente llamada amessages.createrechaza la solicitud. Solución: envuelve cada llamada a herramienta entry/excepty siempre añade untool_result; en caso de fallo, estableceis_error: Trueen lugar de omitirlo. - Olvidar
is_erroren una llamada a herramienta fallida. Devolver la cadena de la excepción comocontentordinario sinis_error: Truehace que Claude trate una pila de llamadas o un mensaje de error como datos legítimos, que luego puede repetir al usuario como si fuera una respuesta real. Solución: siempre establece"is_error": Truecuando la herramienta lanzó una excepción o devolvió un estado de fallo. - No manejar un
block.nameno reconocido. Si Claude llama a un nombre de herramienta para el que tu código de despacho no tiene una rama, una cadenaif/elifsin protección se cae y no hace nada o lanza unKeyErrorno manejado. Solución: haz que el caso de "herramienta desconocida" sea una rama explícita que devuelva untool_resultconis_error: True, como lo haceexecute_toolen el Ejemplo de Trabajo anterior. - Pasar un objeto no serializable como
content. Devolver una instancia de clase personalizada, undatetimeo un objeto de fila de base de datos directamente comotool_result["content"]falla cuando se serializa la solicitud. Solución: convierte la salida de la herramienta a una cadena o una estructura simple serializable en JSON antes de construir el bloquetool_result.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| Bucle de ronda manual (esta página) | Quieres control total sobre el orden de ejecución, reintentos y manejo de errores por herramienta | El conjunto de herramientas es lo suficientemente grande y dinámico como para que un bucle gestionado ahorre código repetitivo real |
Bucle de agente gestionado por SDK (client.beta.messages.tool_runner, beta) | Quieres que el SDK impulse el ciclo de creación-ejecución-reenvío en lugar de escribir manualmente el bucle while | Necesitas lógica personalizada por herramienta: diferentes políticas de reintento, registro o formas de resultado no estándar |
| Framework de agente de terceros | Necesitas memoria incorporada, recuperación u orquestación multiagente superpuesta al uso de herramientas | Quieres dependencias mínimas y visibilidad completa de cada solicitud enviada a la API |
| Sin uso de herramientas | La tarea solo necesita generación y no datos en vivo ni efectos secundarios | La respuesta depende de datos en tiempo real, estado específico del usuario o una acción que el modelo no puede realizar por sí solo |
Preguntas Frecuentes
¿Cómo sé cuándo Claude quiere que ejecute una herramienta?
Comprueba response.stop_reason == "tool_use" después de cada llamada a messages.create. Si es verdadero, response.content contiene al menos un bloque tool_use para manejar antes de tener una respuesta final.
¿Está block.input ya analizado, o necesito hacer json.loads() yo mismo?
Ya está analizado. block.input es un diccionario Python que coincide con tu input_schema; no se necesita ningún paso de decodificación y nunca debes recurrir a la coincidencia del texto de respuesta crudo.
¿Por qué el mensaje del asistente necesita response.content en lugar de solo el texto?
El id del bloque tool_use vive dentro de response.content. Si insertas solo texto extraído en messages, ese id se pierde y el tool_result que envías a continuación no tiene nada válido a lo que referenciar.
¿Qué rol usa el mensaje tool_result: assistant o user?
user. Aunque un tool_result es generado por tu código en lugar de escrito por una persona, la API lo modela como el siguiente turno user en la conversación.
¿Qué sucede si Claude pide dos herramientas en el mismo turno?
Itera sobre cada bloque en response.content, ejecutando cada bloque tool_use que encuentres, y recopila todos los bloques tool_result resultantes en una sola lista. Envía esa lista como el content de un único mensaje user, no como dos mensajes separados.
¿Qué debe contener el contenido dentro de un bloque tool_result?
Normalmente una cadena simple. También puede ser una lista de bloques de contenido (por ejemplo, texto más una imagen) cuando la salida de la herramienta tiene más de una parte.
¿Qué hago si mi función de herramienta lanza una excepción?
try:
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})Devuelve un tool_result en cualquier caso, con is_error: True en caso de fallo, para que Claude pueda ver qué sucedió y adaptarse.
¿Puedo omitir la devolución de un tool_result si la llamada a la herramienta no tiene sentido?
No. Cada bloque tool_use en el turno del asistente necesita un tool_result correspondiente en el siguiente mensaje user, incluso si ese resultado es solo una cadena de error que explica por qué la llamada no se pudo completar.
¿Qué pasa si Claude solicita un nombre de herramienta que no reconozco?
Trátalo como cualquier otro fallo: devuelve un tool_result con is_error: True y un mensaje como "Herramienta desconocida: <nombre>", en lugar de dejar que una rama no manejada bloquee tu bucle.
¿Necesito seguir pasando tools= en la llamada a messages.create de seguimiento?
Sí. Claude puede necesitar llamar a una herramienta nuevamente basándose en el resultado que acabas de devolver, por lo que la solicitud de seguimiento debe incluir la misma lista tools que la llamada original.
¿Por qué tool_use_id es tan importante?
Es lo único que empareja un tool_result con el bloque tool_use específico al que está respondiendo. Un tool_use_id faltante o incorrecto produce una conversación mal formada que la API rechazará.
¿La ejecución de herramientas debe ser secuencial o puedo ejecutar herramientas en paralelo?
A la API no le importa el orden de ejecución; solo requiere que cada bloque tool_use reciba un tool_result en el mismo mensaje de seguimiento. Puedes ejecutar múltiples herramientas concurrentemente (por ejemplo, con un grupo de hilos) siempre que recopiles todos los resultados en un único mensaje user antes de llamar a messages.create nuevamente.
Relacionado
- Conceptos básicos de uso de herramientas - el tutorial completo de 10 ejemplos del cual se extrae la ronda de esta página.
- Definición de esquemas de herramientas con name, description e input_schema - escritura del
input_schemaque produce valoresblock.inputconfiables. - Construcción de un bucle de uso de herramientas multi-turno - generalización del bucle
whileque se muestra aquí en una utilidad reutilizable. - Ejecución de llamadas a herramientas paralelas en un solo turno - más sobre el procesamiento por lotes de múltiples bloques
tool_resultjuntos.
Versiones de Stack: Escrito contra la línea de modelos 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 modelos, versiones de SDK y precios cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs antes de confiar en ellos.