Retrieval systems  ·  5 min read  ·  2024.09

Building a RAG pipeline from scratch, no framework

The agent answering questions on this site is a RAG pipeline I wrote from scratch, with no framework. Not because frameworks are bad, but because building it by hand is the only way to actually understand every failure mode. Here is the whole thing, using this site as the worked example.

The five moving parts

A retrieval-augmented pipeline is smaller than it looks. Mine is five steps:

  1. Chunking. My facts live in small markdown documents. A paragraph-based chunker splits them with one paragraph of overlap, so a fact that straddles a boundary still lands whole in at least one chunk.
  2. Embedding. Each chunk becomes a vector with OpenAI’s text-embedding-3-small, sliced to 1024 dimensions. The same model embeds the visitor’s question at query time, so questions and documents live in the same space.
  3. Storage and search. Vectors go into pgvector on Postgres with an HNSW index. Retrieval is a cosine-similarity nearest-neighbour query: given the question vector, pull the closest chunks.
  4. Grounding. Only chunks above a similarity threshold are allowed into the prompt. If nothing clears the bar, the agent says it does not have that, instead of guessing.
  5. Generation. The retrieved chunks and the question are handed to a small model as data, and the answer is streamed back token by token.

One embedding, reused three times

The detail I like most is that a request calls the embedding API exactly once. That single question vector does triple duty: it feeds a similarity gate that rejects off-topic questions before any expensive model runs, it checks a semantic cache, and it drives retrieval. One call, three jobs. Building it by hand is what surfaced that optimization; a framework would have hidden the three passes behind three abstractions.

Where the honesty lives

The part I care about is grounding. The prompt treats the retrieved documents and the question as data, never as instructions, and it makes the model cite its sources by ID. Those IDs resolve server-side against the exact chunks that were retrieved, so the model cannot cite a document it never saw. I wrote about why that matters in a separate note on citing versus summarizing.

Take a system apart until the math is obvious, then ship only the parts you actually need. This pipeline is a few hundred lines of plain code, and I can point at every one of them.

Sistemas de recuperación  ·  5 min de lectura  ·  2024.09

Un pipeline RAG desde cero, sin framework

El agente que responde preguntas en esta web es un pipeline RAG que escribí desde cero, sin framework. No porque los frameworks sean malos, sino porque construirlo a mano es la única forma de entender de verdad cada modo de fallo. Aquí está entero, usando esta misma web como ejemplo.

Las cinco piezas

Un pipeline de recuperación es más pequeño de lo que parece. El mío son cinco pasos:

  1. Chunking. Mis datos viven en documentos markdown pequeños. Un chunker por párrafos los divide con un párrafo de solapamiento, así un hecho que cae en el límite sigue quedando entero en al menos un chunk.
  2. Embedding. Cada chunk se convierte en un vector con text-embedding-3-small de OpenAI, recortado a 1024 dimensiones. El mismo modelo convierte la pregunta del visitante en tiempo de consulta, así preguntas y documentos viven en el mismo espacio.
  3. Almacenamiento y búsqueda. Los vectores van a pgvector sobre Postgres con un índice HNSW. La recuperación es una consulta de vecino más cercano por similitud coseno: dado el vector de la pregunta, se traen los chunks más próximos.
  4. Grounding. Solo los chunks por encima de un umbral de similitud entran al prompt. Si nada supera el listón, el agente dice que no tiene eso, en vez de adivinar.
  5. Generación. Los chunks recuperados y la pregunta se le pasan a un modelo pequeño como datos, y la respuesta se emite token a token.

Un embedding, reutilizado tres veces

El detalle que más me gusta es que una petición llama a la API de embeddings exactamente una vez. Ese único vector de la pregunta hace triple trabajo: alimenta un gate de similitud que rechaza preguntas fuera de tema antes de que corra ningún modelo caro, comprueba una caché semántica y dirige la recuperación. Una llamada, tres tareas. Construirlo a mano es lo que sacó a la luz esa optimización; un framework habría escondido las tres pasadas tras tres abstracciones.

Donde vive la honestidad

La parte que me importa es el grounding. El prompt trata los documentos recuperados y la pregunta como datos, nunca como instrucciones, y hace que el modelo cite sus fuentes por ID. Esos IDs se resuelven en el servidor contra los chunks exactos que se recuperaron, así que el modelo no puede citar un documento que nunca vio. Escribí sobre por qué eso importa en otra nota, la de citar frente a resumir.

Desmonta un sistema hasta que las matemáticas son obvias, y luego lleva a producción solo las piezas que necesitas. Este pipeline son unos cientos de líneas de código plano, y puedo señalar cada una.