Agentes RAG en producción
Un agente sólo es útil cuando consulta sistemas reales sin inventarse las respuestas.
- Cuándo
- en curso
- Con
- embeddings, retrieval, tool use, clientes reales
Agentes de IA con retrieval-augmented generation funcionando con clientes de verdad: un chatbot con retrieval para un estudio de fotografía, y Juanito, el asistente conversacional de El Empleado Juan — clasifica conversaciones, consulta datos de asistencia y nómina, y opera contra la plataforma.
Lo que los tutoriales no cuentan
Un demo de RAG se construye en una tarde. Lo difícil empieza después, cuando hay alguien del otro lado esperando un dato correcto:
- El retrieval falla en silencio. Si la búsqueda no devuelve nada útil, el modelo no se calla: responde igual, en un tono perfectamente seguro y sin la información. Es el peor modo de fallo posible, porque no parece un fallo.
- Trocear el documento es una decisión de producto, no de infraestructura. El tamaño del fragmento decide qué preguntas se pueden contestar.
- Un agente que consulta datos reales necesita un contrato, no un prompt. Herramientas tipadas, resultados validados, y un camino explícito para «no lo sé» que sea más barato que inventar.
- Sin evaluación no hay mejora, sólo una sensación. Un conjunto de preguntas con respuesta conocida vale más que cualquier ajuste de temperatura.
La regla que aplico: el agente puede equivocarse de tono, nunca de dato.
El contrato del «no lo sé»
El fallo caro no es equivocarse: es responder con seguridad cuando el retrieval no trajo nada. Aquí eso no puede pasar en silencio, porque «no lo sé» es un valor de retorno y no una virtud que se le pide al modelo.
// Dos formas de terminar, y ambas son válidas. Si la única forma de
// éxito es «dato», el modelo aprende que callarse es fracasar — y deja
// de callarse.
const Respuesta = z.discriminatedUnion('tipo', [
z.object({ tipo: z.literal('dato'), valor: z.string(), fuente: z.string().min(1) }),
z.object({ tipo: z.literal('sin_fundamento'), falta: z.string() }),
]);
async function responder(pregunta: string): Promise<Respuesta> {
const trozos = await recuperar(pregunta, { k: 8, minScore: 0.35 });
// Sin contexto suficiente no se llama al modelo. Llamarlo aquí es
// pedirle explícitamente que invente, y lo hará en un tono
// perfectamente seguro.
if (trozos.length === 0) {
return { tipo: 'sin_fundamento', falta: pregunta };
}
const salida = await modelo.completar({ pregunta, trozos, schema: Respuesta });
// La cita se comprueba contra lo recuperado, no contra lo que el
// modelo dice haber citado. Es la diferencia entre tener fuentes y
// parecer que las tienes.
return trozos.some((t) => t.id === salida.fuente)
? salida
: { tipo: 'sin_fundamento', falta: 'la cita no corresponde a lo recuperado' };
}
El minScore es el parámetro que más discusión ahorra: por debajo de él no hay
respuesta que valga, y eso se decide una vez con un conjunto de preguntas de
respuesta conocida en vez de discutirlo caso por caso.
Escríbeme
Si estás pensando en hacer algo con IA y quieres hablarlo con alguien que ya lo puso en producción, escríbeme. Contesto yo.
hola@giron.ai