Agent Mentor Learn
Llamada a herramientas en agentes: conseguir que los agentes hagan cosas de verdad · Lección 4 de 6

Lección 4: Diseñar interfaces de herramientas: nombre, descripción, parámetros, valor de retorno

Objetivos de aprendizaje:

  • Juzgar si la descripción de una herramienta le da al modelo lo suficiente para elegir la herramienta correcta y rellenar los parámetros correctos
  • Usar enum y required de JSON Schema para cerrar el margen de uso indebido de los parámetros, y saber cuándo usar el modo strict para convertir esas restricciones en garantías firmes
  • Diseñar valores de retorno y mensajes de error sobre los que el modelo pueda actuar para corregirse solo

Requisitos: Terminaste la Lección 3 y conoces la diferencia entre los cinco tipos de herramientas: read / write / execute / search / call | Anterior: Lección 3 << | Siguiente: Lección 5 >>

Una herramienta, dos descripciones, dos resultados

Supongamos que tu caja de herramientas tiene una herramienta de búsqueda de código. Esta es la primera versión de cómo está registrada:

El usuario pregunta: «¿En qué directorio está utils.ts?».

Lo único que el modelo tiene a mano son esas dos líneas: el nombre y la descripción. No hay forma de que distinga si search_files busca archivos por nombre o busca una cadena dentro del contenido de los archivos; la descripción no lo dice. El modelo elige esta herramienta y pasa utils.ts como query:

Si esta herramienta en realidad hace búsqueda de texto completo (busca la cadena utils.ts dentro del contenido de cada archivo), y el contenido de ningún archivo contiene literalmente esos caracteres, el resultado vuelve vacío. El modelo recibe un resultado vacío y no puede distinguir si el archivo no existe o si su enfoque de búsqueda era el equivocado, así que adivina. Lo habitual es que pruebe unos cuantos sinónimos y busque otra vez, y siga recibiendo resultados vacíos.

Ahora cambia la descripción por esta:

Misma pregunta, pero esta vez el modelo lee «para encontrar archivos por su nombre, usa code_search_glob» y cambia directamente a code_search_glob, que está registrada en la misma caja de herramientas, pasando el parámetro correcto:

Entre las dos llamadas no cambió nada: mismo modelo, mismo prompt, ningún cambio en el código de implementación. La única diferencia son esas pocas líneas que el modelo puede leer en la definición de la herramienta: un nombre más preciso, una descripción que deja clara la frontera y nombra la herramienta alternativa, y parámetros con sus propias descripciones. De eso trata esta lección: cada campo de la interfaz de una herramienta es lo único con lo que el modelo puede razonar cuando toma una decisión.

La descripción es todo lo que el modelo ve al elegir una herramienta

Quienes desarrollan tienden a escribir herramientas como escriben comentarios de una API: le ponen a la función un nombre con sentido, meten la lógica en el cuerpo y dejan que quien la necesite lea el código fuente. Ese hábito se rompe con las definiciones de herramientas: el modelo no lee tu código de implementación. Todo lo que ve son los campos name, description e input_schema1; qué herramienta elegir y qué parámetros pasar se reduce por completo a esas pocas líneas.

El requisito oficial para una description es directo: debería ser "A detailed plaintext description of what the tool does, when it should be used, and how it behaves."1 (una descripción detallada en texto plano de qué hace la herramienta, cuándo debería usarse y cómo se comporta). Si falta cualquiera de esas tres cosas, el modelo tiene que adivinar. Si falta «qué hace», el modelo puede saltarse la herramienta por completo y tomar un camino más largo para fingir el resultado. Si falta «cuándo usarla» y la caja de herramientas contiene varias parecidas (por ejemplo, un grep y un glob a la vez), el modelo no puede saber dónde está la frontera, y sus probabilidades de elegir mal suben con el número de herramientas. Si falta «cómo se comporta», el modelo no sabe qué forma tendrá el resultado que reciba, así que no puede escribir la lógica posterior correcta para interpretarlo.

Una buena descripción debería "Avoid ambiguity by clearly describing (and enforcing with strict data models) expected inputs and outputs."2 (evitar la ambigüedad describiendo con claridad las entradas y salidas esperadas) en lugar de buscar una redacción elegante. La description de code_search_grep de la sección anterior funciona porque hace dos cosas: deja claro que busca en el contenido y no en los nombres de archivo, y nombra code_search_glob como la herramienta para encontrar archivos por nombre. Esas dos frases le permiten al modelo elegir entre herramientas parecidas sin ensayo y error.

