<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krishnendu Chatterjee</title>
    <description>The latest articles on DEV Community by Krishnendu Chatterjee (@krish0549).</description>
    <link>https://dev.to/krish0549</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1138177%2F0f8c7bcb-8097-45a0-b753-666075fed2b7.png</url>
      <title>DEV Community: Krishnendu Chatterjee</title>
      <link>https://dev.to/krish0549</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krish0549"/>
    <language>en</language>
    <item>
      <title>How to build a RAG system from scratch in Python (chunk embed retrieve cite)( https://ai.studybydoing.in)</title>
      <dc:creator>Krishnendu Chatterjee</dc:creator>
      <pubDate>Fri, 18 Sep 2026 03:21:53 +0000</pubDate>
      <link>https://dev.to/krish0549/how-to-build-a-rag-system-from-scratch-in-python-chunk-embed-retrieve-cite-fa4</link>
      <guid>https://dev.to/krish0549/how-to-build-a-rag-system-from-scratch-in-python-chunk-embed-retrieve-cite-fa4</guid>
      <description>&lt;p&gt;Most "RAG tutorials" hand you a framework and a &lt;code&gt;.from_documents()&lt;/code&gt; one-liner, and you never actually see what happens inside. So I built one &lt;strong&gt;by hand&lt;/strong&gt; — chunking, embeddings, a tiny&lt;br&gt;
  vector store, hybrid retrieval, re-ranking, and cited generation — to understand each moving part. Here's the mental model and the two pieces that matter most.&lt;/p&gt;

&lt;p&gt;## What RAG actually is&lt;/p&gt;

&lt;p&gt;An LLM only knows what was in its training data. &lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt; lets it answer questions about &lt;em&gt;your&lt;/em&gt; private/current documents by retrieving relevant snippets at&lt;br&gt;
  query time and putting them in the prompt. The model then answers from that supplied context — facts, not guesses.&lt;/p&gt;

&lt;p&gt;The pipeline has two timelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline (build the index once):&lt;/strong&gt; Documents → Chunk (+metadata) → Embed → Vector store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online (per user question):&lt;/strong&gt; Question → Retrieve (vector + keyword) → Re-rank → Build context → LLM → Answer + citations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one line: &lt;strong&gt;RAG = look things up first, then answer from what you found.&lt;/strong&gt; The offline row is a librarian shelving books; the online row is you asking a question and getting the right&lt;br&gt;
  pages handed to you before you write your reply.&lt;/p&gt;

&lt;p&gt;## The highest-leverage decision: chunking&lt;/p&gt;

&lt;p&gt;Models retrieve &lt;strong&gt;chunks, not whole documents&lt;/strong&gt; — so how you split matters more than almost anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too big → irrelevant text dilutes the answer and wastes tokens.&lt;/li&gt;
&lt;li&gt;Too small → facts get split across chunks.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Split on paragraphs, then pack into ~target_words chunks with overlap.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
      &lt;span class="n"&gt;paras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\n\s*\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
      &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;target_words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;  &lt;span class="c1"&gt;# carry last para as overlap
&lt;/span&gt;      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The rules that survived the labs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Split on &lt;strong&gt;semantic boundaries&lt;/strong&gt; (paragraphs, headings) — not blind fixed windows.&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;10–20% overlap&lt;/strong&gt; so a fact near a boundary survives in at least one chunk.&lt;/li&gt;
&lt;li&gt;Carry &lt;strong&gt;metadata&lt;/strong&gt; (source, section, URL, date) — you need it for citations and filtering.&lt;/li&gt;
&lt;li&gt;Aim &lt;strong&gt;~100–250 words&lt;/strong&gt; for precision; add a "parent document" fallback for context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Embeddings + a vector store in ~15 lines&lt;/p&gt;

&lt;p&gt;An embedding turns text into a vector where &lt;strong&gt;similar meanings sit close together&lt;/strong&gt;. Store each chunk's vector; at query time, embed the question and find the nearest chunks by cosine&lt;br&gt;
  similarity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;  &lt;span class="c1"&gt;# swap for any provider
&lt;/span&gt;
  &lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="c1"&gt;# normalize -&amp;gt; cosine similarity collapses into a plain dot product
&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
          &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
          &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# (N chunks x d)
&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
          &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="n"&gt;sims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;                             &lt;span class="c1"&gt;# similarity to every chunk, one step
&lt;/span&gt;          &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things clicked for me here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;embed()&lt;/code&gt; is the one place text becomes numbers&lt;/strong&gt; — wrap it behind a single function so you can swap providers without touching anything else. Golden rule: embed questions and
documents with the &lt;em&gt;same&lt;/em&gt; model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is genuinely what Pinecone / FAISS / pgvector do under the hood&lt;/strong&gt; — they just add persistence, scale, and speed. Building the naive version first makes the real ones far less
magical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;## Then: hybrid retrieval, re-ranking, and &lt;em&gt;citations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The last labs add the parts that separate a demo from something usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid search&lt;/strong&gt; — combine meaning-based (vector) with exact-word (BM25) so you don't miss literal matches like error codes or names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-ranking&lt;/strong&gt; — reorder candidates so the best rise to the top before they hit the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounded, cited generation&lt;/strong&gt; — answer &lt;em&gt;only&lt;/em&gt; from retrieved context, cite the source chunk, and &lt;strong&gt;refuse when the context doesn't contain the answer&lt;/strong&gt; (this is what kills hallucinations
in practice).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## The mental model to keep&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAG beats a bigger context window / fine-tuning when your knowledge is &lt;strong&gt;private, changing, or needs citations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking is the highest-leverage knob&lt;/strong&gt; — get it wrong and nothing downstream saves you.&lt;/li&gt;
&lt;li&gt;Retrieval is &lt;strong&gt;hybrid + re-rank&lt;/strong&gt;, not just "nearest vector."&lt;/li&gt;
&lt;li&gt;Generation must be &lt;strong&gt;grounded and honest&lt;/strong&gt; — cite, or refuse.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want to &lt;em&gt;run&lt;/em&gt; each stage yourself (the lesson has an in-browser Python terminal, no setup) the full build is here 👉 &lt;strong&gt;&lt;a href="https://ai.studybydoing.in/ch03-rag" rel="noopener noreferrer"&gt;Build a RAG System From&lt;br&gt;
  Scratch&lt;/a&gt;&lt;/strong&gt;. It's part of a free course that builds RAG, agents, eval, and production LLM systems by hand:&lt;br&gt;
  &lt;strong&gt;&lt;a href="https://ai.studybydoing.in" rel="noopener noreferrer"&gt;ai.studybydoing.in&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>What is a KV cache in LLM inference? (and why it, not the weights, limits your throughput)</title>
      <dc:creator>Krishnendu Chatterjee</dc:creator>
      <pubDate>Thu, 17 Sep 2026 18:13:53 +0000</pubDate>
      <link>https://dev.to/krish0549/what-is-a-kv-cache-in-llm-inference-and-why-it-not-the-weights-limits-your-throughput-5276</link>
      <guid>https://dev.to/krish0549/what-is-a-kv-cache-in-llm-inference-and-why-it-not-the-weights-limits-your-throughput-5276</guid>
      <description>&lt;p&gt;I've been working through the inference chapter of a free course I've been studying — &lt;strong&gt;&lt;a href="https://ai.studybydoing.in" rel="noopener noreferrer"&gt;AI Engineering: Zero to Production&lt;/a&gt;&lt;/strong&gt; — and&lt;br&gt;
  the KV-cache lesson finally made something click that I'd been hand-waving for months. Sharing my notes here in case it helps someone else.&lt;/p&gt;

