<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[REFRAG: Meta's Trick to Make RAG Fast]]></title><description><![CDATA[REFRAG: Meta's Trick to Make RAG Fast]]></description><link>https://refrag.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 18:13:27 GMT</lastBuildDate><atom:link href="https://refrag.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[REFRAG: Meta's Trick to Make RAG Blazing Fast - and Still Exact]]></title><description><![CDATA[How “Compress • Sense • Expand” lets LLMs handle huge knowledge bases without drowning in tokens.
Retrieval-Augmented Generation (RAG) is the backbone of knowledge-grounded LLM apps — but it’s painfully slow once you start feeding the model thousands...]]></description><link>https://refrag.hashnode.dev/refrag-metas-trick-to-make-rag-blazing-fast-and-still-exact</link><guid isPermaLink="true">https://refrag.hashnode.dev/refrag-metas-trick-to-make-rag-blazing-fast-and-still-exact</guid><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Shashivardhan Reddy Bajari]]></dc:creator><pubDate>Tue, 23 Sep 2025 15:54:20 GMT</pubDate><content:encoded><![CDATA[<hr />
<p><em>How “Compress • Sense • Expand” lets LLMs handle huge knowledge bases without drowning in tokens.</em></p>
<p>Retrieval-Augmented Generation (RAG) is the backbone of knowledge-grounded LLM apps — but it’s painfully slow once you start feeding the model thousands of retrieved tokens. The root problem? Transformer self-attention cost grows roughly with the square of input length, so more tokens → much more compute and latency. Enter <strong>REFRAG</strong> from Meta Superintelligence Labs: a clever new pattern that gives you both speed and accuracy.</p>
<p>REFRAG’s core idea is elegantly simple: don’t make the LLM read everything. Instead, <strong>Compress</strong> most of the retrieved text into tiny dense summaries, <strong>Sense</strong> which pieces contain literal facts that must survive compression, and <strong>Expand</strong> only those pieces back into raw tokens. The result: tiny token inputs for the LLM, huge context coverage, and literal facts preserved.</p>
<h3 id="heading-why-this-matters-quick">Why this matters (quick)</h3>
<ul>
<li><p><strong>Massive speedups</strong> (Meta reports large TTFT improvements) — fewer raw tokens → much less quadratic attention work.</p>
</li>
<li><p><strong>Better scale</strong> — treat documents that are 10–100x longer than the model’s native window.</p>
</li>
<li><p><strong>Fact fidelity</strong> — numbers, names, clause IDs preserved because those chunks are left as raw text.</p>
</li>
<li><p><strong>Lower memory footprint</strong> — smaller KV cache and cheaper inference.</p>
</li>
</ul>
<h3 id="heading-the-refrag-workflow-plain-language">The REFRAG workflow (plain-language)</h3>
<ol>
<li><p><strong>Retrieve</strong> the set of relevant documents/passages as usual.</p>
</li>
<li><p><strong>Chunk</strong> each retrieved passage into tiny fixed-size windows (e.g., 8–32 tokens).</p>
</li>
<li><p><strong>Compress</strong> most chunks into embeddings via a lightweight encoder.</p>
</li>
<li><p><strong>Sense</strong> which chunks are critical (via a policy — small classifier, heuristic scoring, or an RL agent).</p>
</li>
<li><p><strong>Expand</strong> the critical chunks back into raw tokens; keep the rest as embeddings.</p>
</li>
<li><p><strong>Feed</strong> the LLM a hybrid input: user query (raw tokens) + some raw chunks + many chunk embeddings.</p>
</li>
<li><p>LLM decodes with access to both literal facts (raw tokens) and semantic context (embeddings).</p>
</li>
</ol>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*HVFwOoVZVeChNFn_0_UUAw.png" alt /></p>
<h3 id="heading-real-time-examples-concrete">Real-time examples (concrete)</h3>
<h3 id="heading-1-legal-brief-how-much-was-the-final-settlement-and-which-clause-allowed-punitive-damages">1) Legal brief: “How much was the final settlement and which clause allowed punitive damages?”</h3>
<ul>
<li><p>Retrieval pulls 15 passages across a 200-page brief (8,000 tokens).</p>
</li>
<li><p>REF RAG chunks, encodes, and senses: “$15,340,000” and “Clause 7.1” chunks get <strong>expanded</strong> to raw tokens; the rest are embeddings.</p>
</li>
<li><p>LLM now has exact amount + clause literal text plus semantic context — fast and precise.</p>
</li>
</ul>
<h3 id="heading-2-research-literature-scan-list-all-experiments-that-reported-gt90-accuracy-on-dataset-x">2) Research literature scan: “List all experiments that reported &gt;90% accuracy on dataset X”</h3>
<ul>
<li>Retriever returns dozens of papers. REF RAG compresses background, expands only the chunks that contain numeric results (e.g., “accuracy=93.2% on X”), so the model can list exact numbers and cite the right paper lines without reprocessing entire PDFs.</li>
</ul>
<h3 id="heading-3-support-knowledge-base-chatbot-for-product">3) Support knowledge base (chatbot for product):</h3>
<ul>
<li>Many KB articles with long troubleshooting steps. Compress standard instructions and expand only error codes and configuration lines (e.g., <code>ERR_1357</code>, config file snippets). The chatbot answers quickly and copies literal commands correctly.</li>
</ul>
<h3 id="heading-when-to-expand-vs-compress-intuitions">When to expand vs compress (intuitions)</h3>
<ul>
<li><p><strong>Expand</strong> if chunk contains: exact numbers, named entities, configuration/code, clause IDs, dates, legal citations, or anything where literal text matters.</p>
</li>
<li><p><strong>Compress</strong> if chunk is background, rationale, or stylistic language that only needs to be <em>understood</em>, not reproduced.</p>
</li>
</ul>
<p>A lightweight sensing policy can be a simple classifier that flags chunks when:</p>
<ul>
<li><p>Numeric tokens appear</p>
</li>
<li><p>Named-entity density is high</p>
</li>
<li><p>The chunk’s embedding is unusually similar to the query embedding (and contains digits/special tokens)<br />   You can later replace this with a learned RL policy to optimize downstream metrics.</p>
</li>
</ul>
<h3 id="heading-nodejs-example-a-refrag-style-pipeline">Node.js Example: a REFRAG-style pipeline</h3>
<p>The example below is an end-to-end Node.js implementation that demonstrates the REFRAG pattern. It uses:</p>
<ul>
<li><p>A vector DB (abstracted — examples for Pinecone/Milvus/Weaviate or a simple in-memory store)</p>
</li>
<li><p>OpenAI-style embeddings (you can swap any embedding model)</p>
</li>
<li><p>A simple heuristic “sensing” policy (you can later train an RL policy)</p>
</li>
<li><p>Hybrid input assembly and a call to an LLM for final answer generation</p>
</li>
</ul>
<blockquote>
<p><em>This is example code — adapt models/keys/DBs to your stack. Replace</em> <code>process.env.OPENAI_API_KEY</code> <em>etc. with real keys.</em></p>
</blockquote>
<pre><code class="lang-plaintext">// filename: refrag_example.js
// Node 18+
// Install: npm i axios express body-parser dotenv
// Optional: npm i openai (if you prefer official SDK)
</code></pre>
<pre><code class="lang-plaintext">
import express from "express";
import bodyParser from "body-parser";
import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
</code></pre>
<pre><code class="lang-plaintext">const app = express();
app.use(bodyParser.json());
</code></pre>
<pre><code class="lang-plaintext">/**
 * Config - change to your vector DB / endpoints
 */
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const PORT = process.env.PORT || 3000;
</code></pre>
<pre><code class="lang-plaintext">/**
 * ----- Utilities -----
 */