Los nombres también deberían señalar a quién pertenecen: namespacing

El trabajo de la descripción es detallar qué hace la herramienta; el del nombre es otro: evitar que la herramienta se confunda con otra dentro de una caja abarrotada. En cuanto tienes muchas herramientas, sobre todo después de conectar varios servicios externos, nombres como list_prs, send_message o create_issue son nombres que cualquiera podría elegir, y el nombre por sí solo no dice a qué servicio pertenecen.

El consejo oficial es prefijar los nombres de las herramientas con el servicio: "When your tools span multiple services or resources, prefix names with the service (e.g., github_list_prs, slack_send_message). This makes tool selection unambiguous as your library grows, and is especially important when using tool search."3 (cuando tus herramientas abarcan varios servicios o recursos, prefija los nombres con el servicio). Cuando el modelo tiene que elegir una herramienta entre decenas, un nombre con prefijo estrecha el campo de entrada, de modo que puede descartar la mayoría de las opciones sin abrir cada descripción para compararlas línea a línea. Los cinco tipos de herramientas de la Lección 3 (read, write, execute, search, call) se benefician igual si cada uno se apoya en un servicio distinto: fs_read_file y db_read_row a simple vista no son lo mismo, mientras que un read a secas los mezcla.

input_schema: fijar la forma de los parámetros

La descripción decide si el modelo elegirá esta herramienta; el input_schema decide si podrá rellenar los parámetros correctamente1. Aquí hay algo fácil de pasar por alto: en JSON Schema no todos los campos «restringen» un parámetro; algunos solo lo «describen».

Añadir una description a un parámetro solo declara la intención; no rechazará ninguna entrada que no coincida con lo que dice la frase4:

El modelo podría pasar "typescript", podría pasar "ts", podría pasar "archivos TypeScript": la description es solo una sugerencia y nada le impide pasar cualquier cosa. Lo que de verdad frena los valores arbitrarios es enum:

Con enum en su sitio, los valores legales quedan listados de forma explícita, el modelo casi siempre rellena uno de ellos y las probabilidades de un valor arbitrario caen en picado. Pero ojo: esto es una guía fuerte para el modelo, no una garantía firme de la plataforma. En el modo por defecto la API no valida los parámetros contra el esquema por ti, y el modelo seguirá produciendo de vez en cuando entradas con tipos incorrectos o sin campos required5, así que las comprobaciones de valores inválidos en la implementación de tu herramienta tienen que seguir ahí. Lo mismo con required: si una herramienta de «escribir archivo» no marca path como required, el modelo lo omitirá de vez en cuando, y entonces la implementación tiene que fallar con un error o adivinar una ruta por defecto, y ninguna de las dos opciones es buena. Marcar path como required deja ese tipo de uso indebido en probabilidades muy bajas, y que «required» lo imponga de verdad la plataforma depende del modo strict de la sección siguiente.

Recuerda esta distinción: type, enum y required son restricciones reales en términos de validación, mientras que title y description son solo notas para el modelo; por muy detalladas que sean, no constituyen una regla de validación4. Cuando diseñes un input_schema, pregúntate primero: ¿las «entradas que no deberían aparecer» en este parámetro se pueden bloquear directamente con enum o required, en lugar de limitarse a escribir «pasa por favor xxx» en la description? Cómo elevar estas restricciones de «escritas en el esquema» a «impuestas por la plataforma» es la sección siguiente.

Convertir restricciones blandas en garantías firmes: additionalProperties: false y el modo strict

La sección anterior insistió en la diferencia entre «restringir» y «describir», pero hay otra capa que conviene tener clara: escribir una restricción en el esquema y que los parámetros producidos por el modelo pasen realmente la validación siguen siendo dos cosas distintas. En el modo por defecto, la API no interceptará por ti una llamada que no coincide con el esquema: el modelo escribirá de vez en cuando un número como la cadena "2", o directamente omitirá un campo required5.

Hay también una dirección más sutil: el modelo puede añadir campos de la nada. Supongamos que el esquema de una herramienta para crear tickets declara solo dos parámetros, title y priority, pero una llamada vuelve así:

Esa clave skip_review nunca estuvo en las properties del esquema; el modelo se la inventó por su cuenta. El comportamiento por defecto de JSON Schema estándar es justamente permitir que un objeto lleve claves extra no declaradas, y si la implementación de tu herramienta resulta que pasa la entrada entera a un sistema aguas abajo, y el código de ese sistema tiene de verdad una rama que comprueba ese nombre de campo, una sola alucinación del modelo se salta en silencio un paso de revisión que tenía que ocurrir. Añadir "additionalProperties": false en la parte superior del input_schema escribe también «solo se permiten las claves declaradas» en las reglas de validación.

Para que la plataforma imponga todo esto de verdad, añade el campo de nivel superior "strict": true a la definición de la herramienta. El modo strict funciona restringiendo el propio muestreo del modelo: "Setting strict: true on a tool definition guarantees Claude's tool inputs match your JSON Schema by constraining the model's token sampling to schema-valid outputs (a technique called grammar-constrained sampling)."5 (poner strict: true garantiza que las entradas de herramienta coincidan con tu JSON Schema, restringiendo el muestreo de tokens del modelo a salidas válidas según el esquema). Se respetan type, enum, required y additionalProperties, y los parámetros inválidos simplemente nunca llegan a generarse. En la documentación oficial, los esquemas de ejemplo del modo strict llevan además additionalProperties: false: los dos están pensados para usarse juntos. Solo en este punto se cumple de verdad que «los valores inválidos quedan descartados antes incluso de enviar la petición»; para una herramienta sin modo strict, no puedes quitar ni una línea de validación de parámetros del lado de la implementación.

Valores de retorno: dale al modelo algo que pueda usar después, no un log para humanos

Cuando una herramienta termina, su resultado se envuelve en un bloque tool_result y se devuelve al modelo. Los campos centrales son tool_use_id (a qué llamada corresponde), content (el resultado) e is_error (si falló)6. De los tres, el que más a menudo se escribe mal es el content en caso de fallo.

Supongamos que una herramienta de «escribir archivo» falla porque el directorio no existe. Dos formas de escribirlo:

Esto devuelve el log del sistema tal cual. El modelo puede ver que falló, pero no puede saber qué hacer a continuación; lo habitual es que repita exactamente la misma llamada, choque con el mismo error por segunda vez y caiga en un bucle.

El mismo fallo, pero esta versión le dice al modelo tres cosas: cuál fue el fallo, a qué herramienta puede llamar para arreglarlo y qué otra ruta tiene disponible. La especificación de MCP es explícita: "Clients SHOULD provide tool execution errors to language models to enable self-correction."7 (los clientes DEBERÍAN entregar los errores de ejecución de herramientas a los modelos de lenguaje para permitir la autocorrección), con la condición de que ese mensaje lleve por sí mismo las pistas necesarias para corregir, y no una traza de pila que solo entiende quien está depurando el código.

Cantidad y granularidad de herramientas: más no es mejor

Una caja de herramientas más grande no es una caja mejor. Cada definición de herramienta (name, description e input_schema juntos) tiene que empaquetarse en el contexto antes de que empiece la conversación, y en cuanto tienes muchas herramientas ese sobrecoste crece deprisa. El equipo de ingeniería de Anthropic dio una cifra: "That's 58 tools consuming approximately 55K tokens before the conversation even starts." (son 58 herramientas consumiendo unos 55K tokens antes siquiera de que empiece la conversación); tanto contexto quemado antes de que la conversación arranque de verdad. También han visto casos más extremos internamente: "At Anthropic, we've seen tool definitions consume 134K tokens before optimization."8 (hemos visto definiciones de herramientas consumir 134K tokens antes de optimizar). Cuanto más abarrotado está el contexto, menos espacio le queda al modelo para razonar sobre la tarea real.

El segundo problema que llega con un número alto de herramientas no tiene nada que ver con los tokens: elegir se vuelve más difícil. Acumula varias herramientas con funciones parecidas y el modelo tendrá que gastar un paso extra solo en «cuál uso», con las probabilidades de equivocarse subiendo junto con el número de herramientas: "More tools don't always lead to better outcomes."2 (más herramientas no siempre llevan a mejores resultados). Por eso también las secciones anteriores insistían en que una descripción tiene que dejar clara la frontera.

Lo contrario, una granularidad demasiado gruesa, tampoco funciona. Una herramienta de «operaciones de archivo» que mete leer, escribir, borrar y editar en un solo input_schema y distingue el comportamiento con un parámetro action obliga al modelo a adivinar primero el valor correcto de action y después qué parámetros rellenar: más propenso a errores que dividirla en herramientas de responsabilidad única como fs_read_file y fs_write_file. El compromiso práctico: divide primero las herramientas siguiendo los cinco tipos de la Lección 3 y después, cuando el número crezca, controla las probabilidades de una mala elección con namespacing y descripciones precisas, en lugar de apilar una herramienta que lo hace todo para mantener el número bajo.