&lt;p&gt;The thing that surprised me: I always assumed the model &lt;strong&gt;weights&lt;/strong&gt; were what filled up the GPU. Turns out the thing that actually caps &lt;em&gt;how many users you can serve at&lt;br&gt;
  once&lt;/em&gt; is usually the &lt;strong&gt;KV cache&lt;/strong&gt;. Here's what I took away.&lt;/p&gt;

&lt;p&gt;## What the KV cache actually is&lt;/p&gt;

&lt;p&gt;At each decode step, the model attends to &lt;strong&gt;every previous token&lt;/strong&gt;. Recomputing the keys and values for all of them, every step, would be hugely wasteful — so they get&lt;br&gt;
  stored. That store is the KV cache.&lt;/p&gt;

&lt;p&gt;One decode step, in plain terms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New token&lt;/strong&gt; — the latest token the model is processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute its K, V&lt;/strong&gt; — think of &lt;code&gt;K&lt;/code&gt; ("key") as &lt;em&gt;what this token is about&lt;/em&gt; and &lt;code&gt;V&lt;/code&gt; ("value") as &lt;em&gt;the info it carries&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append to the cache&lt;/strong&gt; — this is why the cache grows &lt;strong&gt;every single step&lt;/strong&gt;, and why long chats keep eating memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attend over all cached K, V&lt;/strong&gt; — to pick the next word, the model compares the new token against everything stored, without recomputing it. That's the whole speed
win.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: &lt;strong&gt;the KV cache is the model remembering the K/V of past tokens so it never redoes that work.&lt;/strong&gt; Fast decode, but it's the main thing eating GPU memory.&lt;/p&gt;

