Módulo 4: Herramientas: el agente que actúa sobre sistemas reales
2. Qué es una tool y cómo el agente decide usarla (tool calling)
Descripción
Al terminar esta lección vas a poder describir la anatomía exacta de una tool — sus tres partes obligatorias — y trazar, para un mensaje concreto de un cliente, el JSON exacto que el modelo produce para decidir usarla, con qué datos la llena y qué hace n8n con esa decisión. Vas a poder distinguir, ante un agente que se comporta mal, si el problema está en lo que el modelo decidió mandar o en lo que la tool hizo con eso — dos causas que se ven idénticas desde afuera y muy distintas cuando abres el panel de ejecución.
Esto importa porque "tool calling" es el mecanismo que hace posible todo lo que se prometió en la lección anterior: un agente que actúa sobre sistemas reales, no solo que opina sobre ellos. Si trabajas como consultor de automatización — o simplemente eres tú quien sostiene el agente que armaste —, vas a terminar en una sala (o un canal de Slack) explicando por qué el agente le aplicó un descuento al cliente equivocado, o por qué falló al buscar un pedido. La respuesta casi nunca es "la IA se equivocó" en abstracto: es una de dos cosas concretas, el modelo decidió mal los argumentos o la tool ejecutó mal con argumentos correctos. Sin entender el mecanismo, no puedes distinguir cuál fue.
Conexión con el módulo: en la lección 1 viste, a grandes rasgos, qué significa dar "manos" al agente. En el Módulo 1 ya usaste una tool en dos ocasiones — la lección 3 te mostró el bucle razonar-actuar-observar, en el que la tool aparece como el paso "actuar", y la lección 5 te mostró que una tool conectada al nodo AI Agent tiene un nombre y una descripción, y que una descripción vaga hace que el agente la use mal. Esta lección abre esa caja un nivel más: qué es, técnicamente, lo que el modelo recibe sobre cada tool antes de decidir; qué JSON exacto produce cuando decide llamarla; y cómo llena cada dato de esa llamada, no solo si la llama o no. Todavía no vas a ver el catálogo de tools nativas de n8n (buscar, crear, enviar) — eso es exactamente el trabajo de la lección 3, justo después de esta.
Una tool es un formulario que el modelo llena, no un botón que aprieta
Piensa en una oficina grande con varios departamentos especializados — Facturación, Recursos Humanos, Logística —, cada uno con su propio formulario de solicitud. Cada formulario tiene, impreso arriba, un nombre corto ("Solicitud de reembolso") y un párrafo que explica exactamente cuándo corresponde usar ese formulario y no otro ("Usa este formulario cuando el cliente ya pagó pero pide su dinero de vuelta; para cambios de producto sin devolver dinero, usa el formulario de Cambios"). Debajo, una serie de campos en blanco, cada uno con su propia instrucción de qué va ahí: "Número de orden (solo dígitos)", "Monto en dólares, con dos decimales", "Motivo (texto libre)".
La persona que atiende el mostrador —en esta analogía, el modelo— nunca entra a Facturación ni toca el sistema contable directamente. Lo que hace es leer el nombre y el párrafo de cada formulario disponible para decidir cuál corresponde a lo que el cliente está pidiendo, y después llenar cada campo en blanco leyendo su instrucción y extrayendo el valor correspondiente de lo que el cliente dijo. Una vez lleno, entrega el formulario al departamento — y es el departamento, no la persona del mostrador, quien de verdad accede al sistema, hace el cargo o la devolución, y regresa un comprobante.
Eso es exactamente lo que es una tool para un modelo de lenguaje, sin metáfora de por medio. Técnicamente, cada tool que le ofreces a un modelo con capacidad de tool calling es un objeto con tres partes, ni una más:
name— el nombre corto del formulario. Un identificador, no una frase.description— el párrafo que dice cuándo corresponde usar esta tool y no otra. Es el único lugar donde el modelo lee "para qué sirve": no ve tu código, no ve qué hace el nodo por dentro.input_schema(oparameters, según el proveedor del modelo) — la lista de campos en blanco: cada uno con su propio nombre, su propio tipo de dato (texto, número, booleano...), su propia descripción de qué instrucción sigue el modelo para llenarlo, y si es obligatorio o no.
La documentación oficial de Claude lo muestra así, con un ejemplo mínimo de una tool get_weather:
{
"name": "get_weather",
"description": "Get the current weather for a given location.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}
}
Fíjate en algo: la descripción no está solo en el nivel de la tool completa ("Get the current weather...") — también está dentro de cada campo ("City and state, e.g. San Francisco, CA"). Ya viste, en la lección 5 del Módulo 1, que una descripción vaga a nivel de tool hace que el agente la use en el momento equivocado. Lo que todavía no habías visto es que el mismo problema existe un nivel más abajo: una descripción vaga en un campo específico hace que el modelo llene ese campo con el formato equivocado, aunque haya decidido usar la tool correcta.
n8n construye este objeto por ti, a partir de lo que configuras en el nodo tool que conectas a ai_tool. El campo Description del nodo es literalmente el description de la tool completa — el mismo que ya usaste en el Módulo 1. Y cada valor de un campo del nodo que envuelves con la función $fromAI(...) se convierte en una entrada de properties dentro de input_schema: la clave que le das como primer argumento es el nombre del campo, el segundo argumento es su descripción, el tercero su tipo. Cualquier campo del nodo que no envuelvas con $fromAI(...) queda fuera del formulario por completo — es un valor fijo que tú decidiste al construir el flujo, y el modelo ni siquiera sabe que existe como algo que podría cambiar.
Ejemplo trabajado
Vuelve a la tool get_order_status del agente de soporte de TuTienda que configuraste en la lección 5 del Módulo 1. Ahí la viste así, simplificada:
tool.name = "get_order_status"
tool.description = "Usa esta herramienta cuando el cliente dé un número de
pedido y pregunte por su estado o fecha de entrega."
tool.url = "https://api.tutienda.com/orders/{order_id}/status"
Esa versión escondía un detalle a propósito, para no adelantar esta lección. Así se ve completa, con el campo URL usando $fromAI() para marcar exactamente qué parte del valor decide el modelo:
# Nodo: HTTP Request, conectado a ai_tool
tool.name = "get_order_status"
tool.description = "Usa esta herramienta cuando el cliente dé un número de
pedido y pregunte por su estado o fecha de entrega. No la
uses para preguntas sobre política de cambios o devoluciones."
tool.url = "https://api.tutienda.com/orders/{{ $fromAI('order_id',
'Número de pedido que mencionó el cliente, solo dígitos,
sin el símbolo #', 'string') }}/status"
tool.method = "GET" # valor fijo — no tiene $fromAI(), el modelo no lo decide
Lo que n8n arma a partir de esa configuración —y envía a la API del modelo junto con el resto de la conversación, en cada turno— es, en esencia, esto:
{
"name": "get_order_status",
"description": "Usa esta herramienta cuando el cliente dé un número de pedido y pregunte por su estado o fecha de entrega. No la uses para preguntas sobre política de cambios o devoluciones.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Número de pedido que mencionó el cliente, solo dígitos, sin el símbolo #"
}
},
"required": ["order_id"]
}
}
Nota que method no aparece en ningún lado de ese objeto, porque nunca lo marcaste con $fromAI(). El modelo no sabe que ese campo existe; para él, esta tool tiene un único dato que llenar.
Turno del cliente: "¿Dónde está mi pedido #4521?"
Qué esperar — lo que el modelo produce. El modelo lee la conversación, ve las tools disponibles, decide que get_order_status aplica (la descripción de la tool calza con la pregunta) y arma la llamada. En vez de escribir una respuesta de texto para el cliente, produce un bloque estructurado —tool_use, en la terminología de la API de Claude— con el nombre de la tool y los argumentos:
{
"type": "tool_use",
"id": "toolu_01A4521exampleid",
"name": "get_order_status",
"input": { "order_id": "4521" }
}
Interpretación de ese input: el modelo tomó "#4521" del mensaje del cliente y lo transformó en "4521" —sin el símbolo #— porque la descripción del campo se lo pidió explícitamente ("solo dígitos, sin el símbolo #"). Si esa descripción no hubiera dicho nada sobre el formato, no hay garantía de qué habría mandado el modelo: podría haber sido "4521", "#4521" o "order-4521" — las tres son lecturas razonables del mismo mensaje, y solo la descripción del campo elimina la ambigüedad.
Qué esperar — lo que hace n8n con eso. El modelo nunca toca api.tutienda.com. n8n toma el input que acaba de recibir, sustituye {{ $fromAI('order_id', ...) }} por el valor "4521" dentro de la URL configurada, y ejecuta el nodo HTTP Request exactamente como ejecutaría cualquier otro nodo del flujo: una petición GET real, con las credenciales reales que configuraste, contra el sistema real. La respuesta de esa API —supongamos {"status": "in_transit", "estimated_delivery": "2026-07-24"}— se empaqueta como el resultado de la tool (un tool_result, del lado de la API del modelo) y se agrega a la conversación como un dato más disponible para la siguiente pasada de razonamiento.
Qué esperar — la respuesta final. Con ese resultado ya en el contexto, el modelo razona una vez más (el mismo ciclo razonar-actuar-observar que viste en la lección 3 del Módulo 1) y esta vez no necesita llamar ninguna tool más: ya tiene el dato. Produce la respuesta de texto para el cliente: "Tu pedido #4521 está en tránsito, con entrega estimada el 24 de julio."
El punto completo del ejemplo es que hay dos actores, no uno. El modelo decide —qué tool, con qué argumentos, leyendo únicamente name, description e input_schema de cada tool disponible— y n8n ejecuta: toma esa decisión, la combina con lo que configuraste como fijo, y corre la acción real contra el sistema real. El modelo jamás ve tu URL completa, tu API key, ni nada del sistema salvo el texto que n8n decide devolverle como resultado.
Cómo se llena cada campo — y qué pasa cuando falta un dato
Ya viste que cada campo del input_schema trae su propia descripción, y que el modelo la usa como instrucción de formato. Pero queda una pregunta sin responder: ¿de dónde saca el modelo el valor si el cliente nunca lo dijo?
La respuesta corta es que no hay magia: el modelo solo puede llenar un campo con información que está, de una forma u otra, disponible en el contexto que recibió —el mensaje actual, el historial si hay memoria conectada, o el resultado de una tool anterior en el mismo ciclo—. Cuando un campo obligatorio (required) no tiene ningún valor disponible en ese contexto, dos cosas pueden pasar, y cuál pasa depende del modelo que conectaste en ai_languageModel. La documentación oficial de Claude advierte específicamente que Opus reconoce con más consistencia cuándo falta un dato obligatorio y responde pidiéndolo, mientras que Sonnet a veces también lo pide —sobre todo si el prompt lo instruye a razonar antes de llamar la tool— pero otras veces prefiere inferir un valor razonable en vez de preguntar. Ninguno de los dos comportamientos es un error del mecanismo; son dos formas distintas de resolver la misma ambigüedad, y la diferencia importa cuando decides qué modelo conectar a un agente que maneja datos donde "inventar un valor razonable" no es aceptable (un monto de reembolso, una fecha de vencimiento).
Esto te da una palanca de diseño concreta, no solo una curiosidad: si quieres que el agente siempre pida el dato en vez de adivinarlo, puedes decirlo en el System Message ("si el cliente no dio un número de pedido, pregúntalo antes de usar cualquier tool que lo necesite"). Estás usando el prompt —la pieza que ya conoces desde el Módulo 1— para ajustar el comportamiento del paso de razonar antes de que llegue a "actuar".
Errores comunes
Pensar que el modelo se conecta directamente al sistema real (conceptual). Qué pasa: cuando algo falla —una llamada HTTP con timeout, un error de credenciales—, alguien asume que "el modelo se conectó mal a la API", o le preocupa que el modelo tenga acceso directo a la base de datos o a las claves del sistema. Por qué pasa: el término "tool calling" y ver al agente "ejecutar" una acción dan la sensación de que el modelo hace el trabajo de punta a punta. Pero como viste en el ejemplo trabajado, el modelo solo produce un bloque tool_use —un JSON con un nombre y unos argumentos— y ahí termina su participación en esa vuelta del ciclo. La ejecución real, las credenciales y el acceso al sistema viven exclusivamente en el nodo de n8n conectado a ai_tool, nunca en el modelo. Cómo detectarlo: si el error que ves tiene forma de error de infraestructura (timeout, 401, 500, un campo que la API rechaza), la causa está del lado de la ejecución — revisa el nodo tool, no el prompt del modelo. Cómo corregirlo: separa mentalmente "qué decidió el modelo" (visible en el input del tool_use, en el panel de ejecución) de "qué pasó al ejecutar eso" (visible en el output del propio nodo tool) — son dos fallas distintas con dos arreglos distintos.
Confundir un campo fijo con uno que decide el modelo (conceptual). Qué pasa: alguien intenta, vía prompt o instruyendo al agente en el chat, que use un endpoint distinto o cambie un parámetro que en realidad quedó fijo en la configuración del nodo (sin $fromAI()), y no logra nada, porque ese campo nunca estuvo expuesto como parte del input_schema — el modelo ni siquiera sabe que existe como algo variable. El caso inverso también ocurre: alguien marca con $fromAI() un campo sensible —una URL base, un tipo de operación como crear o borrar— sin darse cuenta de que ese valor queda expuesto a lo que el modelo decida, potencialmente influenciado por lo que escriba quien esté chateando con el agente. Por qué pasa: en el panel del nodo, un campo con un valor fijo y un campo envuelto en $fromAI(...) se ven casi iguales —ambos son texto dentro de un input—, así que la diferencia no salta a la vista si no la buscas a propósito. Cómo detectarlo: revisa, campo por campo, la configuración de cada tool conectada: todo lo que lleva $fromAI(...) es parte del formulario que el modelo llena; todo lo demás es una decisión que ya tomaste tú al construir el flujo y no cambia sin importar qué pida la conversación. Cómo corregirlo: antes de conectar una tool, decide explícitamente y por escrito cuáles de sus campos deben depender de la conversación y cuáles deben quedar fijos — no lo dejes a lo que resulte más rápido de configurar en el momento. (Esta decisión se vuelve más seria —qué NO se le confía nunca al agente— en la lección 5 de este módulo; aquí el punto es puramente mecánico: saber cuál es cuál.)
No revisar el input real del tool_use al debuggear (práctico). Qué pasa: el agente responde mal o falla al usar una tool, y la primera reacción es asumir que "la tool está rota" —revisar la API, las credenciales, el endpoint— sin haber mirado antes qué argumentos mandó realmente el modelo. En más de un caso el problema nunca estuvo en la tool: el modelo mandó un argumento mal formado (una fecha en formato distinto al esperado, un id con espacios o símbolos de más) y la tool simplemente ejecutó, correctamente, con un dato incorrecto. Por qué pasa: es más rápido mirar la respuesta final del agente que abrir el panel de ejecución de n8n y entrar al nodo tool específico para ver su input real. Cómo detectarlo: en el panel de ejecución, el nodo de la tool muestra, en su pestaña de input, exactamente los argumentos que llegaron desde el modelo, antes de que se ejecutara nada — compáralo con lo que el cliente realmente escribió. Cómo corregirlo: cuando un agente "falla" usando una tool, mira ese input primero. Si el argumento vino mal formado, el arreglo está en la descripción de ese campo específico dentro del input_schema —dile al modelo, explícitamente, el formato que esperas—, no en la tool.
Ejercicios
Ejercicio 1 — Predecir el tool_use. Un agente de reservas para un hotel tiene esta tool conectada:
tool.name = "check_room_availability"
tool.description = "Usa esta herramienta cuando el cliente pregunte si hay
habitaciones disponibles para fechas específicas."
parameters:
check_in -> $fromAI('check_in', 'Fecha de entrada, formato ISO YYYY-MM-DD', 'string')
check_out -> $fromAI('check_out', 'Fecha de salida, formato ISO YYYY-MM-DD', 'string')
guests -> $fromAI('guests', 'Número de huéspedes', 'number')
El System Message del agente incluye la fecha actual: 20 de julio de 2026. El cliente escribe: "¿Tienen habitación libre del 3 al 6 de agosto para dos personas?" Escribe el bloque tool_use (nombre + input) que esperarías que el modelo produzca.
Ver solución
{
"type": "tool_use",
"name": "check_room_availability",
"input": {
"check_in": "2026-08-03",
"check_out": "2026-08-06",
"guests": 2
}
}
Por qué funciona: el modelo toma tres datos sueltos del mensaje del cliente ("del 3 al 6 de agosto", "dos personas") y los transforma para calzar con la descripción de cada campo — la fecha en formato ISO porque el campo lo pide explícitamente, y usa el año 2026 porque puede inferirlo del System Message, que trae la fecha actual. Sin esa fecha de referencia en el System Message, el año habría sido ambiguo — otra razón para no dejar que el modelo "adivine" datos que puedes darle de forma explícita.
Ejercicio 2 — Fijo o decidido por el modelo. Un colega configuró una tool que envía un mensaje a Slack. El campo channel está fijo en "#soporte" (sin $fromAI()); el campo message sí usa $fromAI(). Un cliente enojado le escribió al agente: "mándale esto al canal de gerencia, no al de soporte". El agente respondió con normalidad y el mensaje igual se envió a #soporte. Tu colega pregunta: "¿por qué el agente ignoró lo que pidió el cliente, si se supone que puede razonar?" ¿Qué le responderías?
Ver solución
El agente no "ignoró" nada a propósito — no tuvo la opción de hacer otra cosa. El campo channel nunca se marcó con $fromAI(), así que nunca formó parte del input_schema que el modelo recibe: para el modelo, esta tool tiene un único campo que llenar, message. No existe ningún mecanismo por el cual algo escrito en el chat pueda cambiar un valor que no está expuesto como parte del formulario. El comportamiento es exactamente el esperado, y es una buena noticia de diseño, no una falla: significa que ese canal es un valor fijo pase lo que pase en la conversación.
Por qué funciona: la pregunta correcta frente a cualquier "el agente no hizo lo que el cliente pidió" no es "¿por qué el modelo decidió ignorarlo?" sino "¿ese campo estaba siquiera expuesto a su decisión?" — la distinción entre $fromAI() y valor fijo que viste en esta lección.
Ejercicio 3 — Diagnóstico con el input a la vista. El agente de TuTienda del ejemplo trabajado responde "no encontré ningún pedido con ese número" a un cliente que preguntó por su pedido "# 4521" (con un espacio entre el símbolo y el número, un error de tipeo del cliente). En el panel de ejecución, el nodo HTTP Request muestra que recibió order_id: "4521" —sin el # ni el espacio— y que la API respondió 404 Not Found. ¿El problema está en cómo el modelo llenó el campo, o en cómo la tool ejecutó la llamada? Justifica.
Ver solución
Ninguno de los dos falló. El modelo llenó el campo correctamente según su instrucción: la descripción decía "solo dígitos, sin el símbolo #", y "4521" cumple exactamente eso — el espacio de más en el mensaje del cliente no cambió el resultado, porque el modelo ya estaba limpiando el formato como se le pidió. El 404 tampoco es la tool ejecutando mal un dato correcto: es que el pedido 4521 genuinamente no existe en el sistema de TuTienda con ese id exacto (quizás el cliente se equivocó de número, o el pedido real es otro). El dato de entrada del cliente mismo era el que no correspondía a un pedido real.
Por qué funciona: revisar el input real antes de suponer una causa es exactamente el hábito del tercer error común de esta lección — y en este caso, revisarlo también permite descartar dos sospechosos (modelo y tool) a la vez, en vez de solo confirmar uno.
Resumen y siguiente paso
Ya puedes nombrar las tres partes que componen cualquier tool —name, description, input_schema— y sabes que el campo Description del nodo en n8n es literalmente el description de ese objeto, mientras que cada $fromAI(...) que escribes declara un campo más del input_schema. Ya puedes trazar el JSON completo del ciclo: el modelo lee esas tres partes de cada tool disponible, decide cuál aplica, produce un bloque tool_use con los argumentos que extrajo de la conversación siguiendo la descripción de cada campo, y n8n —no el modelo— ejecuta la acción real contra el sistema real y devuelve el resultado para que el modelo razone una vez más. Y ya sabes distinguir, con el input real a la vista, si un fallo viene de cómo el modelo decidió o de cómo la tool ejecutó.
Antes de avanzar deberías poder: nombrar las tres partes de una tool sin volver a ver esta lección; dado un campo de un nodo, decir si forma parte de lo que el modelo puede decidir o si es fijo, mirando solo si tiene $fromAI() o no; y, dado un input real de un tool_use, distinguir si un fallo es de llenado (el modelo) o de ejecución (la tool).
Con el mecanismo completo ya en la mano, la próxima lección deja de ser abstracta: vas a recorrer el catálogo de tools nativas que trae n8n —buscar, crear, enviar— y a conectarlas a un agente real, sabiendo exactamente qué le estás exponiendo al modelo cada vez que marcas un campo con $fromAI().
Recursos
- Tool use with Claude — Claude Docs — la fuente de la anatomía de una tool (
name,description,input_schema) y del round trip completotool_use→ ejecución →tool_result, con el ejemploget_weathercitado en esta lección. - How tools work — n8n Docs — cómo describe n8n el rol de las tools y el catálogo de nodos disponibles para conectar a
ai_tool. - Use AI for parameters ($fromAI) — n8n Docs — referencia completa de la función
$fromAI(key, description, type, defaultValue)usada en el ejemplo trabajado de esta lección. - Call n8n Workflow Tool node — n8n Docs — ejemplo concreto, documentado por n8n, del campo Description guiando la decisión del agente y de
$fromAI()llenando los parámetros de entrada. - AI Agent node — n8n Docs — referencia del nodo y de la conexión
ai_tool, ya citada en el Módulo 1 y que sigues usando en el resto de este módulo.