</code></pre>
<pre><code class="lang-plaintext">// naive tokenizer split for chunking (replace with real tokenizer if needed)
function tokenizeText(text) {
  return text.split(/\s+/);
}
function detokenize(tokens) {
  return tokens.join(" ");
}
</code></pre>
<pre><code class="lang-plaintext">// chunk text into fixed-size chunks (nTokens per chunk)
function chunkText(text, nTokens = 16) {
  const tokens = tokenizeText(text);
  const chunks = [];
  for (let i = 0; i &lt; tokens.length; i += nTokens) {
    chunks.push(detokenize(tokens.slice(i, i + nTokens)));
  }
  return chunks;
}
</code></pre>
<pre><code class="lang-plaintext">/**
 * Embeddings call (OpenAI embedding endpoint example)
 * Replace with your model or local encoder for speed.
 */
async function getEmbedding(text) {
  const resp = await axios.post(
    "https://api.openai.com/v1/embeddings",
    {
      input: text,
      model: "text-embedding-3-small", // example
    },
    { headers: { Authorization: `Bearer ${OPENAI_API_KEY}` } }
  );
  return resp.data.data[0].embedding;
}
</code></pre>
<pre><code class="lang-plaintext">/**
 * Vector DB abstraction:
 * - storeChunks: accepts [{id, chunk, embedding, meta}, ...]
 * - query(queryEmbedding, topK) =&gt; [{id, chunk, score, meta}, ...]
 *
 * For demo, we use an in-memory "vector DB" with cosine similarity search.
 */
