volver al blog

Abstención en RAG: enseñar al sistema a decir «no lo sé»

ragevaluacionllm

La pregunta que el corpus no contesta

Alguien pregunta por la política de devoluciones de un producto que tu base de conocimiento no menciona. El recuperador, obediente, devuelve los cinco fragmentos más parecidos: uno sobre devoluciones de otro producto, otro sobre garantías, tres sobre envíos. El modelo recibe contexto con aspecto de relevante y hace lo que mejor sabe hacer: componer una respuesta fluida.

Nadie ha mentido a propósito. El recuperador siempre devuelve k resultados, porque así está definido: ordena, no juzga. Y el generador siempre responde, porque así lo entrenaron. El resultado es un sistema sin salida de emergencia.

La abstención es esa salida. No es una cortesía de producto, es una propiedad de fiabilidad: un sistema que sabe cuándo callar vale más que uno que acierta un punto más de media y se equivoca con aplomo en la cola.

Por qué «di que no lo sabes» en el prompt no basta

La primera reacción es añadir una línea al prompt: «Si la respuesta no está en el contexto, di que no la sabes». Ayuda, y conviene dejarla. Pero tiene dos límites.

El primero es que el modelo juzga la relevancia con los mismos fragmentos que le han colado. Si el contexto parece contestar la pregunta, la instrucción no se dispara. El segundo es que no puedes medir ni ajustar una instrucción: no hay un dial que gire de «más prudente» a «más atrevido».

Una decisión de abstención útil tiene tres propiedades: se toma con una señal numérica, es ajustable con un umbral y se evalúa con datos. Eso obliga a sacarla del prompt y ponerla en código.

Tres señales para decidir antes de generar

Hay tres sitios de los que sacar la señal, de más barato a más caro.

SeñalQué mideCostePunto débil
Similitud del mejor fragmentoCuánto se parece el top-1 a la consultaGratis, ya la tienesLos umbrales no se transfieren entre modelos de embeddings
Puntuación del rerankerRelevancia real consulta-fragmentoUna pasada del rerankerDepende de la calibración del modelo
Veredicto de un juezSi el contexto permite responderUna llamada al LLMLatencia y coste por consulta

La similitud coseno es la tentación obvia, y funciona como primer filtro. Pero su escala no significa lo mismo en todos los modelos: un 0,62 puede ser excelente en uno y mediocre en otro. Por eso el umbral no se copia de un blog, se calibra con tu corpus.

El reranker suele dar la mejor relación calidad-precio, porque su puntuación está entrenada para decir si el fragmento responde a la pregunta, no solo si se parece a ella. El juez es la red de seguridad para los casos dudosos.

Implementarlo: un embudo con dos umbrales

La estructura que mejor funciona no es un único corte, sino una zona intermedia. Por encima del umbral alto, se responde. Por debajo del bajo, se declina. Entre ambos, se paga la llamada al juez.

from dataclasses import dataclass

@dataclass
class Decision:
    accion: str          # "responder" | "abstenerse"
    motivo: str

UMBRAL_ALTO = 0.80       # calibrados con tu golden set, no con intuición
UMBRAL_BAJO = 0.35

def decidir(consulta: str, fragmentos: list[dict]) -> Decision:
    # El reranker ya ha ordenado: fragmentos[0] es el mejor candidato
    mejor = max((f["score_reranker"] for f in fragmentos), default=0.0)

    if mejor >= UMBRAL_ALTO:
        return Decision("responder", f"score {mejor:.2f} sobre el umbral alto")
    if mejor < UMBRAL_BAJO:
        return Decision("abstenerse", f"score {mejor:.2f} bajo el umbral bajo")

    # Zona gris: solo aquí se gasta una llamada al LLM
    if juez_contexto_suficiente(consulta, fragmentos):
        return Decision("responder", "el juez confirma contexto suficiente")
    return Decision("abstenerse", "el juez no encuentra soporte en el contexto")

Dos detalles de diseño. Primero, la decisión devuelve un motivo: cuando alguien pregunte por qué el sistema no contestó, necesitarás verlo en la traza, no reconstruirlo. Segundo, el juez recibe una pregunta cerrada («¿este contexto contiene lo necesario para responder? sí/no») y no un «puntúa del 1 al 10». Las decisiones binarias son más estables.

Qué dice el sistema cuando se abstiene

Abstenerse mal es casi tan malo como inventar. Un «no tengo información» seco deja al usuario donde estaba. Una abstención útil hace una de estas cosas:

  • Dice qué sí encontró y qué no: «Tengo la política de devoluciones del modelo A, pero no del B».
  • Ofrece la siguiente acción: reformular, ampliar el filtro o pasar a una persona.
  • Registra la consulta sin respuesta, porque es la mejor lista de huecos de tu corpus.

Este último punto se subestima. Cada abstención es una señal de producto: si veinte usuarios preguntan lo mismo y el sistema calla, no tienes un problema de modelo sino de contenido.

Cómo saber si la abstención funciona

Aquí está el truco que casi nadie aplica: la abstención se evalúa con dos errores opuestos, y optimizar uno degrada el otro.

  • Abstención de más: el sistema calla cuando la respuesta sí estaba. Mide la utilidad perdida.
  • Abstención de menos: el sistema responde cuando no debía. Mide el riesgo asumido.

Necesitas un golden set con dos tipos de preguntas: contestables y deliberadamente incontestables (las segundas se fabrican fácil, mezclando temas que el corpus no cubre). Con ellas dibujas una curva: cada umbral te da un par de tasas de error, y el punto de trabajo lo eliges tú según el coste de cada fallo. En un asistente legal o médico, prefieres callar de más. En un buscador de recetas internas, quizá no.

Si ya mides el recuperador con recall o nDCG, esto es el mismo ejercicio con otra etiqueta: «esta consulta no tiene respuesta».

Qué hacer esta semana

  1. Añade al golden set entre veinte y cincuenta preguntas sin respuesta en el corpus.
  2. Registra la puntuación del mejor fragmento de cada consulta y mira cómo se distribuyen contestables frente a incontestables. Si las dos nubes se solapan mucho, el problema es el recuperador, no el umbral.
  3. Elige un umbral, mide los dos errores y decide cuál te cuesta más.
  4. Deja el prompt como segunda línea de defensa, no como la primera.

Un sistema que dice «no lo sé» a tiempo no es menos capaz. Es el único en el que se puede confiar cuando sí responde.