&lt;p&gt;## The part that actually surprised me — the arithmetic&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kv_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes_per&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="c1"&gt;# leading 2 = we store BOTH K and V
&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;heads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;seq_len&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bytes_per&lt;/span&gt;

  &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kv_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; MB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 1024.0 MB → 1 GiB for a SINGLE 2k sequence
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;~1 GB of KV cache for one 2,048-token request&lt;/strong&gt; on an ordinary 32-layer/32-head model. That was the "oh" moment for me — the cache, not the weights, is what bounds&lt;br&gt;
  concurrency. And it grows on two axes at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context length&lt;/strong&gt; — longer prompts = bigger cache per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; — every simultaneous request needs its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Two levers I hadn't fully understood&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagedAttention (the vLLM idea):&lt;/strong&gt; instead of each request reserving one big contiguous block for its &lt;em&gt;max possible&lt;/em&gt; length (wasteful), it manages the cache like OS&lt;br&gt;
  virtual memory — non-contiguous pages on demand. That's how a server fits way more concurrent requests in the same GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching:&lt;/strong&gt; if requests share a prefix (long system prompt, RAG context, few-shot preamble), you cache its KV and skip re-prefilling it. On hosted APIs it's a&lt;br&gt;
  billing discount; self-hosting it's real compute saved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;BIG_SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_manual.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# long, reused preamble
&lt;/span&gt;
  &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BIG_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- marks this block cacheable
&lt;/span&gt;      &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize section 4.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# cache_creation_input_tokens vs cache_read_input_tokens
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What made it concrete: &lt;strong&gt;run it twice.&lt;/strong&gt; First call = big cache &lt;em&gt;write&lt;/em&gt;, ~zero read. Second call = the &lt;em&gt;read&lt;/em&gt; jumps, the write drops. The savings only land on the 2nd+&lt;br&gt;
  call that reuses the prefix.&lt;/p&gt;

&lt;p&gt;## The mental model I'm keeping&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KV cache = remembered K/V for past tokens → fast decode.&lt;/li&gt;
&lt;li&gt;Grows with &lt;strong&gt;context length × concurrency&lt;/strong&gt; → it, not the weights, limits how many users you serve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PagedAttention&lt;/strong&gt; cuts waste when self-hosting; &lt;strong&gt;prompt caching&lt;/strong&gt; reuses shared prefixes to cut repeat cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to actually &lt;em&gt;run&lt;/em&gt; the size formula and the caching example (the lesson has an in-browser Python terminal, no setup), the original is here 👉 &lt;strong&gt;&lt;a href="https://ai.studybydoing.in/ic3-kv-cache" rel="noopener noreferrer"&gt;KV-Cache &amp;amp;&lt;br&gt;
  Attention Memory in LLM Inference&lt;/a&gt;&lt;/strong&gt;. The whole course builds RAG, agents, eval, and production LLM stuff by hand:&lt;br&gt;
  &lt;strong&gt;&lt;a href="https://ai.studybydoing.in" rel="noopener noreferrer"&gt;ai.studybydoing.in&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>machinelearning</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
