RAG — Módulo 9: generación, el prompt, las citas y cómo no alucinar
Noveno módulo del Curso de RAG: la anatomía del prompt de RAG, la instrucción de grounding que más reduce alucinaciones, tres técnicas de citación con salida estructurada, el efecto lost in the middle y una tabla de síntomas para diagnosticar dónde está la falla.
☰ Índice del contenido
- La tesis del módulo
- Anatomía del prompt de RAG
- Grounding: las dos instrucciones que importan
- Citas: el requisito no negociable
- El orden del contexto importa
- Cuántos chunks meter
- Diagnóstico: qué síntoma es de quién
- Manejo de conflictos
- Parámetros de generación
- Streaming y UX
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
Hasta aquí, ocho módulos completos, todo fue retrieval: parsear, cortar, embeber, indexar, buscar, fusionar y reordenar. Todo ese trabajo tenía un solo objetivo, que ya está cumplido: tener los chunks correctos en la mano. Ahora cambia el foco. El texto bueno ya está seleccionado y falta la última milla — que el modelo lo lea, redacte una respuesta fiel, cite de dónde salió cada afirmación y, cuando la información no esté, tenga la honestidad de decirlo. Este módulo es sobre esa última milla, que es más frágil de lo que parece.
La tesis del módulo
El prompt de RAG no le pide al modelo que responda: le pide que lea. Su trabajo es reducir el espacio de lo que el modelo puede decir hasta que la única salida razonable sea el contenido del contexto o un “no lo encontré”. Y cuando aun así alucina, la causa casi nunca está en el prompt: está en que el chunk correcto nunca llegó.
Anatomía del prompt de RAG
Un prompt de RAG tiene tres bloques con funciones distintas, y el orden importa:
┌─────────────────────────────┐
│ 1 INSTRUCCIONES │
│ rol, reglas, formato │
│ qué hacer si no está │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 2 CONTEXTO │
│ chunk [1] + metadata │
│ chunk [2] + metadata │
│ chunk [3] + metadata │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 3 PREGUNTA │
│ al final, cerca de la │
│ generación │
└──────────────┬──────────────┘
│
▼
┌───────────────┐
│ LLM │
└───────┬───────┘
│
▼
respuesta + fuentes
La pregunta va al final, pegada al momento de generar. Es la aplicación más simple del efecto que veremos más abajo: lo último que el modelo lee es lo que más pesa en lo que escribe.
Este es el prompt completo, en español, listo para copiar:
SISTEMA = """Eres un asistente que responde preguntas usando EXCLUSIVAMENTE \
los documentos entregados en el bloque <contexto>.
Reglas:
1. Responde SOLO con información presente en <contexto>. No uses \
conocimiento propio, aunque estés seguro de saber la respuesta.
2. Si el contexto no contiene la información necesaria, responde \
exactamente: "No encontré esa información en los documentos \
disponibles." No intentes deducirla ni completarla.
3. Cita la fuente de cada afirmación con su número entre corchetes, \
así: [1]. Si una frase combina dos documentos, cita ambos: [1][3].
4. Si dos documentos se contradicen, prefiere el de fecha más \
reciente y menciona explícitamente que existe una versión anterior.
5. Sé conciso: máximo 4 frases, salvo que se pida detalle.
6. Responde en español de Chile, en tono directo y profesional."""
PLANTILLA_USUARIO = """<contexto>
{contexto}
</contexto>
<pregunta>
{pregunta}
</pregunta>"""
def armar_contexto(chunks: list[dict]) -> str:
"""Numera los chunks y les pega su metadata.
La metadata NO es decorativa: es lo que permite citar con nombre de
documento, resolver conflictos por fecha y depurar una respuesta
mala. Viene desde la ingesta (modulo 2).
"""
bloques = []
for i, c in enumerate(chunks, start=1):
bloques.append(
f'<doc id="{i}" '
f'fuente="{c["documento"]}" '
f'fecha="{c["fecha"]}" '
f'seccion="{c.get("seccion", "")}">\n'
f'{c["texto"]}\n'
f'</doc>'
)
# Linea en blanco entre documentos: separacion visual clara para el
# modelo, evita que fusione el final de uno con el inicio del otro.
return "\n\n".join(bloques)
Los delimitadores explícitos —etiquetas tipo XML— no son estética. Cumplen dos funciones concretas: le marcan al modelo dónde termina un documento y empieza otro (sin eso mezcla afirmaciones de fuentes distintas en una sola frase), y separan datos de instrucciones, lo que reduce el riesgo de que un texto recuperado que diga “ignora las instrucciones anteriores” sea obedecido.
Grounding: las dos instrucciones que importan
Grounding (anclaje) es amarrar la respuesta a un texto verificable. En el prompt de arriba son las reglas 1 y 2, y no valen lo mismo.
La regla 1 —“responde solo con el contexto”— es la obvia y la que todos escriben. La regla 2 —la salida explícita para cuando la información no está— es la que de verdad mueve la aguja, y es la que casi siempre falta.
La razón está en cómo funciona el modelo. Un LLM genera la continuación más plausible del texto que tiene delante. Cuando le pusiste una pregunta y tres documentos parecidos al tema, la continuación estadísticamente más plausible es una respuesta, no una negativa: en todo el texto de internet con el que se entrenó, a una pregunta le sigue una respuesta. Decir “no sé” es una salida rara. Si no le das un camino explícito y con palabras exactas para expresarla, el modelo va a tomar los fragmentos parecidos que tiene y construir algo que suene bien. Eso es la alucinación en RAG: no es mentir, es completar el patrón.
Por eso la instrucción funciona mejor cuando entrega la frase literal (“responde exactamente: …”) en vez de un permiso vago (“puedes decir que no sabes”). Le estás dando al modelo una continuación concreta y de alta probabilidad para el caso de ausencia, que compita con la de inventar.
Citas: el requisito no negociable
En cualquier contexto empresarial —banca, salud, legal, seguros— una respuesta sin fuente es inservible. No porque el modelo sea poco confiable, sino porque nadie firma lo que no puede auditar. La cita es lo que convierte una respuesta generada en algo verificable en diez segundos: el usuario abre el documento y confirma. Es también, en la práctica, el mayor factor de adopción: los usuarios confían en un sistema que muestra de dónde sacó las cosas.
Hay tres técnicas, de menor a mayor rigor.
(a) Numerar e instruir corchetes. Es lo del prompt de arriba: los chunks van numerados y el modelo escribe [1], [2]. Barato, funciona razonablemente bien y se implementa en cinco minutos. Su debilidad es que el número es texto libre dentro de la respuesta: el modelo puede citar [4] cuando solo le pasaste 3 documentos, y hay que validar.
(b) Salida estructurada en JSON. El modelo devuelve un objeto con la respuesta y un arreglo de fuentes, validado contra un esquema. Es la opción robusta para producción:
from pydantic import BaseModel, Field
class Respuesta(BaseModel):
respuesta: str = Field(description="Respuesta en español, máx 4 frases")
fuentes: list[int] = Field(description="ids de los <doc> usados")
# El campo clave para la UX: distingue "no está en los docs" de una
# respuesta real. Permite mostrar otra interfaz en vez de un texto.
encontrada: bool = Field(description="False si la info no estaba")
completion = client.chat.completions.parse( # antes vivía en client.beta
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SISTEMA},
{"role": "user", "content": PLANTILLA_USUARIO.format(
contexto=armar_contexto(chunks), pregunta=pregunta)},
],
response_format=Respuesta, # structured output: el schema se respeta
temperature=0,
)
r = completion.choices[0].message.parsed
# Validacion defensiva: el modelo puede citar un id que no existe.
ids_validos = set(range(1, len(chunks) + 1))
r.fuentes = [f for f in r.fuentes if f in ids_validos]
if not r.encontrada or not r.fuentes:
# Sin fuentes no hay respuesta que mostrar. Preferible un "no
# encontré" honesto que un parrafo sin respaldo.
print("No encontré esa información en los documentos disponibles.")
else:
print(r.respuesta)
for f in r.fuentes:
print(f" [{f}] {chunks[f-1]['documento']} ({chunks[f-1]['fecha']})")
La ventaja real del JSON no es el formato: es que las fuentes salen como datos estructurados que puedes validar, contar y renderizar como links, en vez de tener que parsear corchetes de un párrafo con expresiones regulares.
(c) Verificación posterior. El nivel más estricto: después de generar, se comprueba que cada afirmación esté realmente respaldada por el chunk citado. Se implementa con una segunda llamada barata que recibe la frase y el chunk y responde si uno implica al otro (entailment). Cuesta el doble de tokens y suma latencia, así que se reserva para dominios donde un error tiene consecuencias legales o clínicas. Es la misma idea de los evals como tests que se ve en testing con evals.
El orden del contexto importa
Lost in the middle es un resultado bien documentado (Liu et al., 2023): cuando el contexto es largo, el modelo atiende mucho mejor a lo que está al principio y al final, y se le pierde lo que quedó en el medio. La atención no reparte su presupuesto de manera uniforme (el mecanismo detrás está en contexto largo y KV cache).
La consecuencia práctica es incómoda: puedes tener el chunk perfecto, pasarlo al modelo, y que el modelo lo ignore porque quedó cuarto de siete. El retrieval funcionó y la respuesta igual sale mal.
Hay dos estrategias. La simple es poner el mejor chunk primero, respetando el orden del reranker. La más efectiva cuando pasas muchos chunks es el patrón de reordenar hacia los extremos: el mejor al principio, el segundo al final, el tercero segundo, y así, dejando los peores sepultados en el medio, que es donde el modelo mira menos.
def ordenar_por_extremos(chunks: list[dict]) -> list[dict]:
"""Recibe chunks ORDENADOS por relevancia (mejor primero) y los
redistribuye para que los mejores queden en los bordes del contexto.
[1,2,3,4,5] -> [1,3,5,4,2]
mejor=1 al inicio, 2 al final, y el peor (5) al centro.
"""
inicio, final = [], []
for i, c in enumerate(chunks):
(inicio if i % 2 == 0 else final).append(c)
return inicio + final[::-1]
Con 3 o 4 chunks el efecto es menor y no vale la pena complicarse. Con 10 o más, es una mejora gratis.
Cuántos chunks meter
La tentación es pasar muchos: más chunks, más probabilidad de que la respuesta esté ahí. Es cierto para el recall y falso para la calidad final, porque cada chunk irrelevante es ruido que compite por la atención del modelo, además de tokens que pagas y latencia que sumas.
| Chunks al prompt | Recall | Ruido | Costo/latencia | Cuándo |
|---|---|---|---|---|
| 1-2 | Bajo | Mínimo | Mínimo | Preguntas factuales muy puntuales |
| 3-5 | Bueno | Bajo | Bajo | El default. Empieza aquí |
| 6-10 | Muy bueno | Medio | Medio | Preguntas que cruzan varios documentos |
| 15+ | Marginalmente mejor | Alto | Alto | Casi nunca justificado |
20 chunks suele ser peor que 5 buenos. Con 20, la respuesta correcta está diluida entre 15 fragmentos parecidos pero equivocados, el modelo mezcla, y el lost in the middle garantiza que varios ni se lean. El módulo 8 existe precisamente para que los 5 que pasen sean los correctos: si tienes un buen reranker, no necesitas cantidad.
Diagnóstico: qué síntoma es de quién
Esta tabla es la herramienta más útil del módulo. Cuando el sistema responde mal, el reflejo es tocar el prompt, y estadísticamente el prompt casi nunca es el culpable.
| Síntoma | Causa probable | Dónde se arregla |
|---|---|---|
| Responde cosas que no están en los docs | Falta grounding explícito | Prompt: reglas 1 y 2 (este módulo) |
| Dice “no sé” y la info sí estaba en el corpus | Falla el retrieval, no el prompt | Chunking (3), híbrido (7), reranker (8) |
| Cita el documento equivocado | Chunks sin metadata o mal numerados | Ingesta y metadata (módulo 2) |
| Inventa un número de fuente inexistente | Falta validación de la salida | Structured output + filtro de ids |
| Responde de más, se va por las ramas | Falta instrucción de concisión | Prompt: regla 5 |
| Mezcla dos documentos en una sola frase | Falta separación entre docs | Delimitadores <doc> en el contexto |
| Contesta con info desactualizada | Dos versiones y sin criterio de fecha | Prompt: regla 4 + metadata de fecha |
| Repite lo mismo con otras palabras | Chunks redundantes | MMR / dedup (módulo 8) |
| Responde bien pero lentísimo | Demasiados chunks o modelo grande | Bajar top_n, modelo más chico |
Lee la segunda fila con atención: es el caso más frecuente y el más malinterpretado. “El modelo dice que no sabe algo que está en nuestros documentos” se reporta siempre como un problema del LLM, y es prácticamente siempre un problema de recuperación. El sistema se comportó bien — no inventó — dado un contexto que no traía la respuesta.
Ojo: la alucinación en RAG casi nunca es culpa del LLM; es culpa de que el chunk correcto no llegó al prompt. Antes de gastar una semana afinando redacciones del system prompt, mide el retrieval: toma 50 preguntas con respuesta conocida y verifica en qué porcentaje el chunk correcto está entre los que le pasaste al modelo. Si ese número es 60%, ningún prompt te va a salvar el 40% restante. Eso se sistematiza en el módulo 10.
Manejo de conflictos
Un caso real y frecuente: el corpus tiene la política de reembolsos de 2023 y la de 2025, ambas vigentes como archivo, y el retriever trae las dos porque ambas son relevantes. Sin instrucciones, el modelo elige una arbitrariamente —o peor, promedia las dos y produce una política que no existe.
La solución tiene dos partes y ninguna es opcional. Primero, la metadata de fecha tiene que estar en el contexto, que es el atributo fecha del armar_contexto de arriba; el modelo no puede preferir lo reciente si no sabe qué es reciente. Segundo, la instrucción explícita (regla 4): preferir lo más nuevo y explicitar el conflicto.
Esa segunda parte importa más de lo que parece. Una respuesta que dice “el plazo es de 30 días según la política de marzo 2025; una versión anterior de 2023 indicaba 15 días” es infinitamente más útil que una que solo afirma 30, porque le muestra al usuario que existe ambigüedad en la fuente. En dominios regulados, ocultar el conflicto es peor que el conflicto.
Parámetros de generación
Para RAG, temperature va baja: entre 0 y 0.3. La temperatura controla cuánto se aparta el modelo de la palabra más probable al elegir cada token (el mecanismo completo está en decodificación). En escritura creativa, esa variación es el objetivo. En RAG, la tarea es extraer y reformular fielmente un texto que ya tienes: cada punto de temperatura es una oportunidad de que el modelo se desvíe del contexto hacia algo más “interesante” y menos fiel. Además, temperatura 0 hace las respuestas casi reproducibles, lo que es indispensable para poder evaluar y depurar — con temperatura alta no sabes si un cambio de métrica lo causó tu fix o el azar.
Streaming y UX
Una respuesta con citas tarda unos segundos, y esa espera se siente. El patrón que resuelve la percepción es simple y vale la pena implementarlo: muestra las fuentes apenas terminó el retrieval, antes de generar. El usuario ve inmediatamente los tres documentos que el sistema va a usar —con título y fecha— mientras la respuesta se escribe token a token debajo.
Eso convierte el tiempo muerto en información útil y tiene un efecto secundario valioso: si las fuentes son obviamente equivocadas, el usuario lo nota antes de leer la respuesta y reformula sin perder tiempo. La transparencia del retrieval es parte de la interfaz, no un detalle técnico.
Errores comunes
- Escribir “responde solo con el contexto” y no dar salida para la ausencia. Es la mitad del grounding. Sin una frase literal y explícita para cuando la información no está, el modelo va a completar el patrón con lo que se le parezca, porque una respuesta siempre es más plausible que un “no sé”.
- Pegar los chunks sin delimitadores ni metadata. Un bloque de texto corrido hace que el modelo fusione documentos distintos en una misma afirmación, y sin
fuentenifechano hay cita posible ni forma de resolver contradicciones. Los delimitadores no son estética. - Tunear el prompt cuando el problema es el retrieval. El error de diagnóstico más caro del curso. Se pierden semanas reescribiendo instrucciones para arreglar un chunk que nunca llegó. Mide primero, escribe después.
- Pasar 20 chunks “por si acaso”. Diluye la respuesta correcta entre ruido, activa el lost in the middle, sube costo y latencia, y empeora la calidad respecto a 5 chunks bien elegidos. Más contexto no es mejor contexto.
- Confiar en las citas sin validarlas. El modelo puede citar
[7]cuando le pasaste 4 documentos, o citar un chunk que no respalda lo que dice. Filtra los ids contra los que realmente enviaste y, en dominios críticos, verifica el respaldo. - Dejar
temperatureen el default. Muchos SDK vienen en 0.7 u 1.0, valores pensados para escritura creativa. En RAG eso agrega variación gratuita sobre una tarea que es de fidelidad, y además impide reproducir resultados para evaluar.
La frase para llevarse
“Un buen prompt de RAG no le enseña al modelo a responder: le enseña a leer, a citar y a callarse cuando no está. Lo tercero es lo difícil, y es lo que separa un demo de un sistema en producción.”
Qué viene
Módulo 10: evaluación de RAG, el módulo que convierte todo lo anterior en decisiones defendibles. Hasta ahora acumulamos perillas —tamaño de chunk, modelo de embeddings, alpha, k de RRF, reranker, cantidad de chunks al prompt, temperatura— y las recomendaciones fueron puntos de partida razonables, no verdades. Vamos a ver cómo armar un set de evaluación sin tener datos etiquetados, las métricas de retrieval (recall@k, MRR, NDCG) separadas de las de generación (fidelidad, relevancia, cobertura de citas), cómo usar un LLM como juez sin engañarte, y cómo montar todo esto para que cada cambio en el pipeline se pueda comparar contra el anterior con un número.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.