Capítulo 11 de 22 · intermedio
RAG de principio a fin
Qué cubre esta sesión
Ya tienes las dos mitades. Los embeddings convierten texto en vectores; un vector store recupera los pasajes más cercanos a una pregunta. Retrieval-augmented generation es la unión: recupera los chunks relevantes, aumenta el prompt pegándolos como contexto, y genera una respuesta que el modelo debe sacar de ese contexto — citando lo que usó, y admitiendo cuando la respuesta no está ahí. Esa es toda la técnica. Es como logras que un modelo responda preguntas sobre tus documentos, tus datos, tu producto — conocimiento con el que nunca fue entrenado — sin fine-tuning y sin que se invente cosas.
Tres letras, tres pasos, y el tercero es donde vive la disciplina. Un modelo al que le entregas contexto lo ignorará con gusto y responderá de memoria a menos que le digas que no. El prompt es donde impones "solo del contexto", "cita tus fuentes" y "di que no sabes" — y esas reglas son la diferencia entre un asistente útil y un mentiroso confiado.
OpenAI
Retrieval primero — la búsqueda vectorial de la semana pasada, puesta en línea para que todo el loop quepa en un archivo. Embebe la base de conocimiento una vez, luego rankéala contra la pregunta y toma los primeros.
# Embed the KB once, then for a question return the top-k chunks by cosine.
# This is the whole "retrieval" step — the vector store from last week, inlined.
KB_VECS = embed([c["text"] for c in KB])
def retrieve(question: str, k: int = 3) -> list[dict]:
qv = embed([question])[0]
scored = sorted(zip(KB, KB_VECS), key=lambda cv: cosine(qv, cv[1]), reverse=True)
return [chunk for chunk, _ in scored[:k]]
Luego aumenta y genera. Los chunks recuperados se vuelven un bloque de contexto
numerado, y las instrucciones hacen el trabajo pesado: responde solo del
contexto, cita los [id], y rehúsa cuando el contexto se queda corto.
# Augment + generate. The retrieved chunks become a numbered context block; the
# instructions force the model to answer ONLY from it, cite the chunk ids, and
# say so when the context doesn't cover the question. Grounding lives in the
# prompt, not in hope.
def answer(question: str) -> str:
chunks = retrieve(question)
context = "\n".join(f"[{c['id']}] {c['text']}" for c in chunks)
response = client.responses.create(
model=_CHAT_MODEL,
instructions=(
"Answer the question using ONLY the context below. Cite the sources "
"you use by their [id]. If the context does not contain the answer, "
"say 'I don't have that in the docs.' Do not use outside knowledge.\n\n"
f"Context:\n{context}"
),
input=[{"role": "user", "content": question}],
)
return response.output_text
El demo hace tres preguntas a propósito. Dos son respondibles desde la KB y reciben respuestas con citas. La tercera — "what's your CEO's name?" — no está en los docs, y las instrucciones hacen que el modelo lo diga en lugar de inventar un nombre. Córrelo sin esa última regla alguna vez y míralo adivinar; ese es el modo de falla que RAG existe para prevenir, y el prompt es lo único que lo previene.
Gemini
El mismo pipeline sobre los modelos de Gemini. El retrieval es byte por byte igual; solo cambian la llamada de embed y la llamada de chat.
def answer(question: str) -> str:
chunks = retrieve(question)
context = "\n".join(f"[{c['id']}] {c['text']}" for c in chunks)
response = client.models.generate_content(
model=_CHAT_MODEL,
contents=[types.Content(role="user", parts=[types.Part(text=question)])],
config=types.GenerateContentConfig(
system_instruction=(
"Answer the question using ONLY the context below. Cite the sources "
"you use by their [id]. If the context does not contain the answer, "
"say 'I don't have that in the docs.' Do not use outside knowledge.\n\n"
f"Context:\n{context}"
),
),
)
return response.text or ""
Algo que vale la pena nombrar: la calidad de RAG es la calidad del retrieval. Si el chunk correcto no está en el top-k, ninguna cantidad de prompting salva la respuesta — el modelo solo puede trabajar con lo que le entregues. Cuando un sistema de RAG da una respuesta equivocada, mira qué se recuperó antes de culpar al modelo; nueve de cada diez veces el chunk que necesitaba nunca llegó al contexto.
Ponlo a trabajar
Tres apps de docker-compose bajo code/showcase/<slug>/, cada una el mismo loop
de recuperar-aumentar-generar apuntado a un trabajo distinto. La misma rutina:
bash bootstrap-secrets.sh, docker compose up --build,
http://localhost:3000.
Showcase 1 — Q&A de documentos
El loop canónico sobre los docs de un producto. Las respuestas citan los [id]
de sus chunks, y los ids recuperados se muestran debajo de la respuesta para que
puedas verificar las citas contra lo que de verdad se trajo. Trazable por
construcción.
Showcase 2 — Rechazo con fundamento
El mismo loop con un corpus estrecho de políticas de RH y una regla dura de rechazo. Las preguntas dentro de alcance reciben respuestas con citas; las de fuera de alcance ("what's the stock price?") reciben un tajante "eso no está cubierto" en lugar de una alucinación. Este es el comportamiento que hace a RAG confiable — y el que la gente olvida probar.
Showcase 3 — Resumen con citas
RAG más allá de las preguntas y respuestas: dale un tema, recupera varias notas relevantes y sintetiza un resumen corto citando cada afirmación. Retrieval más síntesis, con fuentes — la forma de un asistente de investigación.
Las tres recuperan, luego fundamentan, luego generan, y las tres hacen que el modelo muestre sus fuentes. El corpus y la instrucción cambian; el loop no.
Córrelo
El README de esta carpeta tiene la versión de Python, el comando de instalación,
las dos variables de entorno y los comandos exactos. Las keys vienen del .env
sin trackear en la raíz del curso. El retrieval es puro standard library; solo
las llamadas al SDK necesitan keys. El chat de este propio blog responde a partir
de sus artículos exactamente de esta forma, sobre pgvector.
Lo que te llevas
RAG es recuperar, aumentar, generar — y el paso de generar es donde ganas o pierdes, porque un modelo responderá de memoria a menos que el prompt lo fuerce al contexto. Haz tres reglas no negociables: responde solo desde lo recuperado, cita las fuentes, y rehúsa cuando el contexto no cubra la pregunta. Luego recuerda que el techo de RAG es el techo del retrieval — un prompt perfecto no puede rescatar un chunk faltante, así que cuando las respuestas estén mal, inspecciona primero qué se trajo. Esto cierra el arco de retrieval: embeddings, un store, y ahora generación con fundamento sobre tu propio conocimiento. La próxima semana ampliamos lo que el modelo puede recibir en absoluto — visión y entrada multimodal, donde el "documento" es una imagen.