const vectorDB = {
  store: [],
</code></pre>
<pre><code class="lang-plaintext">  async storeChunks(chunksWithEmb) {
    this.store.push(...chunksWithEmb);
  },
</code></pre>
<pre><code class="lang-plaintext">  _cosine(a, b) {
    let dot = 0, na = 0, nb = 0;
    for (let i = 0; i &lt; a.length; i++) { dot += a[i] * b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
    return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
  },
</code></pre>
<pre><code class="lang-plaintext">  async queryByEmbedding(queryEmbedding, topK = 10) {
    const res = this.store
      .map(item =&gt; ({ ...item, score: this._cosine(item.embedding, queryEmbedding) }))
      .sort((a,b) =&gt; b.score - a.score)
      .slice(0, topK);
    return res;
  }
};
</code></pre>
<pre><code class="lang-plaintext">/**
 * Simple sensing policy:
 * - accepts a chunk string and its embedding and returns {expand: boolean, reason: string}
 * - heuristic:
 *   * if chunk contains digits, $, % or clause-like tokens -&gt; expand
 *   * if chunk embedding sim to query embedding past threshold and contains punctuation like ':' or '(' -&gt; expand
 *
 * You can replace this with a small classifier or RL model later.
 */
function simpleSensingPolicy(chunk, chunkEmbedding, queryText, queryEmb, simScoreToQuery = 0.8) {
  const literalTrigger = /\d|[$%#]|Clause|clause|Section|§|\bErr_|config|:=/i;
  if (literalTrigger.test(chunk)) return { expand: true, reason: "literal tokens detected" };
  // otherwise compare embeddings if available (here we pass sim externally if computed)
  // For simplicity, user may provide simScoreToQuery computed outside; we'll mock it below
  return { expand: false, reason: "no literal tokens" };
}
</code></pre>
<pre><code class="lang-plaintext">/**
 * Assemble hybrid prompt for LLM:
 * - user query raw tokens
 * - expanded raw chunks (literal strings)
 * - compressed chunks embedded as "summaries" (we'll show their semantic tags)
 *
 * You will need an LLM that can accept a combination of raw tokens and metadata/embedding inputs.
 * Many production systems encode embeddings into the prompt as short natural-language summaries —
 * or use specialized LLMs that accept key/value memory. Below we represent compressed embeddings
 * as small semantic summaries (via a "summarizeChunk" step) to make a simple demo.
 */
async function summarizeChunk(chunk) {
  // Quick summarization via LLM or heuristic; here we truncate and return a tiny summary.
  // Replace with a cheap instruction-tuned model or use the embedding to find keywords.
  if (chunk.length &gt; 120) return chunk.slice(0, 120) + "…";
  return chunk;
}
</code></pre>
<pre><code class="lang-plaintext">/**
 * Call LLM to get final answer (raw prompt assembly)
 */
async function callLLMForAnswer(userQuery, rawChunks, compressedSummaries) {
  // Build a prompt that includes:
  // 1) user's query
  // 2) exact chunks (literal)
  // 3) compressed summaries as bullet points
  let prompt = `You are an assistant grounded on provided evidence.\n\nUser question:\n${userQuery}\n\nEvidence (exact):\n`;
  for (const r of rawChunks) prompt += `- ${r}\n`;
  prompt += `\nContext summaries:\n`;
  for (const s of compressedSummaries) prompt += `- ${s}\n`;
</code></pre>
<pre><code class="lang-plaintext">  // Call OpenAI completion / chat
  const resp = await axios.post(
    "https://api.openai.com/v1/chat/completions",
    {
      model: "gpt-4o-mini", // replace with appropriate model
      messages: [{ role: "system", content: "You are a precise assistant that uses exact evidence." },
                 { role: "user", content: prompt }],
      max_tokens: 400
    },
    { headers: { Authorization: `Bearer ${OPENAI_API_KEY}` } }
  );
</code></pre>
<pre><code class="lang-plaintext">  return resp.data.choices[0].message.content;
}
</code></pre>
<pre><code class="lang-plaintext">/**
 * Main pipeline endpoint: /ask
 * Body: { query: string }
 */
app.post("/ask", async (req, res) =&gt; {
  try {
    const { query } = req.body;
    if (!query) return res.status(400).json({ error: "query required" });
</code></pre>
<pre><code class="lang-plaintext">    // 1) (Demo) retrieve top K docs from DB by running query embedding -&gt; vectorDB.queryByEmbedding
    const queryEmb = await getEmbedding(query);
    const retrievalHits = await vectorDB.queryByEmbedding(queryEmb, 50); // many chunks
</code></pre>
<pre><code class="lang-plaintext">    // 2) For each retrieved chunk: run sensing policy (heuristic)
    // We'll compute a simple cosine sim with query as already in retrievalHits.score
    const rawChunks = [];
    const compressedSummaries = [];
    for (const hit of retrievalHits) {
      const policy = simpleSensingPolicy(hit.chunk, hit.embedding, query, queryEmb, hit.score);
      if (policy.expand) {
        rawChunks.push(hit.chunk);
      } else {
        // compress -&gt; create a short natural-language summary for prompt
        compressedSummaries.push(await summarizeChunk(hit.chunk));
      }
    }
</code></pre>
<pre><code class="lang-plaintext">    // limit counts to keep token lightweight
    const maxRaw = 40;
    const maxSummaries = 200;
    const finalRaw = rawChunks.slice(0, maxRaw);
    const finalSummaries = compressedSummaries.slice(0, maxSummaries);
</code></pre>
<pre><code class="lang-plaintext">    // 3) Call LLM with hybrid input
    const answer = await callLLMForAnswer(query, finalRaw, finalSummaries);
</code></pre>
<pre><code class="lang-plaintext">    res.json({ answer, rawCount: finalRaw.length, summaryCount: finalSummaries.length });
  } catch (err) {
    console.error(err?.response?.data || err.message || err);
    res.status(500).json({ error: "internal", details: err?.message || err });
  }
});
</code></pre>
<pre><code class="lang-plaintext">/**
 * Small route to index some demo docs into in-memory vector DB
 */
app.post("/index", async (req, res) =&gt; {
  try {
    const { docs } = req.body; // docs: [{id, text, meta}]
    if (!Array.isArray(docs)) return res.status(400).json({ error: "docs array required" });
</code></pre>
<pre><code class="lang-plaintext">    const chunksWithEmb = [];
    for (const doc of docs) {
      const chunks = chunkText(doc.text, 16);
      for (let i = 0; i &lt; chunks.length; i++) {
        const chunk = chunks[i];
        const emb = await getEmbedding(chunk);
        chunksWithEmb.push({
          id: `${doc.id}::${i}`,
          chunk,
          embedding: emb,
          meta: { docId: doc.id, ...doc.meta }
        });
      }
    }
    await vectorDB.storeChunks(chunksWithEmb);
    res.json({ indexed: chunksWithEmb.length });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: err.message });
  }
});
</code></pre>
<pre><code class="lang-plaintext">app.listen(PORT, () =&gt; {
  console.log(`REFRAG demo app running on http://localhost:${PORT}`);
});
</code></pre>
<hr />
<h3 id="heading-how-to-try-this-quickly-example-flow">How to try this quickly (example flow)</h3>
<ol>
<li><p>Start the server: <code>node refrag_example.js</code> (set <code>OPENAI_API_KEY</code> in <code>.env</code>).</p>
</li>
<li><p>Index a legal brief or doc collection:</p>
</li>
</ol>
<pre><code class="lang-plaintext">curl -X POST http://localhost:3000/index -H "Content-Type: application/json" -d '{
  "docs": [
    { "id": "brief1", "text": "Clause 7.1 states that punitive damages up to $15,340,000 may be awarded. Final settlement of $15,340,000 was recorded on October 26, 2023. Additional commentary ...", "meta": {"type":"legal"}}
  ]
}'
</code></pre>
<ol>
<li>Ask a query:</li>
</ol>
<pre><code class="lang-plaintext">curl -X POST http://localhost:3000/ask -H "Content-Type: application/json" -d '{"query":"What was the final settlement amount and which clause allowed punitive damages?"}'
</code></pre>
<p>Expected behavior: the pipeline will expand the chunk containing <code>$15,340,000</code> and <code>Clause 7.1</code>, keep the rest compressed, and the LLM returns the exact dollar amount and clause.</p>
<h3 id="heading-notes-amp-practical-tips">Notes &amp; practical tips</h3>
<h3 id="heading-use-a-fast-local-lightweight-encoder-for-chunk-embeddings">Use a fast, local lightweight encoder for chunk embeddings</h3>
<p>For performance, prefer a tiny encoder you can run locally (e.g., a distilled sentence-transformer) rather than remote embeddings for every chunk. Meta’s REFRAG uses a small dedicated encoder — that’s the key to keeping preprocessing fast.</p>
<h3 id="heading-real-rl-agent-vs-heuristics">Real RL agent vs. heuristics</h3>
<p>Start with heuristics (digit detection, named-entity density, similarity thresholds). Once you have metrics (accuracy, precision, latency), train a small RL policy that optimizes the downstream LLM’s answer quality and latency tradeoff.</p>
<h3 id="heading-where-to-store-embeddings">Where to store embeddings</h3>
<p>For production: Pinecone, Milvus, Weaviate, or similar. For prototypes, in-memory or simple SQLite-based vector stores work.</p>
<h3 id="heading-how-to-represent-embeddings-to-the-llm">How to represent embeddings to the LLM</h3>
<p>Most LLM APIs don’t accept raw embeddings alongside tokens. Two common approaches:</p>
<ul>
<li><p>Convert embeddings back to short natural-language <em>summaries</em> (cheap and widely compatible).</p>
</li>
<li><p>Use a model that supports plugging in key/value memories (some research and advanced production stacks do this). REFRAG-style systems often use special model support for dense memory.</p>
</li>
</ul>
<h3 id="heading-metrics-to-optimize">Metrics to optimize</h3>
<ul>
<li><p><strong>TTFT (time-to-first-token)</strong>: how fast the model can start returning an answer.</p>
</li>
<li><p><strong>Answer accuracy</strong>: exact literal correctness for numbers/names.</p>
</li>
<li><p><strong>Memory usage</strong>: KV cache size.</p>
</li>
<li><p><strong>Token cost</strong>: fewer raw tokens → lower cost.</p>
</li>
</ul>
<h3 id="heading-how-to-iterateimprove">How to iterate/improve</h3>
<ol>
<li><p>Replace <code>simpleSensingPolicy</code> with a small fine-tuned classifier that flags chunks that contain <em>verbatim facts</em> (train on labeled chunks).</p>
</li>
<li><p>Replace <code>summarizeChunk</code> with a fast instruction model that produces compact semantic tags for embeddings.</p>
</li>
<li><p>Implement a budgeted selection algorithm (maximize coverage/utility under a raw-token budget).</p>
</li>
<li><p>Train an RL agent with reward = downstream correctness — λ * latency.</p>
</li>
</ol>
<h3 id="heading-tldr-why-refrag-matters">TL;DR — Why REFRAG matters</h3>
<p>REFRAG is a smart, practical way to let LLMs “read” enormous knowledge bases quickly without losing the literal facts that matter. It’s a <em>systems</em> trick: do cheap compression up front, sense which tiny bits must remain literal, and feed the model a hybrid input. You get the speed of embeddings and the exactness of raw text — the best of both worlds.</p>
]]></content:encoded></item></channel></rss>