Guardrails: comprobaciones antes y después del modelo
En resumen: Los guardrails son comprobaciones antes y después del modelo: ocultan los datos que el modelo no debe ver, miden la respuesta contra las reglas de la tienda y, ante la duda, pasan la conversación a una persona.
Problema
El asistente prometió 250 €
El cliente está furioso y escribe su número de tarjeta directamente en el chat. El asistente envía el mensaje al modelo y muestra la respuesta:
FakeModel model = new FakeModel();String message = "El pedido 1187 llegó roto. Pagué con la tarjeta 4111 1111 1111 1111, ¡quiero mi dinero!";System.out.println("cliente: " + message);String answer = model.create(SYSTEM, List.of(new Message("user", message)));System.out.println("respuesta: " + answer);cliente: El pedido 1187 llegó roto. Pagué con la tarjeta 4111 1111 1111 1111, ¡quiero mi dinero!respuesta: ¡Lo sentimos! Te devolveremos 250 € hoy mismo.Dos errores, sin una sola herramienta:
- El número de tarjeta fue al modelo y quedó en el registro del chat. El registro se guarda, se copia y lo leen los colegas.
- El pedido 1187 cuesta 89 €, y el asistente promete 250 €, «hoy mismo». El dinero solo lo devuelve un colega, y ahora el cliente tiene la promesa por escrito.
El modelo no es malicioso. Escribe un texto verosímil y cortés, y un cliente furioso que quiere su dinero suena a «devuélvele su dinero».
La idea
Comprobaciones alrededor del modelo
Los guardrails son código antes y después del modelo. No confían en el modelo, igual que un programa no confía en la entrada que viene de fuera:
- Una comprobación de entrada, antes del modelo. Quitas lo que el modelo no debe ver: un número de tarjeta, datos personales. Aquí también rechazas los mensajes demasiado largos que solo cuestan tokens.
- Una comprobación de salida, después del modelo. El borrador se mide contra las reglas de la tienda: sin promesas de dinero, solo pedidos de este cliente, ningún importe por encima del valor del pedido.
- Una persona en el bucle. Si una regla falla, el cliente recibe una respuesta segura y el borrador va a un colega.
- Un log. Cada decisión queda registrada: qué se ocultó, qué regla detuvo la respuesta. Sin los datos personales en sí.
Ante la duda, el programa detiene la respuesta en vez de dejarla pasar. Esto es fallar cerrado: mejor que el cliente espere un día a un colega que recibir una promesa que la tienda no puede cumplir. La misma regla está en la lección sobre entrada hostil.
Los guardrails no reemplazan las defensas contra prompt injection; las complementan. Ningún prompt detiene al modelo del todo, así que el código que lo rodea limita el daño.
Míralo
La comprobación de entrada
El número de tarjeta pasa a ser **** 1111, y el log dice por qué:
// Antes del modelo: la tarjeta no llega al modelo ni al log.static String checkInput(String raw, List<String> log) { String text = CARD.matcher(raw).replaceAll("**** $1"); if (text.equals(raw)) { log.add("entrada: ok"); } else { log.add("entrada: número de tarjeta oculto"); } return text;}$1 en replaceAll es el primer grupo entre paréntesis: los últimos cuatro dígitos.Los datos de la tienda y las reglas están al principio del programa:
static final String SYSTEM = "Eres el asistente de soporte de una tienda en línea. Responde con brevedad y solo sobre los pedidos del cliente.";static final String CUSTOMER = "C-17";static final String FALLBACK = "Pasé tu pregunta a un colega. Te escribirá en un día hábil.";static final Map<String, Order> ORDERS = Map.of( "1042", new Order("C-17", 45), "1187", new Order("C-17", 89), "2210", new Order("C-23", 120));static final List<String> MONEY = List.of("te devolveremos", "te reembolsaremos");static final Pattern CARD = Pattern.compile("\\d{4} \\d{4} \\d{4} (\\d{4})");static final Pattern ORDER_ID = Pattern.compile("(?<!\\d)\\d{4}(?!\\d)");static final Pattern AMOUNT = Pattern.compile("(\\d+) €");static final List<String> REVIEW = new ArrayList<>();Míralo
La comprobación de salida
El borrador del modelo contra las reglas. Cada infracción es una línea en una lista, y una lista vacía significa «pasó»:
static boolean isOwn(String id) { Order order = ORDERS.get(id); return order != null && order.customer().equals(CUSTOMER);}// Después del modelo: el borrador frente a las reglas de la tienda.static List<String> checkOutput(String draft, Order order) { List<String> problems = new ArrayList<>(); String lower = draft.toLowerCase(); for (String phrase : MONEY) { if (lower.contains(phrase)) { problems.add("promete dinero"); } } Matcher ids = ORDER_ID.matcher(draft); while (ids.find()) { String id = ids.group(); if (!isOwn(id)) { problems.add(id + " no es tu pedido"); } } int limit = order == null ? 0 : order.total(); Matcher sums = AMOUNT.matcher(draft); while (sums.find()) { int sum = Integer.parseInt(sums.group(1)); if (sum > limit) { problems.add(sum + " € > " + limit + " €"); } } return problems;}El importe se mide contra el pedido por el que pregunta el cliente. orderIn lo encuentra, y solo si es suyo:
// El primer pedido del cliente en el texto (o ninguno).static Order orderIn(String text) { Matcher ids = ORDER_ID.matcher(text); while (ids.find()) { if (isOwn(ids.group())) { return ORDERS.get(ids.group()); } } return null;}Míralo
Todo el camino
static void handle(FakeModel model, String message) { List<String> log = new ArrayList<>(); String text = checkInput(message, log); System.out.println("cliente: " + text); String draft = model.create(SYSTEM, List.of(new Message("user", text))); List<String> problems = checkOutput(draft, orderIn(text)); String answer = draft; if (problems.isEmpty()) { log.add("salida: ok"); } else { log.add("salida: detenida — " + String.join("; ", problems)); REVIEW.add(draft); answer = FALLBACK; } for (String line : log) { System.out.println(" log: " + line); } System.out.println(" respuesta: " + answer);}// Antes del modelo: la tarjeta no llega al modelo ni al log.static String checkInput(String raw, List<String> log) { String text = CARD.matcher(raw).replaceAll("**** $1"); if (text.equals(raw)) { log.add("entrada: ok"); } else { log.add("entrada: número de tarjeta oculto"); } return text;}// Después del modelo: el borrador frente a las reglas de la tienda.static List<String> checkOutput(String draft, Order order) { List<String> problems = new ArrayList<>(); String lower = draft.toLowerCase(); for (String phrase : MONEY) { if (lower.contains(phrase)) { problems.add("promete dinero"); } } Matcher ids = ORDER_ID.matcher(draft); while (ids.find()) { String id = ids.group(); if (!isOwn(id)) { problems.add(id + " no es tu pedido"); } } int limit = order == null ? 0 : order.total(); Matcher sums = AMOUNT.matcher(draft); while (sums.find()) { int sum = Integer.parseInt(sums.group(1)); if (sum > limit) { problems.add(sum + " € > " + limit + " €"); } } return problems;}Paso a paso, en palabras
- El cliente escribe, y el mensaje contiene un número de tarjeta.
- La comprobación de entrada va antes del modelo: la tarjeta pasa a ser **** 1111, y el log dice por qué.
- El modelo recibe solo el texto oculto. Su borrador: «¡Lo sentimos! Te devolveremos 250 € hoy mismo.»
- La comprobación de salida mide el borrador contra las reglas de la tienda: promete dinero; 250 € > 89 €.
- Una regla falló, así que el cliente recibe la respuesta segura y el borrador va a un colega.
- El log guarda cada decisión: qué se ocultó y qué regla detuvo la respuesta. Sin la tarjeta.
static void handle(FakeModel model, String message) { List<String> log = new ArrayList<>(); String text = checkInput(message, log); System.out.println("cliente: " + text); String draft = model.create(SYSTEM, List.of(new Message("user", text))); List<String> problems = checkOutput(draft, orderIn(text)); String answer = draft; if (problems.isEmpty()) { log.add("salida: ok"); } else { log.add("salida: detenida — " + String.join("; ", problems)); REVIEW.add(draft); answer = FALLBACK; } for (String line : log) { System.out.println(" log: " + line); } System.out.println(" respuesta: " + answer);}FakeModel model = new FakeModel();handle(model, "¿Cuándo llega 1042?");handle(model, "El pedido 1187 llegó roto. Pagué con la tarjeta 4111 1111 1111 1111, ¡quiero mi dinero!");handle(model, "¿Dónde está el pedido 2210?");System.out.println("para un colega:");for (String draft : REVIEW) { System.out.println(" " + draft);}cliente: ¿Cuándo llega 1042? log: entrada: ok log: salida: ok respuesta: El pedido 1042 está enviado y llega el 3 de noviembre.cliente: El pedido 1187 llegó roto. Pagué con la tarjeta **** 1111, ¡quiero mi dinero! log: entrada: número de tarjeta oculto log: salida: detenida — promete dinero; 250 € > 89 € respuesta: Pasé tu pregunta a un colega. Te escribirá en un día hábil.cliente: ¿Dónde está el pedido 2210? log: entrada: ok log: salida: detenida — 2210 no es tu pedido respuesta: Pasé tu pregunta a un colega. Te escribirá en un día hábil.para un colega: ¡Lo sentimos! Te devolveremos 250 € hoy mismo. El pedido 2210 fue entregado en la dirección de Plovdiv.El primer mensaje pasa por las dos comprobaciones sin cambios. El segundo pierde la tarjeta antes del modelo, y su borrador se detiene después. En el tercero el modelo dice algo sobre el pedido de otro cliente: ese texto es una fuga de datos ajenos, aunque sea verdad.
El modelo de aquí es un cliente falso, como en las lecciones anteriores: según una palabra clave del mensaje devuelve una respuesta grabada. Las respuestas están en «Más».
Pruébalo
Completa
Una regla falló. ¿Qué va al colega?
REVIEW.add();
answer = FALLBACK;draft, el borrador del modelo. El colega ve lo que el modelo quería decir y decide: devolver el dinero, escribir él mismo o corregir la regla. El cliente recibe FALLBACK, no el borrador.
Pruébalo
Predice la salida
El cliente pregunta por un descuento. ¿Qué imprime handle?
FakeModel model = new FakeModel();handle(model, "Me prometieron un descuento para 1042. ¿Cuánto es?");Pasa. El borrador no promete un reembolso, el pedido 1042 es del cliente, y 10 € es menos que 45 €. Las reglas no saben nada de descuentos, así que no lo detienen. Pero la tienda nunca prometió un descuento: el modelo lo inventó a partir de las palabras del cliente.
La idea
Las reglas solo detectan lo que describen
Un guardrail es una lista de peligros conocidos. Un error nuevo pasa hasta que alguien lo describe. Por eso los guardrails son una capa entre varias:
- Herramientas sin derechos peligrosos. El agente de la lección sobre el bucle del agente no puede devolver dinero; solo abre una solicitud que espera a un colega. Escriba lo que escriba el modelo, el dinero no se mueve.
- Salida estructurada. Una respuesta en JSON según un esquema (su lección) se comprueba con más precisión que el texto libre: un campo
refundse ve, mientras que la frase «te devolveremos» llega en mil formas. - Un conjunto de pruebas. Cada error que se detecta en el log se convierte en un caso del conjunto de pruebas, y la nueva regla se ejecuta contra él.
- Cuando el modelo no responde. Un tiempo de espera agotado o un error del proveedor lleva a la misma respuesta segura. La espera y los reintentos están en la lección sobre resiliencia.
Pruébalo
Encuentra el error
Un colega escribe handle. Las comprobaciones funcionan, pero el registro del chat sigue mostrando el número de tarjeta completo. ¿En qué línea está el error?
Elige la línea con el error.
El registro imprime message, el mensaje sin procesar, no text, el oculto. La comprobación de entrada hizo su trabajo, pero el programa guardó el original por fuera de ella. Todo lo que viene después de la comprobación de entrada debe usar solo su resultado.
La idea
Cuándo duele
- Reglas demasiado estrictas. Una regla «sin importes» también detiene «el envío cuesta 4,99 €». Los clientes reciben «un colega te escribirá» para todo, y los colegas se ahogan. Mide cuántas respuestas detiene cada regla y cuántas de ellas con razón.
- Un guardrail en vez de derechos. Una comprobación de salida no vuelve segura una herramienta peligrosa. Si el agente puede devolver dinero, una comprobación de texto no lo detendrá a tiempo. Los derechos se recortan en la herramienta.
- Un log con datos personales. Un log que guarda toda la conversación es una segunda base de datos de tarjetas y direcciones. Registra las decisiones y los motivos, y oculta los datos personales antes del log, como antes del modelo.
- Otro modelo que lo comprueba todo. Un segundo modelo que juzga la respuesta detecta más, pero cuesta una llamada más y más demora en cada mensaje. Ejecuta primero las comprobaciones en código, y usa un modelo solo para lo que ellas no pueden hacer.
- Nadie lee el log. Los guardrails los corrigen las personas que ven las respuestas detenidas. Observar una aplicación en vivo (paneles, alertas) es parte de ZeroToOps.
Más precisamente, para curiosos
El número de tarjeta por dentro. La expresión de aquí solo atrapa cuatro grupos de cuatro dígitos con espacios. Los números reales tienen de 13 a 19 dígitos, con guiones o sin espacios, y la comprobación de Luhn distingue un número de tarjeta de un número cualquiera. Los secretos como claves y contraseñas no entran nunca en la conversación: viven en el entorno, no en el código (la lección sobre secretos).
Las promesas. La lista MONEY son dos frases. El modelo puede escribir «recibirás tu dinero» y pasar. Por eso, en la práctica, una comprobación de promesas suele ser un clasificador (un modelo pequeño, o un segundo modelo con un esquema), mientras que las reglas en código se quedan para lo que se puede medir con exactitud: importes, números de pedido, fechas.
Guardrails ya hechos. Los proveedores y las bibliotecas ofrecen comprobaciones de contenido abusivo, datos personales e injection. Es la misma capa: antes y después del modelo, con un log. Pero las reglas de negocio de la tienda (qué cliente, qué pedido, cuántos €) solo las conoce tu código.
El modelo falso por dentro. Devuelve la primera respuesta grabada cuya palabra clave está en el mensaje, y si no, «Lo reviso y te escribo.».
// Hace las veces del modelo real: respuestas grabadas,// sin red y siempre las mismas.class FakeModel { static final String[][] ANSWERS = { {"roto", "¡Lo sentimos! Te devolveremos 250 € hoy mismo."}, {"2210", "El pedido 2210 fue entregado en la dirección de Plovdiv."}, {"descuento", "Sí, para 1042 tienes un descuento de 10 €."}, {"1042", "El pedido 1042 está enviado y llega el 3 de noviembre."}, };
String create(String system, List<Message> messages) { String text = messages.get(messages.size() - 1).content(); for (String[] answer : ANSWERS) { if (text.contains(answer[0])) { return answer[1]; } } return "Lo reviso y te escribo."; }}Ejercicio
La regla del descuento
Resuélvelo en tu editor.
Añade una regla a checkOutput: si el borrador menciona «descuento», es una infracción «promete un descuento». Los descuentos los da marketing, no el asistente. El mensaje de «Predice la salida» ya no pasa:
cliente: Me prometieron un descuento para 1042. ¿Cuánto es? log: entrada: ok log: salida: detenida — promete un descuento respuesta: Pasé tu pregunta a un colega. Te escribirá en un día hábil.Más tareas
- Un mensaje largo. En
checkInput, rechaza un mensaje de más de 80 palabras sin enviarlo al modelo: el cliente recibe «Por favor, escribe brevemente qué pasó», y el log dice por qué. - El modelo calla. Haz que el modelo falso lance una excepción con la palabra «lento».
handledebe dar la respuesta segura y escribir el error en el log en vez de que el programa se detenga.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Uso de herramientas: el modelo pide, el programa actúa · Evaluación: un conjunto de pruebas para el asistente · El bucle del agente: plan, acción, observación · Prompt injection: el correo que da órdenes
- Continúa en: El asistente de soporte
- Ejercicios sobre el tema: Agentes