Lectura: unos 20 minutos
Un agente es tan bueno como sus herramientas, y eso no es una frase bonita: es literalmente dónde está el trabajo. El bucle de la lección anterior son treinta líneas y no se toca nunca más. Todo lo que vas a escribir a partir de ahora son herramientas, y todo lo que se te va a estropear también.
Lo que hay dentro
- Las seis reglas de una descripción que funciona
- El esquema de parámetros, campo a campo
- Qué devuelve: forma, tamaño y errores
- Herramientas que ESCRIBEN: el contrato de las cuatro condiciones
- Idempotencia, con código
- Preparar y confirmar: por qué son dos herramientas
- Los límites van en el código, nunca en el prompt
- Cuántas herramientas caben
- Cuando la herramienta llama a otro sistema
- El alcance: lo que la herramienta puede ver
- El agente de pedidos, entero
- Probar una herramienta sin gastar ni una llamada
1. Las seis reglas de una descripción que funciona
Todas salen de lo mismo: el modelo elige con lo que lee. Una descripción no es documentación para ti — es la interfaz.
-
Di cuándo usarla, no sólo qué hace
«Devuelve el stock de un producto» describe. «Úsala cuando te pregunten cuánto queda de algo o si hace falta pedirlo» decide. La segunda mitad es la que el modelo necesita para elegir entre cinco herramientas parecidas.
-
Di de dónde salen los argumentos
Si un campo tiene que venir de otra herramienta, dilo con esas palabras. Es lo que evita el argumento inventado, que es el fallo más frecuente de todos.
-
Di qué NO hace
«No incluye productos retirados» y «no sirve para consultar precios» ahorran vueltas enteras. Un modelo que no sabe que algo está fuera de alcance lo intenta igual.
-
Avisa de lo caro
«Sin filtro devuelve el catálogo entero; evítalo» funciona. Los modelos hacen caso a esas advertencias sorprendentemente bien, y te ahorran la factura de la lección 1.
-
Nombra la herramienta como un verbo
buscar_producto,situacion_stock,preparar_pedido. El nombre lo lee el modelo tantas veces como la descripción.herramienta_2oconsultaDatosGeneralle quitan información gratis. -
Escríbela corta
Se envía en cada vuelta, como las instrucciones. Cuatro frases buenas rinden más que quince líneas — y cuestan tres veces menos.
| Mal | Bien |
|---|---|
"Consulta el stock." |
"Stock, mínimo y días de cobertura de UN producto. Úsala cuando
pregunten cuánto queda o si hay que pedir. El código tiene que venir de
buscar_producto." |
"Envía un pedido al proveedor." |
"Envía DE VERDAD el pedido ya preparado. Sólo se puede llamar con el
identificador que devuelve preparar_pedido y después de que el usuario haya
dicho que sí. No la uses para calcular nada." |
2. El esquema de parámetros, campo a campo
El esquema no es burocracia: es lo único que impide que llegue basura a tu función. Y cada campo lleva su propia descripción, que también la lee el modelo.
{
name: 'preparar_pedido',
description: 'Calcula las lineas de un pedido para un proveedor y devuelve un borrador '
+ 'con un identificador. NO envia nada. Es el paso previo obligatorio de confirmar_pedido.',
parameters: {
type: 'object',
properties: {
proveedor: {
type: 'string',
// ⚠️ enum: la lista cerrada evita que invente un proveedor
enum: ['artsana', 'kenvue', 'johnson'],
description: 'Proveedor al que va el pedido',
},
cubrir_dias: {
type: 'integer',
description: 'Dias de venta que se quieren cubrir. Entre 3 y 30. Si no te lo dicen, 14',
},
solo_codigos: {
type: 'array',
items: { type: 'string' },
description: 'Limitar a estos codigos. Vacio = todo lo que este bajo minimo',
},
},
required: ['proveedor'],
},
}enum es la herramienta más infravalorada que existe.
Convierte «el modelo podría escribir cualquier cosa» en «el modelo sólo puede escribir una
de estas tres». Siempre que un campo tenga un número finito de valores válidos —un
proveedor, un estado, un tipo de documento— va con enum. Es gratis y elimina
una familia entera de fallos.
entre 3 y 30 no
impide que llegue un 900: eso es una frase, no una comprobación. La validación de verdad va
en la primera línea de tu función, y esto vale para todo lo que sigue.
3. Qué devuelve: forma, tamaño y errores
Lo que devuelve una herramienta entra en el contexto y se queda ahí durante el resto del bucle. Tres reglas.
3.1. Devuelve un objeto, nunca un texto suelto
// ❌ El modelo tiene que interpretar prosa
return 'Hay 12 unidades, por debajo del minimo de 30.';
// ✅ Datos, con sus nombres
return { stock: 12, minimo: 30, bajo_minimo: true, dias_de_cobertura: 3.8 };Con lo segundo, el modelo puede citar la cifra exacta y compararla. Con lo primero tiene que releer una frase y volver a extraer los números — que es justo el momento en el que aparece el resumen que no cuadra.
3.2. Devuelve poco, y avisa de lo que has recortado
const TOPE = 25;
if (filas.length > TOPE) {
return {
encontrado: true,
total: filas.length,
mostradas: TOPE,
// ⚠️ decirlo es obligatorio: si no, el modelo cree que son todas
nota: `Hay ${filas.length} filas; se muestran las ${TOPE} mas urgentes. Afina el filtro.`,
filas: filas.slice(0, TOPE),
};
}3.3. Los errores se devuelven, no se lanzan
Ya estaba en la lección anterior; aquí va con la forma exacta que conviene usar siempre:
return {
ok: false,
motivo: 'El proveedor pfizer no responde desde las 14:02.',
reintentable: true, // ¿tiene sentido volver a intentarlo?
alternativa: 'situacion_stock tiene el ultimo dato conocido, de esta manana.',
};alternativa es el campo que casi nadie pone y el que más cambia el
comportamiento. Sin él, el modelo se rinde o improvisa; con él, sabe exactamente
qué intentar después. Es la diferencia entre el agente del turno 4 de la lección 1 y uno
que contesta «no he podido» con dos herramientas sin usar.
4. Herramientas que ESCRIBEN: el contrato
Aquí cambia todo. Una herramienta que lee, si se equivoca, gasta medio céntimo. Una que escribe manda un pedido, cambia un precio o borra una línea. Cuatro condiciones, y no son opcionales ni son «buenas prácticas»: son las cuatro cosas sin las cuales esto no se pone en producción.
| Condición | Qué significa | Qué pasa sin ella |
|---|---|---|
| 1. Idempotente | Llamarla dos veces con lo mismo produce UN efecto | Dos pedidos reales. El fallo de la llamada doble |
| 2. Confirmada | Una persona dice que sí, viendo qué y por qué | El agente decide solo cosas que cuestan dinero |
| 3. Acotada | Topes duros en tu código: importe, cantidad, destinatario | Un cero de más pasa sin que nada chirríe |
| 4. Registrada | Queda escrito qué hizo, con qué argumentos y cuándo | No se puede auditar ni deshacer. Y habrá que hacerlo |
5. Idempotencia, con código
La palabra asusta y la idea es de primero: si esto se ejecuta dos veces, que pase una sola cosa. Se consigue con una clave que identifique la operación, no la llamada.
const enviados = new Map(); // en serio: una tabla, no memoria
function confirmar_pedido({ borrador_id }) {
// 1) ¿ya se envio este borrador? Entonces devolvemos lo de antes.
if (enviados.has(borrador_id)) {
const ya = enviados.get(borrador_id);
return { ok: true, duplicado: true, numero_pedido: ya.numero,
nota: 'Este pedido ya estaba enviado; no se ha enviado otro.' };
}
const borrador = borradores.get(borrador_id);
if (!borrador) return { ok: false, motivo: 'Ese borrador no existe o ha caducado.' };
if (!borrador.aprobado_por) return { ok: false, motivo: 'Nadie lo ha aprobado todavia.' };
const numero = enviarAlProveedor(borrador); // lo unico irreversible
enviados.set(borrador_id, { numero, cuando: new Date().toISOString() });
registrar('confirmar_pedido', { borrador_id, numero });
return { ok: true, numero_pedido: numero, lineas: borrador.lineas.length };
}ok: true. No es un
error: el estado que se pedía —«este pedido enviado»— ya se cumple. Devolver un error haría
que el modelo reintentase, que es exactamente lo contrario de lo que se busca.
duplicado: true está para el registro, no para asustar a nadie.
Map del ejemplo es de mentira. Vive en la memoria del
programa: se borra cada vez que el proceso arranca, que en un servidor de verdad es
constantemente. La deduplicación se guarda donde se guardan los datos —una tabla con el
borrador_id como clave única—, y esa restricción de unicidad es el candado
real. Todo lo demás es cortesía.
6. Preparar y confirmar: por qué son DOS herramientas
borrador_id que alguien ha aprobado. Fíjate en que la pantalla de aprobación sale gratis: es el borrador que ya devolvía la primera.
La tentación es una sola herramienta hacer_pedido que calcule y envíe.
Partirla en dos es la decisión de diseño más rentable de toda esta serie.
preparar_pedido | confirmar_pedido | |
|---|---|---|
| Qué hace | Calcula y guarda un borrador | Lo envía |
| ¿Es reversible? | Sí, del todo | No |
| ¿La puede llamar el agente solo? | Sí, las veces que quiera | Sólo con aprobación humana |
| Qué devuelve | Las líneas, el importe y un borrador_id |
El número de pedido |
Con esto, el agente puede trabajar libremente —preparar, recalcular, comparar tres proveedores, descartar— sin que nada salga de tu farmacia. Y lo único irreversible está detrás de un botón que pulsa una persona con las líneas delante.
7. Los límites van en el código, nunca en el prompt
Esto es lo que separa un juguete de algo que puede tocar tu negocio.
const TOPE_IMPORTE = 1500;
const TOPE_LINEAS = 40;
function preparar_pedido({ proveedor, cubrir_dias = 14, solo_codigos = [] }) {
// Validacion de verdad, no la del esquema
if (!['artsana', 'kenvue', 'johnson'].includes(proveedor))
return { ok: false, motivo: `Proveedor no valido: ${proveedor}` };
if (cubrir_dias < 3 || cubrir_dias > 30)
return { ok: false, motivo: 'cubrir_dias tiene que estar entre 3 y 30.' };
const lineas = calcularLineas(proveedor, cubrir_dias, solo_codigos);
const importe = lineas.reduce((s, l) => s + l.importe, 0);
// ⚠️ El tope NO deja pasar. No avisa: no deja.
if (importe > TOPE_IMPORTE || lineas.length > TOPE_LINEAS) {
return { ok: false, motivo: `El pedido sale a ${importe.toFixed(2)} EUR en `
+ `${lineas.length} lineas y supera el tope. Acota con solo_codigos o baja cubrir_dias.`,
reintentable: true };
}
const id = crypto.randomUUID();
borradores.set(id, { proveedor, lineas, importe, creado: Date.now() });
return { ok: true, borrador_id: id, proveedor, lineas, importe: +importe.toFixed(2) };
}if.
solo_codigos o baja
cubrir_dias» le da dos caminos concretos. Un rechazo bien escrito es
información, igual que un error.
8. Cuántas herramientas caben
Menos de las que crees. No hay un número mágico, pero sí un comportamiento muy claro:
| Cuántas | Qué pasa |
|---|---|
| 2 – 6 | Elige bien casi siempre. Es el rango cómodo |
| 7 – 12 | Empieza a confundir las parecidas. Los nombres y el «cuándo usarla» pasan a ser críticos |
| Más de 15 | Elige mal a menudo, y las descripciones ya pesan más que los datos en cada vuelta |
que de tipo
enum. El modelo elige entre cinco valores de un campo en vez de entre cinco
herramientas, que se le da bastante mejor — y las descripciones dejan de repetirse cinco
veces en cada vuelta.
8.1. Agrupar, con código
// ❌ Cinco declaraciones casi iguales, reenviadas en cada vuelta
consultar_stock · consultar_ventas · consultar_caducidades
consultar_precios · consultar_proveedores
// ✅ Una, con un enum
{
name: 'consultar',
description: 'Consulta un dato del sistema de gestion. Elige "que" segun lo que necesites. '
+ 'Siempre acotada: si no pasas codigo ni filtro, devuelve solo las 25 filas mas relevantes.',
parameters: {
type: 'object',
properties: {
que: { type: 'string', enum: ['stock', 'ventas', 'caducidades', 'precios', 'proveedores'],
description: 'Que se quiere consultar' },
codigo: { type: 'string', description: 'Codigo de producto, si aplica' },
desde: { type: 'string', description: 'Fecha ISO de inicio, para ventas y caducidades' },
},
required: ['que'],
},
}
Y en tu código, un switch de cinco ramas. Has cambiado «el modelo elige entre
cinco herramientas» por «el modelo elige un valor de una lista cerrada», que se le da mucho
mejor, y las descripciones han pasado de cinco párrafos a uno.
consultar(que:'stock') y consultar(que:'enviar_pedido')
sería una sola puerta para las dos cosas, y toda la frontera de la lección 1 —el humano
entre leer y escribir— se cae con ella. La agrupación es por parecido, no por
comodidad.
9. Cuando la herramienta llama a otro sistema
Leer un CSV no falla. Llamar a tu programa de gestión, al servicio del proveedor o al correo, sí — y falla de formas que no se parecen a un error de programación.
-
Pon un tiempo máximo. Siempre
Sin él, una llamada que no contesta deja el agente colgado indefinidamente y tú sin saber si va o no va. Diez o quince segundos, y si se pasa, se devuelve como dato:
{ ok: false, motivo: 'el servicio tardó más de 15 s', reintentable: true }.const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 15000); try { const r = await fetch(url, { signal: ctrl.signal }); return await r.json(); } catch (e) { return { ok: false, motivo: 'El servicio no respondio en 15 s.', reintentable: true }; } finally { clearTimeout(t); } -
Reintenta tú, no el modelo
Un fallo pasajero se reintenta dentro de tu función —dos veces, esperando un poco más cada vez— y el modelo ni se entera. Si lo dejas para el modelo, gastas una vuelta entera del bucle en algo que costaba doscientos milisegundos, y encima puede decidir hacer otra cosa.
Pero sólo lo que lee. Reintentar solo algo que escribe es la llamada doble con otro nombre: ahí el reintento va con la clave de idempotencia por delante o no va.
-
Distingue «no hay» de «no se sabe»
Si el sistema de caducidades no responde y devuelves una lista vacía, el modelo entenderá que no caduca nada. Es la mentira más fácil de cometer y la más difícil de ver:
{ ok: false }y{ ok: true, filas: [] }significan cosas opuestas y se escriben casi igual.
10. El alcance: lo que la herramienta puede ver
Una herramienta no debería poder hacer nada que no necesite para su trabajo. No es paranoia: es que el día que el modelo pida algo raro —o alguien se lo haga pedir— lo único que lo detiene es lo que la herramienta no puede hacer.
| En vez de | Haz |
|---|---|
| Una conexión a la base de datos con permisos totales | Un usuario de sólo lectura para las herramientas que leen, y otro distinto —con permiso sobre una tabla— para las que escriben |
Una herramienta ejecutar_sql |
Consultas concretas con parámetros. Nunca, jamás, SQL que venga del modelo |
Un enviar_correo(a, asunto, cuerpo) |
enviar_correo(plantilla, datos), con las plantillas escritas por
ti y el destinatario sacado de tu base, no del argumento |
| La clave del proveedor dentro del agente | La llamada al proveedor detrás de tu propio endpoint, que valida antes |
ejecutar_sql parece la más práctica y es la peor idea de
todas. Da al modelo una herramienta capaz de leer cualquier tabla —incluidas las
de pacientes— y, según el usuario, de borrarlas. Se justifica siempre igual: «así no tengo
que escribir una herramienta por consulta». Escríbelas.
11. El agente de pedidos, entero
Todo lo anterior junto, funcionando. Cuatro herramientas: consultar,
preparar_pedido, confirmar_pedido y nada más.
$ node agente.js "prepara el pedido de artsana para dos semanas"
vuelta 1 PIDE consultar {"que":"stock"}
vuelta 2 PIDE preparar_pedido {"proveedor":"artsana","cubrir_dias":14}
Borrador listo (id 8f2c...), 14 lineas, 612,40 EUR. Las tres mayores:
- OMEPRAZOL 20MG 28 CAPS: 70 uds (quedan 4, vendes 71/mes)
- IBUPROFENO 600MG 40 COMP: 90 uds (quedan 12, vendes 96/mes)
- ENANTYUM 25MG 20 COMP: 45 uds (sin stock, vendes 44/mes)
Revisa y aprueba en la pantalla de pedidos.
(3 vueltas)
El agente ha terminado ahí. No ha enviado nada y no puede:
confirmar_pedido comprueba borrador.aprobado_por y sin eso
devuelve el motivo. La aprobación no pasa por el agente — pasa por una pantalla tuya, con
las 14 líneas y el importe delante.
Y si le pides demasiado:
$ node agente.js "prepara el pedido de artsana para dos meses"
vuelta 1 PIDE preparar_pedido {"proveedor":"artsana","cubrir_dias":60}
vuelta 2 PIDE preparar_pedido {"proveedor":"artsana","cubrir_dias":30}
No he podido preparar 60 dias: el maximo son 30. Con 30 dias el pedido sale a
2.180,90 EUR y supera el tope de 1.500, asi que tampoco. Dime que productos
quieres cubrir o bajamos a 14 dias.
(3 vueltas)if dentro del código, ha entendido los motivos
porque estaban redactados, ha intentado una alternativa razonable y ha parado pidiendo una
decisión. Ni una línea de esa conversación estaba escrita de antemano — y aun así el
resultado está acotado por completo.
12. Probar una herramienta sin gastar ni una llamada
Una herramienta es una función normal, así que se prueba como una función normal: sin modelo, sin clave y sin gastar cuota. Esto es lo que hace que un agente se pueda mantener, y es la parte que casi nadie hace.
import { preparar_pedido, confirmar_pedido } from './agente.js';
import assert from 'node:assert';
// 1) Un proveedor inventado NO revienta: devuelve el motivo
const a = preparar_pedido({ proveedor: 'el_de_la_esquina' });
assert.equal(a.ok, false);
assert.match(a.motivo, /no valido/);
// 2) El tope de importe NO deja pasar
const b = preparar_pedido({ proveedor: 'artsana', cubrir_dias: 30 });
if (!b.ok) assert.match(b.motivo, /tope/);
// 3) Confirmar dos veces el mismo borrador envia UNA vez
const c = preparar_pedido({ proveedor: 'artsana', cubrir_dias: 7 });
aprobar(c.borrador_id);
const p1 = confirmar_pedido({ borrador_id: c.borrador_id });
const p2 = confirmar_pedido({ borrador_id: c.borrador_id });
assert.equal(p1.numero_pedido, p2.numero_pedido); // ⚠️ el mismo numero
assert.equal(p2.duplicado, true);
console.log('las tres pasan');12.1. Pégalo aquí
Las doce cosas que se comprueban abajo son las mismas doce que llevas leyendo desde el principio de la lección. Pégale un contrato —el de arriba, uno tuyo, o el que te acabe de escribir un modelo— y te dice cuáles no cumple. Se hace entero en tu navegador: no sale nada de aquí.
El contrato, en nueve líneas
- La descripción dice cuándo usarla, no sólo qué hace.
- Los campos con valores finitos van con
enum. - Los valores por defecto están escritos en la descripción del campo.
- Devuelve un objeto con nombres, no prosa.
- Si recorta, lo dice en el propio dato.
- Los errores se devuelven con
motivoy, si se puede,alternativa. - Si escribe: es idempotente, se confirma, está acotada y queda registrada.
- Los topes están en un
if, no en el prompt. - Hay una prueba que la ejecuta sin modelo.
En la última lección se saca todo esto del portátil: dónde vive, cuánto cuesta de verdad, cómo se vigila, qué es la inyección de prompt y qué pasa cuando el agente se equivoca en una farmacia.