El agente que nunca terminaba
Un agente de IA es un bucle en el que un modelo elige su propio paso siguiente. Como nada le dice cuándo rendirse, necesita un plan y una regla para parar.
Un agente de soporte repite toda la noche una consulta que falla. Cada intento cuesta dinero y ninguno puede salir bien. El bucle era correcto. Faltaba la regla de parada.
Al final de esta lección
Nota
Puedo hacer que un agente planifique sus pasos y pare según una regla.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Agente | un bucle en el que un modelo elige el paso siguiente | |
| Plan | los pasos que un agente piensa dar, como datos | |
| Presupuesto de pasos | el máximo de pasos de una ejecución | 3 |
| Regla de parada | una prueba que termina el bucle | |
| Resultado | el motivo por el que acabó una ejecución |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un plan es un dato, no una intuición
Pide al modelo un plan como lista JSON y trátalo como un dato. Tu programa puede contarlo, comprobar cada paso y enseñárselo a una persona antes de ejecutar nada.
Un plan es una propuesta. El modelo puede devolver una lista con mala forma o un paso que ninguna herramienta sabe hacer. Comprueba el plan antes de seguirlo.
Ejemplo resuelto: un plan y su ejecución
| Paso | El plan dice | Resultado de la herramienta | Estado |
|---|---|---|---|
| 1 | buscar pan | pan cuesta 90 céntimos | ok |
| 2 | buscar leche | leche cuesta 110 céntimos | ok |
| 3 | buscar kiwi | ninguna herramienta sirve | fallo |
| 4 | decir los precios | no se llega | decide el presupuesto o el atasco |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Escribir la ayuda del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la ayuda del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el agente planificador, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const PRICES = { pan: 90, leche: 110, naranja: 60 };' 'const messages = [' ' { role: \"system\", content: \"Respondes solo con JSON.\" },' ' { role: \"user\", content: \"Planifica cómo averiguar el precio del pan, de la leche y del kiwi. Responde con un array JSON de 3 cadenas cortas, un producto por cadena. Responde solo con el JSON.\" },' '];' '// Muestra grabada, usada solo si ningún modelo responde.' 'const sample = JSON.stringify([\"pan\", \"leche\", \"kiwi\"]);' > plan1.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el agente planificador, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function step(text) {' ' const key = Object.keys(PRICES).find(function (k) { return text.toLowerCase().indexOf(k) >= 0; });' ' if (key === undefined) return { ok: false, note: \"ninguna herramienta sirve: \" + text };' ' return { ok: true, note: key + \" cuesta \" + PRICES[key] + \" céntimos\" };' '}' >> plan1.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el agente planificador, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const reply = await ask(messages, sample, 100);' ' let plan = null;' ' try { plan = JSON.parse(reply.slice(reply.indexOf(\"[\"), reply.lastIndexOf(\"]\") + 1)); } catch (error) { }' ' if (Array.isArray(plan) === false) { console.log(\"No es una lista, uso un plan fijo.\"); plan = [\"pan\", \"leche\"]; }' ' const budget = 3, maxFails = 2;' ' let used = 0, fails = 0, outcome = \"terminado\";' ' for (const text of plan) {' ' if (used === budget) { outcome = \"presupuesto\"; break; }' ' used += 1;' ' const r = step(String(text));' ' fails = r.ok ? 0 : fails + 1;' ' console.log(\"Paso \" + used + \": \" + r.note);' ' if (fails === maxFails) { outcome = \"atascado\"; break; }' ' }' ' console.log(\"Resultado: \" + outcome + \" tras \" + used + \" pasos\");' '}' 'main();' >> plan1.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecutar el agente y leer el resultado
docker exec -w /home/node/project agent node plan1.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una salida posible
| Línea | Qué puede mostrar |
|---|---|
| Pasos 1 y 2 | Los precios del pan y de la leche |
| Paso 3 | ninguna herramienta sirve: kiwi |
| Resultado | terminado tras 3 pasos: el plan se acabó y un solo fallo no basta para quedar atascado |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Tres formas de acabar una ejecución
Una ejecución puede acabar porque el trabajo está terminado, porque el agente está atascado o porque se agotó el presupuesto. Cada resultado es un mensaje distinto para el usuario y una línea distinta en el registro.
Compruébalos en ese orden. Una ejecución que acaba en su último paso permitido está terminada, no fuera de presupuesto. Atascado significa fallos repetidos sin avance.
Un error frecuente
Advertencia
Mucha gente cree que un presupuesto basta como regla de parada. Un presupuesto solo limita el coste. No distingue una ejecución acabada de una atascada. Deja el presupuesto como último recurso y añade reglas para terminado y atascado.