Course EN

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.