Resumen

  • La descripción es el único texto que el modelo ve cuando elige una herramienta y rellena parámetros; detallar «qué hace, cuándo usarla, cuándo no» importa más que escribirla con elegancia
  • Añadir un prefijo de servicio (namespacing) al nombre ayuda al modelo a descartar de entrada un buen puñado de opciones irrelevantes, cuando hay muchas herramientas
  • En el input_schema, type, enum y required son restricciones reales en términos de validación mientras que title y description son solo notas; enum más required deja las probabilidades de valores arbitrarios muy bajas, y convertir «las entradas inválidas simplemente nunca se generan» en una garantía firme requiere additionalProperties: false más el modo strict5
  • Los valores de retorno, sobre todo en caso de fallo, necesitan detallar «por qué falló» y «qué hacer a continuación» para que el modelo pueda corregirse solo en lugar de reintentar sin cambios
  • Más herramientas no es mejor: las definiciones consumen tokens de contexto, y cuanto más parecidas son las herramientas más fácil es que el modelo elija mal; la granularidad tampoco es «cuanto más fina mejor»: divide primero por función y después controla las probabilidades de una mala elección con nombres y descripciones claros

>> Lección 5: Permisos y seguridad: los límites de lo que un agente puede hacer

Footnotes

  1. Define tools — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools 2 3

  2. Writing effective tools for AI agents—using AI agents | Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents 2

  3. How to implement tool use - Claude Platform Docs — https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use

  4. Creating your first schema - JSON Schema — https://json-schema.org/learn/getting-started-step-by-step 2

  5. Strict tool use — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use 2 3 4

  6. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls

  7. Tools - Model Context Protocol — https://modelcontextprotocol.io/docs/concepts/tools

  8. Introducing advanced tool use on the Claude Developer Platform | Anthropic Engineering — https://www.anthropic.com/engineering/advanced-tool-use

Ejercicios

01

Un proyecto tiene una herramienta de «escribir archivo», definida ahora mismo así:

Nivel 1: Reescribe una definición de herramienta vaga

La caja de herramientas también tiene una herramienta edit_file que hace una sola cosa: reemplazos locales dentro de un archivo existente. El modelo llama a menudo a write_file cuando debería llamar a edit_file para un cambio pequeño, y sobrescribe el archivo entero.

Reescribe la description y el input_schema de write_file para que:

  1. La description deje claro que esta herramienta sobrescribe por completo el contenido del archivo, y apunte a edit_file para cambios locales
  2. El input_schema añada un parámetro restringido por enum que distinga «crear cuando el archivo no existe» de «sobrescribir cuando el archivo ya existe», para que el modelo no sobrescriba sin querer un archivo que no debería tocar
  3. Comprobación: si al modelo le llega «cambia el número de puerto en config.json a 8080», ¿seguiría echando mano de write_file?
Criterios de finalización · marcado local
02

Abajo hay un registro de ida y vuelta simplificado pero real. Una herramienta de «ejecutar tests» fue llamada 3 veces seguidas, con entradas idénticas cada vez:

Nivel 2: Diagnostica una llamada que falló por un mal valor de retorno
Llamada 1: { "name": "run_tests", "input": { "suite": "unit" } }Devuelve: { "content": "Error: connect ECONNREFUSED 127.0.0.1:5432", "is_error": true }
Llamada 2: { "name": "run_tests", "input": { "suite": "unit" } }Devuelve: { "content": "Error: connect ECONNREFUSED 127.0.0.1:5432", "is_error": true }
Llamada 3: { "name": "run_tests", "input": { "suite": "unit" } }Devuelve: { "content": "Error: connect ECONNREFUSED 127.0.0.1:5432", "is_error": true }

Responde a esto:

  1. ¿Por qué el modelo repite la llamada 3 veces con parámetros idénticos en lugar de probar algo distinto?
  2. ¿Qué necesita hacer el modelo para resolver la causa real (la conexión a la base de datos fue rechazada; no hay nada escuchando en el puerto 5432)? Supón que la caja de herramientas también tiene una herramienta start_service.
  3. Reescribe el campo content como un mensaje de error que llevaría al modelo a cambiar de enfoque antes de la segunda llamada.
Criterios de finalización · marcado local