Herramientas: exponer funciones reales al modelo
Una herramienta es la única puerta por la que un modelo puede tocar el mundo, y por eso es a la vez la pieza más poderosa y la más peligrosa del agente. El modelo nunca ejecuta nada: emite la intención de llamar a una función con unos argumentos, y es tu runtime quien valida, autoriza y ejecuta. Vemos qué ve exactamente el modelo cuando le describes una herramienta, por qué el esquema de argumentos es a la vez documentación y frontera de confianza, cómo se convierte un argumento validado en un efecto real dentro del agente y por qué la autorización jamás puede delegarse en quien pide la llamada.
Un modelo sin herramientas es un consejero elocuente encerrado en una habitación: sabe describir con precisión cómo se emite un reembolso, pero no puede emitir ninguno. La herramienta es la puerta de esa habitación, y ponerla cambia la naturaleza del sistema por completo, porque a partir de ese momento un texto generado estadísticamente puede convertirse en un cargo en una tarjeta, un correo enviado o una fila borrada. Conviene entonces mirar la puerta con cuidado. El modelo no ejecuta nunca nada: lo único que hace es pedir, con un nombre y unos argumentos. Todo lo que ocurre después —validar, autorizar, ejecutar, contar lo que pasó— sigue siendo tuyo, y es exactamente ahí donde se decide si tienes un agente útil o un desastre con buena prosa.
- Entender qué recibe el modelo de una herramienta y por qué su descripción es parte del prompt.
- Escribir el esquema de argumentos como contrato ejecutable y como frontera de validación.
- Definir herramientas dentro del agente para que accedan a su estado, su SQLite y sus bindings.
- Separar la intención del modelo de la autorización real y evitar el problema del delegado confundido.
Lo que el modelo ve de una herramienta
Cuando declaras una herramienta, al modelo no le llega tu código: le llega una ficha. Un nombre, una descripción en prosa y un esquema de argumentos traducido a JSON Schema. Con eso, y solo con eso, tiene que decidir si esa función sirve para lo que le han pedido y qué valores poner en cada campo. La consecuencia práctica desconcierta al principio: la descripción de una herramienta no es un comentario, es prompt, y se escribe con el mismo cuidado con el que escribirías la instrucción del sistema.
Una buena ficha dice para qué sirve la herramienta, qué devuelve y —esto es lo que casi todo el mundo olvida— cuándo no debe usarse. Los agentes que se comportan de forma errática rara vez tienen un problema de modelo: tienen dos herramientas cuyas descripciones se solapan, o una descripción que promete más de lo que la función hace.
import { tool } from "ai";
import { z } from "zod";
const buscarPedido = tool({
description:
"Busca un pedido del cliente autenticado por su codigo. Devuelve estado y fecha. " +
"No sirve para modificar el pedido ni para buscar por nombre de cliente.",
inputSchema: z.object({
codigo: z
.string()
.regex(/^[A-Z]-\d{4}$/)
.describe("Codigo del pedido, con el formato A-1001"),
}),
execute: async ({ codigo }) => {
const pedido = await consultarPedido(codigo);
return pedido ?? { error: "no_encontrado", codigo };
},
});
Fíjate en el describe de cada campo: viaja al modelo junto con el tipo y reduce drásticamente los argumentos mal formados. Y fíjate en el retorno del caso fallido. Un agente no espera excepciones, espera observaciones: si devuelves un objeto que dice qué salió mal, el modelo puede corregir el rumbo —pedir el código correcto, probar otra herramienta— en lugar de romper el bucle entero.
| El modelo aporta | El runtime aporta |
|---|---|
| El nombre de la herramienta elegida | La comprobación de que esa herramienta existe y está activa |
| Unos argumentos propuestos en JSON | La validación del esquema antes de ejecutar nada |
| Una intención derivada del texto | La autorización real contra la identidad del usuario |
| Nada más | El efecto, su registro y el resultado devuelto al bucle |
El esquema es una frontera de confianza
Es tentador leer el esquema como una comodidad de tipado. No lo es: es el punto donde entra en tu sistema una cadena generada por un proceso estadístico que ha leído, entre otras cosas, texto escrito por terceros. Los argumentos de una llamada a herramienta son entrada no confiable, con el mismo estatus que el cuerpo de una petición HTTP anónima, y merecen el mismo trato.
Por eso el esquema debe ser lo más estrecho posible, no lo más permisivo. Un identificador con su formato exacto en lugar de una cadena libre. Un importe con mínimo y máximo en lugar de un número. Una enumeración cerrada en lugar de un texto que luego interpretas. Cada restricción que expresas en el esquema es una clase entera de errores que el modelo ya no puede cometer, y además le informa: al ver el máximo, deja de proponer valores fuera de rango.
Estrecho antes que cómodo
Formatos, rangos y enumeraciones cerradas. Lo que no está en el esquema, no puede llegar a tu función.
Descripción por campo
Cada describe es contexto para el modelo y documentación para ti. Sale casi gratis y evita la mitad de los fallos.
Errores como datos
Devuelve objetos de error legibles en vez de lanzar. El modelo los lee como una observación y reintenta con criterio.
Autorización aparte
El esquema valida la forma, nunca el permiso. Quién puede hacer qué se comprueba dentro de execute.
Hay un corolario que ahorra muchas discusiones de diseño: si una restricción se puede expresar en el esquema, no la escribas en la descripción. Pedirle al modelo en prosa que no supere los doscientos euros funciona la mayoría de las veces, y esa mayoría es precisamente el problema, porque produce un sistema que parece correcto hasta el día que no lo es. Un máximo declarado en el esquema no falla nunca, porque no depende de que nadie lo interprete.
Del argumento al efecto, dentro del agente
Una herramienta suelta es una función; una herramienta definida dentro del agente es otra cosa, porque el this del método le da acceso a todo lo que el agente es: sus bindings en this.env, su estado en this.state y su base embebida a través de this.sql. Ahí es donde la herramienta deja de ser genérica y se vuelve la herramienta de este usuario, con esta memoria y este presupuesto.
export class AgenteSoporte extends Agent<Env, Estado> {
private herramientas() {
return {
emitirReembolso: tool({
description: "Emite un reembolso sobre un pedido del cliente de esta conversacion.",
inputSchema: z.object({
pedido: z.string().regex(/^[A-Z]-\d{4}$/),
importe: z.number().positive().max(200),
motivo: z.enum(["defectuoso", "no_recibido", "duplicado"]),
}),
needsApproval: async ({ importe }) => importe > 50,
execute: async ({ pedido, importe, motivo }) => {
const duenos = this.sql`SELECT cliente FROM pedidos WHERE id = ${pedido}`;
if (duenos[0]?.cliente !== this.state.clienteId) {
return { error: "pedido_ajeno" };
}
const recibo = await this.env.PAGOS.reembolsar(pedido, importe, motivo);
this.setState({ ...this.state, euros: this.state.euros + importe });
return { ok: true, recibo };
},
}),
};
}
}
Hay tres decisiones cargadas de intención en esas pocas líneas. La primera es que la identidad del cliente no es un argumento: se lee del estado del agente, donde la puso el proceso de autenticación. Si fuese un argumento, el modelo podría proponer cualquier valor, y bastaría con que un texto malicioso dentro de un ticket le sugiriese otro identificador para que reembolsase pedidos ajenos. La segunda es la comprobación de propiedad antes del efecto: el esquema garantiza que el código tiene forma de código, jamás que sea tuyo. La tercera es needsApproval, que detiene la ejecución y pide confirmación humana cuando el importe pasa de un umbral; lo trataremos a fondo en la última lección, pero fíjate en que es una propiedad de la herramienta, no del modelo.
sequenceDiagram participant M as modelo participant A as agente participant X as sistema externo M->>A: quiero emitirReembolso con estos argumentos A->>A: valida el esquema A->>A: comprueba que el pedido es del cliente A->>X: ejecuta el reembolso X-->>A: recibo A-->>M: resultado como observacion nueva
Ese diagrama contiene toda la doctrina de la lección. Entre la intención del modelo y el efecto en el mundo hay dos guardias que no puedes saltarte, y ninguno de los dos es el modelo. Cuando además conectas herramientas de terceros por MCP, la regla se vuelve más urgente, no menos: una herramienta remota es código que no escribiste, invocada por un componente que no controlas, así que la frontera de confianza sigue estando en tu agente y en ningún otro sitio.
Herramientas que no escribiste tú
La clase Agent trae cliente de MCP, así que un agente puede conectarse a servidores externos y sumar sus herramientas a las propias. Es una capacidad enorme: el catálogo deja de ser lo que tú programaste y pasa a incluir lo que ofrece un tercero, sin desplegar nada. Y es exactamente por eso que hay que tratarla con una desconfianza deliberada.
Piensa en qué has aceptado al conectar un servidor remoto. Primero, que el catálogo puede cambiar sin que tú te enteres: una herramienta nueva aparece mañana y tu agente puede llamarla. Segundo, y más grave, que las descripciones de esas herramientas —el texto que viaja al modelo y que influye en sus decisiones— las escribe alguien que no eres tú. Una descripción es prompt; un prompt que controla un tercero es una vía de influencia sobre tu agente, y no una vía teórica.
Las mitigaciones son las mismas de siempre, aplicadas con disciplina. Fija una lista blanca de herramientas activas por turno en lugar de aceptar el catálogo entero. Revisa las descripciones que llegan igual que revisarías una dependencia nueva. No mezcles en el mismo agente herramientas remotas y herramientas con efectos irreversibles sobre tus sistemas, o si lo haces, exige aprobación humana en las segundas. Y registra qué herramienta remota se llamó y con qué argumentos, porque cuando algo raro ocurra esa traza será tu única prueba.
Aquí está el vértigo del asunto, y merece decirse con toda claridad porque casi nunca se dice. Al exponer herramientas no estás añadiendo funciones a una librería: estás definiendo el conjunto de instrucciones de una máquina virtual cuyo programa lo escribe, sobre la marcha, un texto en lenguaje natural que puede venir de cualquier parte. Piensa en lo que eso significa combinatoriamente. Con cinco herramientas y ocho pasos de bucle, el espacio de programas posibles ya se cuenta por decenas de miles, y ninguno de ellos está escrito en tu repositorio ni ha pasado por revisión. Tú no auditas el programa, auditas el conjunto de instrucciones y confías en que ninguna composición de ellas sea catastrófica. De ahí se deriva la única disciplina que funciona: no preguntes si una herramienta es segura, pregunta qué se puede componer con ella. Una herramienta que lee correos es inocua; una que envía correos es inocua; juntas son un exfiltrador de datos esperando a que alguien escriba la instrucción adecuada dentro de un correo entrante, porque el modelo no distingue entre el texto que le da su dueño y el texto que encuentra al leer una fuente. Ese es el corazón de la inyección de prompt indirecta, y no se arregla con mejores instrucciones del sistema —a un intérprete no se le pide amablemente que ignore ciertas entradas—, se arregla en la arquitectura: minimizar el conjunto de instrucciones, estrechar los esquemas hasta que solo quepa lo legítimo, autorizar cada efecto contra la identidad real del usuario y no contra la intención del modelo, y exigir confirmación humana justo en la frontera donde una acción se vuelve irreversible. El ingeniero maduro diseña su catálogo de herramientas como quien diseña un lenguaje: sabiendo que la expresividad que concede es exactamente el daño que autoriza, y que quitar una herramienta suele mejorar más el sistema que añadir tres.
- Escribe una herramienta con
tooly un esquema de Zod que incluya formato, rango y enumeración. Comprueba qué recibe el modelo y qué rechaza el validador. - Reescribe su descripción añadiendo qué devuelve y cuándo no debe usarse. Mide si cambia el comportamiento del agente ante una petición ambigua.
- Provoca un fallo real —recurso inexistente— y devuélvelo como objeto de error en vez de lanzar. Observa cómo reacciona el bucle en el siguiente paso.
- Mueve la identidad del usuario del esquema al estado del agente y añade la comprobación de propiedad dentro de
execute. Explica qué ataque acabas de cerrar. - Enumera todas las parejas de herramientas de tu catálogo y busca la composición peligrosa. Decide cuál de las dos recorta, y cuál pasa a exigir aprobación.