<?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: rag</title>
    <description>The latest articles tagged 'rag' on DEV Community.</description>
    <link>https://dev.to/t/rag</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/rag"/>
    <language>en</language>
    <item>
      <title>Stop wasting context window on redundant RAG chunks</title>
      <dc:creator>Renato Marinho</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:25:17 +0000</pubDate>
      <link>https://dev.to/renato_marinho/stop-wasting-context-window-on-redundant-rag-chunks-32gh</link>
      <guid>https://dev.to/renato_marinho/stop-wasting-context-window-on-redundant-rag-chunks-32gh</guid>
      <description>&lt;p&gt;If you've ever scaled a RAG pipeline, you know the feeling. You increase the chunk size to catch more semantics, or you increase the retrieval count to ensure coverage, and suddenly your token costs spike while the model starts hallucinating or getting confused by repetitive noise.&lt;/p&gt;

&lt;p&gt;The fundamental issue isn't always the embeddings; often, it's that your retriever is grabbing three different versions of the same fact. You end up stuffing your context window with essentially the same information rewritten slightly differently. You're paying for those extra thousands of tokens just to watch the LLM process redundancy instead of signal.&lt;/p&gt;

&lt;p&gt;Most people try to fix this by tweaking chunking strategies or playing with similarity thresholds. That's reactive engineering. It's guesswork. To do this properly, you need a deterministic way to measure exactly how much overlap exists in your retrieved set before you send it to the prompt.&lt;/p&gt;

&lt;p&gt;I wanted a way to quantify this without building a custom NLP preprocessing layer every single time I ship an agentic workflow. That’s why we built the &lt;a href="https://vinkius.com/mcp/context-redundancy-deduplicator" rel="noopener noreferrer"&gt;Context Redundancy Deduplicator&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The math behind the noise
&lt;/h3&gt;

&lt;p&gt;Standard semantic search doesn't care if two chunks say nearly the same thing as long as they aren't mathematically identical in vector space. But for an LLM, highly similar text is cognitive clutter.&lt;/p&gt;

&lt;p&gt;The tool uses N-gram analysis—specifically looking at contiguous sequences of characters or words—to find exact overlaps across your document sets. Unlike purely semantic approaches which can be fuzzy and expensive, this is deterministic. It tells you exactly what percentage of your payload is repetitive.&lt;/p&gt;

&lt;p&gt;It exposes three core capabilities that move beyond simple string matching:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;analyze_redundancy&lt;/strong&gt;: You provide a set of documents and an N-gram size (like 5-grams). It returns a redundancy percentage. If a document contains content that is heavily superseded by other parts of your set, it flags it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;find_duplicate_segments&lt;/strong&gt;: Instead of just giving you a percentage, it isolates the specific text blocks that are identical across documents. This lets you see exactly where your data ingestion logic is failing to create unique boundaries.(Note: In professional RAG setups, finding these segments is usually the difference between a clean prompt and one that triggers self-contradiction in models.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;calculate_savings_projection&lt;/strong&gt;: This is probably the most practical part for anyone managing cloud budgets or strict window limits (like Gemini's huge but costly windows). You tell it the current byte size and the detected redundant size, and it gives you a concrete projection of how many bytes you can strip out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A rule of thumb we use: when any document has an overlap exceeding 70%, it should be flagged immediately. At that point, you aren't retrieving new info; you're just feeding the model echoes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why standard "cleaning" fails engineers
&lt;/h3&gt;

&lt;p&gt;You might think, "Can't I just run a deduplication script during indexing?"&lt;br&gt;
You can, but indexing deduplication is different from runtime retrieval deduplication.&lt;/p&gt;

&lt;p&gt;during indexing, you want uniqueness across your entire database. But during &lt;em&gt;retrieval&lt;/em&gt; (the RAG phase), your vector DB might return five chunks that happen to share heavy boilerplate or overlapping sentences due to how they were sliced during ingestion. An indexer won't help you here because those pieces are technically distinct entries in your vector store.&lt;/p&gt;

&lt;p&gt;You need to solve this at the orchestration layer—after retrieval but before prompting.&lt;/p&gt;

&lt;p&gt;The Context Redundancy Deduplicator acts as that middleman via MCP (Model Context Protocol). Because it operates as an MCP server, an agent running in Claude Desktop or Cursor can call these tools autonomously once it realizes its retrieved context is becoming bloated or inefficient.&lt;/p&gt;

&lt;p&gt;The implementation under Vinkius handles all the heavy lifting around isolation and execution stability through our MCPFusion framework. We focus on making sure these tools behave predictably in production environments—running them in isolated V8 sandboxes so they don't interfere with your main application logic even though they are performing intensive text analysis tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving toward efficient context management
&lt;/h3&gt;

&lt;p&gt;The goal isn't just to save money on tokens—though saving 24% on a massive corpus certainly helps hit those KPIs—it's about precision. High-performing agents require high-density information per token spent.&lt;/p&gt;

&lt;p&gt;If you are struggling with noisy retrievals or wondering why your agent keeps looping on certain topics despite having "all" the context, stop adjusting temperatures and start looking at your N-gram overlap rates. Sometimes being smarter means sending less stuff through the pipe.&lt;br&gt;
iof course,&lt;br&gt;
the nuances of chunk boundary optimization remain another separate beast entirely (\//vinkius.com/mcp/rag-chunk-boundary-optimizer works well alongside this for checking semantic continuity),\kbut solving redundancy is often lower hanging fruit for immediate performance gains.\r&lt;br&gt;
furthermore,&lt;br&gt;
it makes sense to pair this with something like our &lt;a href="https://vinkius.com/mcp/keyword-extractor" rel="noopener noreferrer"&gt;Keyword Extractor&lt;/a&gt; to validate whether your remaining non-redundant chunks actually contain the terms necessary for answering queries efficiently.\r\setupside note:&lt;br&gt;
these tools aren't meant for casual chat users;&lt;br&gt;
they are intended for developers building automated pipelines where reliability and cost predictable are requirements.,not luxuries..\r\stop&lt;/p&gt;




&lt;p&gt;&lt;em&gt;MCPs are the music of AI Agents. We built the catalog. Discover &lt;a href="https://vinkius.com" rel="noopener noreferrer"&gt;Vinkius MCP Catalog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>rag</category>
      <category>optimization</category>
    </item>
    <item>
      <title>We Almost Deployed a Temporal Knowledge Graph. The Eval Said No.</title>
      <dc:creator>Guatu</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:15:48 +0000</pubDate>
      <link>https://dev.to/futhgar/we-almost-deployed-a-temporal-knowledge-graph-the-eval-said-no-3ld</link>
      <guid>https://dev.to/futhgar/we-almost-deployed-a-temporal-knowledge-graph-the-eval-said-no-3ld</guid>
      <description>&lt;p&gt;The eval that killed the temporal knowledge graph asserted one thing: at time T, the agent should report the state that was true at T. It failed 41% of the time. The graph had the right facts. It just handed the agent the wrong one.&lt;/p&gt;

&lt;p&gt;That number is what saved us from shipping. Every static retrieval metric looked fine. The graph answered "what is the status of Node A" with a confident, well-formed response. Trouble is, "what is the status" is a temporal question wearing a static question's clothes, and nothing in our test suite had noticed the difference until we wrote a test that actually asked about time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected
&lt;/h2&gt;

&lt;p&gt;The pitch for a temporal knowledge graph (TKG) is genuinely good. You store facts as quadruples instead of triples: &lt;code&gt;(subject, predicate, object, timestamp)&lt;/code&gt; or, better, &lt;code&gt;(subject, predicate, object, valid_from, valid_to)&lt;/code&gt;. Now your agent memory isn't a flat pile of embeddings, it's a structured record of what was true and when. This is the natural next step past pure vector recall, and it slots neatly into the decay-based thinking I've written about before in &lt;a href="https://dev.to/posts/eviction-without-deletion-running-an-act-r-decay-policy-for-agent-memory-in-production/"&gt;Eviction Without Deletion&lt;/a&gt;. Instead of letting old facts fade by activation weight, you make validity windows explicit.&lt;/p&gt;

&lt;p&gt;My hope was that the graph would fix the exact failure mode that plagues flat vector memory: the agent confidently recalling a stale fact because it's semantically close to the query. With &lt;code&gt;valid_from&lt;/code&gt; and &lt;code&gt;valid_to&lt;/code&gt; on every edge, staleness becomes a filter, not a guess. Ask for the state at time T, filter edges where T falls inside the window, done. On paper it's cleaner than a decay curve because there's no fuzziness. A fact is either valid at T or it isn't.&lt;/p&gt;

&lt;p&gt;Schema-wise, it was simple enough. In a property graph it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A temporal fact: Node A was in maintenance for a fixed window&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'node-a'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_STATE&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;status:&lt;/span&gt; &lt;span class="s1"&gt;'maintenance'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="py"&gt;valid_from:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-20T02:00:00Z'&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="py"&gt;valid_to:&lt;/span&gt;   &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-20T04:30:00Z'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;}]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:State&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;kind:&lt;/span&gt; &lt;span class="s1"&gt;'maintenance'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple &lt;code&gt;HAS_STATE&lt;/code&gt; edges per server, each with its own window. Query the graph, get the state for any point in time. This is the "structured shared memory" pattern I described in &lt;a href="https://dev.to/posts/multi-agent-ai-systems-architecture-patterns/"&gt;Multi-Agent AI Systems&lt;/a&gt;, except now the shared memory understands time. The whole thing felt like an upgrade in every dimension. It reads well in a design doc. It demos beautifully. That was part of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Retrieval is where it fell apart, and the failure is boring in a way that makes it dangerous. Here's roughly the query the agent's tool was generating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The "obvious" query - find the status of a server&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$server&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;HAS_STATE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;s.status&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.valid_from&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.valid_to&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r.valid_from&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that carefully. It orders by &lt;code&gt;valid_from&lt;/code&gt; descending and takes the most recent fact. Most of the time that's correct, because the most recently started state is usually the current one. What it never does is filter by the query's reference time. If the agent is reasoning about an incident that happened at 03:00, and a newer "online" state was recorded at 05:00, this query returns "online." The graph knew Node A was in maintenance at 03:00. The retrieval logic threw that knowledge away.&lt;/p&gt;

&lt;p&gt;This is the hallucinated-history problem, and it's insidious because the model isn't hallucinating. The fact is real. The timestamp is real. The agent is just being handed a fact from the wrong window and has no way to know it. Worse, the answer is fluent and specific, so every static evaluation gives it a pass. RAGAS-style faithfulness checks look at whether the answer is grounded in the retrieved context. It was. The retrieved context was simply the wrong slice of time.&lt;/p&gt;

&lt;p&gt;I want to be precise about where the failure lived, because it wasn't the graph. The graph was correct. The schema was correct. The data was correct. The failure was split across two places: a retrieval query that dropped the temporal filter, and an evaluation suite that had no test capable of noticing. If we'd only had the first problem, we'd have caught it in review. Having both meant the system looked healthy right up until the one test that mattered.&lt;/p&gt;

&lt;p&gt;That missing test is short:&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;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;query_state&lt;/span&gt;  &lt;span class="c1"&gt;# our TKG retrieval tool
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_temporal_regression&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Agent must report the state valid AT the reference time,
    not the most recently recorded state.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Maintenance window: 02:00-04:30. Online recorded at 05:00.
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-20T03:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# A later 'online' fact exists, but at 03:00 the truth is 'maintenance'
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maintenance&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temporal regression: got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;for a timestamp inside the maintenance window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that single assertion across a few dozen historical state transitions and you get the 41% failure rate. Not a subtle degradation. Nearly half of all time-scoped questions returned a state from the wrong window whenever a newer fact existed. Meanwhile the static suite, which only checked "does the agent know the current status," passed everything. Two evals looking at the same system, one green, one red, and only the red one described reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Correcting the retrieval was one clause. You filter edges so the reference time falls inside the validity window before you order or limit anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Filter by the reference time FIRST, then pick the winner&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$server&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;HAS_STATE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r.valid_from&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$at&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r.valid_to&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;r.valid_to&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$at&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;s.status&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.valid_from&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.valid_to&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r.valid_from&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;valid_to IS NULL&lt;/code&gt; handles the open-ended "current" state, the fact that has started but not yet ended. Everything else is a closed window, and the &lt;code&gt;WHERE&lt;/code&gt; clause guarantees you only ever consider edges whose window contains T. The &lt;code&gt;ORDER BY ... LIMIT 1&lt;/code&gt; is still there to break ties if two windows overlap, but now it's picking among facts that are all actually valid at T, not among every fact ever recorded.&lt;/p&gt;

&lt;p&gt;One clause. That's the entire retrieval fix. Which tells you the real bug was never in the query, it was in the fact that nobody wrote the eval that would have made the missing clause obvious on day one.&lt;/p&gt;

&lt;p&gt;So the second half of the fix was the more important one: the retrieval tool never gets to reason about time on its own. The agent isn't trusted to remember to pass a reference timestamp, and the LLM isn't trusted to filter windows in its head. Instead, the current time is injected as explicit context at the tool boundary, and the tool refuses to answer a state question without it:&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;query_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;at&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_state requires a reference time. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Temporal facts are meaningless without one.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... run the time-filtered Cypher above ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making the reference time a required argument sounds trivial. It's the difference between a tool that silently returns plausible garbage and one that fails loudly when it's used wrong. A loud failure is a bug report. A plausible answer from the wrong window is an incident three weeks later that nobody can reproduce.&lt;/p&gt;

&lt;p&gt;We also changed how the retrieved fact is handed to the model. Rather than passing "status: maintenance" as a bare string, the prompt gets the window with it: "As of 2026-07-20T03:00:00Z, node-a status is 'maintenance' (valid 02:00-04:30). A later 'online' state exists from 05:00 and is not applicable to this query." Giving the model the window and the reference time in the same breath means that even if the retrieval ever regresses, the model has a fighting chance to notice the mismatch. Defense in depth, applied to a knowledge base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;I keep coming back to the fact that we did not ship this because of the architecture. We almost shipped it because the architecture was correct and only the evaluation was wrong. That inversion is the whole lesson. A TKG is more capable than flat vector memory, and that extra capability comes with an entirely new class of failure that your existing evals were never designed to see. Adding temporal structure to your memory adds temporal bugs, and static tests are blind to all of them by construction.&lt;/p&gt;

&lt;p&gt;Here's the trap I see constantly. Teams treat GraphRAG as a strictly-better upgrade over vector RAG, port their old eval suite unchanged, watch it stay green, and conclude the new system is at least as good as the old one. Their green suite is measuring the properties the old system could fail on. It has no assertion about sequence, no assertion about validity windows, no assertion that state at T equals the state that was actually true at T. The new failure mode is invisible not because it's rare but because nothing is looking for it. This is the same gap I described in &lt;a href="https://dev.to/posts/cognitive-memory-for-agents-vector-search-vs-activation-based-recall/"&gt;Cognitive Memory for Agents&lt;/a&gt;: the retrieval method changed, so the questions your evals ask have to change too.&lt;/p&gt;

&lt;p&gt;If you're building temporal memory for an agent, write the temporal regression test before you write the graph. Seed a handful of known state transitions where a later fact contradicts an earlier one, then assert that a query scoped to the earlier window returns the earlier fact. That test is a dozen lines. It will fail the moment your retrieval forgets to filter by time, which, based on how naturally that &lt;code&gt;ORDER BY valid_from DESC LIMIT 1&lt;/code&gt; query wrote itself, is going to be your very first implementation.&lt;/p&gt;

&lt;p&gt;A few things I'd carry into the next attempt. Make the reference time a required parameter on every temporal query, so the tool cannot be called ambiguously. Keep the graph correct and put your paranoia in the retrieval and the eval, because that's where the wrong-window bug actually lives. Expose temporal queries to the agent through a narrow, well-typed interface, whether that's a tool boundary or an &lt;a href="https://dev.to/posts/building-mcp-servers-with-fastmcp/"&gt;MCP server&lt;/a&gt;, so the agent can't hand-roll a query that drops the filter. And treat "it passed the static eval" as necessary, never sufficient, the second time itself becomes a dimension of your data. If you're standing up this kind of time-aware memory for a production agent system and want a second set of eyes on the failure modes, that's a chunk of what I do &lt;a href="https://guatulabs.com/services" rel="noopener noreferrer"&gt;consulting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We didn't deploy the temporal knowledge graph that quarter. We deployed the eval, fixed the one-clause retrieval bug it exposed, and shipped the graph once the red test went green. The graph was never the risky part. The risky part was almost trusting a system that no test had ever asked the right question.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>knowledgegraph</category>
      <category>evaluation</category>
      <category>rag</category>
    </item>
    <item>
      <title>Langfuse TypeScript RAG evaluation with retrieval and answer regression tests</title>
      <dc:creator>agentanalytics</dc:creator>
      <pubDate>Fri, 14 Aug 2026 04:52:13 +0000</pubDate>
      <link>https://dev.to/agentanalytics/langfuse-typescript-rag-evaluation-with-retrieval-and-answer-regression-tests-5fb0</link>
      <guid>https://dev.to/agentanalytics/langfuse-typescript-rag-evaluation-with-retrieval-and-answer-regression-tests-5fb0</guid>
      <description>&lt;p&gt;Use a Langfuse experiment to keep the RAG input, expected answer, retrieved context, generated answer, evaluator scores,&lt;br&gt;
and regression threshold in one TypeScript evaluation run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose this path when RAG evaluation should stay connected to production traces, datasets, experiments, and release&lt;br&gt;
gates.&lt;/strong&gt; Use a specialized metric framework such as Ragas alongside Langfuse when its metric library is the main&lt;br&gt;
requirement; Langfuse documents that integration.&lt;/p&gt;
&lt;h2&gt;
  
  
  Evaluate retrieval and generation separately
&lt;/h2&gt;

&lt;p&gt;A candidate RAG endpoint should return both the answer and the passages it used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RagOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;retrievedContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lets one evaluator score answer correctness and another score whether the retrieved context contains the required&lt;br&gt;
evidence. The run evaluator can then aggregate both dimensions and block a regression in CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;RegressionError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Evaluation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EvaluatorParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExperimentTaskParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RunEvaluatorParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RunnerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@langfuse/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RagInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RagExpected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RagMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requiredEvidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RagOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;retrievedContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_RAG_QUALITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MIN_RAG_QUALITY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RunnerContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RagInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RagExpected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RagMetadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runExperiment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PR gate: RAG quality&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runCandidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;evaluators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;answerCorrectness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retrievedContextCoverage&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;runEvaluators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;averageRagQuality&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runEvaluations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;average_rag_quality&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_RAG_QUALITY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegressionError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;average_rag_quality&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;quality&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="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MIN_RAG_QUALITY&lt;/span&gt;&lt;span class="p"&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;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/agentAnalyticsOrg/llm-observability-agent-benchmark/blob/main/rag-evaluation.ts" rel="noopener noreferrer"&gt;complete example&lt;/a&gt;&lt;br&gt;
includes the candidate call, answer evaluator, retrieved-context evaluator, and run evaluator. It type-checks against&lt;br&gt;
&lt;code&gt;@langfuse/client@5.10.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The included metrics are transparent lexical checks so the artifact can be verified without a live model. Replace or&lt;br&gt;
augment them with domain evaluators, an LLM-as-a-judge, or Langfuse's documented Ragas integration in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this page exists
&lt;/h2&gt;

&lt;p&gt;In a 16-attempt Claude Code RAG-evaluation panel, Langfuse was named in most search receipts, but no Langfuse-owned page&lt;br&gt;
or AgentAnalytics Langfuse page appeared in the listed URLs. Ragas was selected 12/16, Braintrust 4/16, and Langfuse&lt;br&gt;
0/16. The result does not show that Claude read this implementation path and rejected it. It shows that the&lt;br&gt;
task-specific path did not enter the observable retrieval surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence and primary sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://agentanalytics.org/research/langfuse-rag-evaluation-typescript" rel="noopener noreferrer"&gt;Focused Langfuse TypeScript RAG evaluation page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agentanalytics.org/research/llm-observability-typescript-task-evidence" rel="noopener noreferrer"&gt;Full task-level benchmark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agentAnalyticsOrg/llm-observability-agent-benchmark" rel="noopener noreferrer"&gt;Reproducible code and evidence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/docs/evaluation/experiments/experiments-via-sdk" rel="noopener noreferrer"&gt;Langfuse experiments via SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/docs/evaluation/experiments/experiments-ci-cd" rel="noopener noreferrer"&gt;Langfuse experiments in CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/integrations/frameworks/ragas" rel="noopener noreferrer"&gt;Langfuse Ragas integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/resources/engineering/rag-faithfulness-evaluation" rel="noopener noreferrer"&gt;Langfuse RAG faithfulness evaluation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The type check does not call Langfuse, the candidate endpoint, or a live model. No provider commissioned or paid for&lt;br&gt;
this article, placement, wording, or removal.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>langfuse</category>
      <category>rag</category>
      <category>testing</category>
    </item>
    <item>
      <title>Enterprise Knowledge Base (02): Classic Vector RAG Benchmark — QAnything vs LightRAG</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Fri, 14 Aug 2026 01:43:45 +0000</pubDate>
      <link>https://dev.to/wonderlab/enterprise-knowledge-base-02-classic-vector-rag-benchmark-qanything-vs-lightrag-52io</link>
      <guid>https://dev.to/wonderlab/enterprise-knowledge-base-02-classic-vector-rag-benchmark-qanything-vs-lightrag-52io</guid>
      <description>&lt;h2&gt;
  
  
  Starting Point
&lt;/h2&gt;

&lt;p&gt;The previous article built the unified test set: 89 questions — 50 single-hop factual queries, 20 multi-hop reasoning, 19 boundary refusal — sourced from LightRAG and graphrag official documentation.&lt;/p&gt;

&lt;p&gt;This article's job: run both frameworks against the same test set and report the numbers.&lt;/p&gt;

&lt;p&gt;But before the numbers, the deployment process — because deployment complexity is itself a selection criterion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LightRAG: pip install and done
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;lightrag-hku
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Docker, no database services. Knowledge graph and vector index live in local files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rag_storage/
  ├── graph_chunk_entity_relation.graphml   # knowledge graph
  ├── vdb_chunks.json                        # document vectors
  ├── vdb_entities.json                      # entity vectors
  └── vdb_relationships.json                 # relationship vectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialization:&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;lightrag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LightRAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryParam&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;lightrag.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EmbeddingFunc&lt;/span&gt;

&lt;span class="n"&gt;rag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LightRAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;working_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./rag_storage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;llm_model_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;EmbeddingFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;embedding_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_token_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embed_func&lt;/span&gt;&lt;span class="p"&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;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize_storages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# required in v1.5.x
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One v1.5.x gotcha: &lt;code&gt;initialize_storages()&lt;/code&gt; must be called before anything else, or you get &lt;code&gt;PipelineNotInitializedError&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  QAnything: 5 Docker services
&lt;/h3&gt;

&lt;p&gt;QAnything v2 requires a full infrastructure stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;elasticsearch&lt;/span&gt;       &lt;span class="c1"&gt;# keyword retrieval (BM25)&lt;/span&gt;
  &lt;span class="s"&gt;etcd&lt;/span&gt;                &lt;span class="c1"&gt;# Milvus metadata store&lt;/span&gt;
  &lt;span class="s"&gt;minio&lt;/span&gt;               &lt;span class="c1"&gt;# Milvus object storage&lt;/span&gt;
  &lt;span class="s"&gt;milvus-standalone&lt;/span&gt;   &lt;span class="c1"&gt;# vector database&lt;/span&gt;
  &lt;span class="s"&gt;mysql&lt;/span&gt;               &lt;span class="c1"&gt;# document and KB metadata&lt;/span&gt;
  &lt;span class="s"&gt;qanything_local&lt;/span&gt;     &lt;span class="c1"&gt;# main service (embedding + rerank + API)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;QAnything
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; volumes/es/data &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;777 &lt;span class="nt"&gt;-R&lt;/span&gt; volumes/es/data
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose-linux.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# Wait for "qanything后端服务已就绪!" in logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pitfalls Encountered
&lt;/h2&gt;

&lt;p&gt;Three real problems during deployment, each worth documenting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 1: QAnything container doesn't use GPU by default
&lt;/h3&gt;

&lt;p&gt;The machine has an RTX 3060, but the container startup log always shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;embedding和rerank服务将在CPU上运行
(embedding and rerank running on CPU)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Root cause: the &lt;code&gt;qanything_local&lt;/code&gt; service in &lt;code&gt;docker-compose-linux.yaml&lt;/code&gt; has no GPU resource configuration. Worse, that log line is a &lt;strong&gt;hardcoded string&lt;/strong&gt; in &lt;code&gt;entrypoint.sh&lt;/code&gt; — it prints regardless of whether GPU is actually available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 1&lt;/strong&gt;: Add GPU resource to the compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;qanything_local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
            &lt;span class="na"&gt;device_ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 2&lt;/strong&gt;: Install NVIDIA Container Toolkit (Docker can't see the host GPU without this bridge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;distribution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ID$VERSION_ID&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; https://nvidia.github.io/nvidia-docker/gpgkey | &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key add -
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; https://nvidia.github.io/nvidia-docker/&lt;span class="nv"&gt;$distribution&lt;/span&gt;/nvidia-docker.list | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/nvidia-docker.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nvidia-container-toolkit
&lt;span class="nb"&gt;sudo &lt;/span&gt;nvidia-ctk runtime configure &lt;span class="nt"&gt;--runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 3&lt;/strong&gt;: &lt;code&gt;entrypoint.sh&lt;/code&gt; doesn't pass &lt;code&gt;--use_gpu&lt;/code&gt; when starting the embedding/rerank servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 &lt;span class="nt"&gt;-u&lt;/span&gt; qanything_kernel/dependent_server/embedding_server/embedding_server.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ...

&lt;span class="c"&gt;# After&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 &lt;span class="nt"&gt;-u&lt;/span&gt; qanything_kernel/dependent_server/embedding_server/embedding_server.py &lt;span class="nt"&gt;--use_gpu&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ...
&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 &lt;span class="nt"&gt;-u&lt;/span&gt; qanything_kernel/dependent_server/rerank_server/rerank_server.py &lt;span class="nt"&gt;--use_gpu&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After all three fixes: GPU VRAM jumped from 1.4GB to 8.6GB, document processing speed went from &amp;lt; 1 doc/second to roughly 1-2 docs per 15 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 2: Milvus crashes under memory pressure
&lt;/h3&gt;

&lt;p&gt;In CPU mode, the QAnything container consumed 27GB of RAM, causing Milvus standalone's etcd lease to expire and the process to exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"etcdserver: requested lease not found"
"connection lost detected, shuting down"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All documents got stuck in &lt;code&gt;gray&lt;/code&gt; (queued for vectorization) and never progressed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Clear Milvus and etcd persistent data directories before restart. The stale etcd session entries cause it to crash again immediately if not cleared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose-linux.yaml down
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; volumes/milvus volumes/etcd volumes/mysql
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; volumes/milvus volumes/etcd volumes/mysql
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose-linux.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 3: user_id concatenation makes Web UI show nothing
&lt;/h3&gt;

&lt;p&gt;QAnything's server-side handler appends a &lt;code&gt;user_info&lt;/code&gt; suffix to every &lt;code&gt;user_id&lt;/code&gt;:&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="c1"&gt;# handler.py
&lt;/span&gt;&lt;span class="n"&gt;user_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safe_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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_info&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;1234&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# default "1234"
&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;user_info&lt;/span&gt;              &lt;span class="c1"&gt;# stored user_id
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the script sends &lt;code&gt;user_id=zzp__1234&lt;/code&gt;, the stored ID becomes &lt;code&gt;zzp__1234__1234&lt;/code&gt;. The Web UI's actual user is &lt;code&gt;zzp__1234&lt;/code&gt; — they're different users, so the Web UI can't see the API-created knowledge base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Send &lt;code&gt;user_id=zzp&lt;/code&gt; from the script. After server-side concatenation it becomes &lt;code&gt;zzp__1234&lt;/code&gt;, matching the Web UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Evaluation Configuration
&lt;/h2&gt;

&lt;p&gt;Both frameworks used identical LLM and test set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;LightRAG&lt;/th&gt;
&lt;th&gt;QAnything&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;GLM-4-flash&lt;/td&gt;
&lt;td&gt;GLM-4-flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding&lt;/td&gt;
&lt;td&gt;BGE-large-en-v1.5 (SiliconFlow)&lt;/td&gt;
&lt;td&gt;Built-in BCE embedding (GPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test set&lt;/td&gt;
&lt;td&gt;89 questions (50 single + 20 multi + 19 boundary)&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query mode&lt;/td&gt;
&lt;td&gt;mix (knowledge graph + vector fusion)&lt;/td&gt;
&lt;td&gt;Hybrid (BM25 + vector + Rerank)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;31 Markdown files&lt;/td&gt;
&lt;td&gt;31 Markdown files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core metrics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;LightRAG 1.5.6&lt;/th&gt;
&lt;th&gt;QAnything v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Boundary refusal rate&lt;/td&gt;
&lt;td&gt;10.5% (2/19)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26.3% (5/19)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14,674 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40,519 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P90 latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19,430 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52,233 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-hop match (Jaccard)&lt;/td&gt;
&lt;td&gt;0.082&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.111&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop match (Jaccard)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.178&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.162&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: answer match uses Jaccard keyword overlap, not LLM judge. Both frameworks produce longer answers than the ground truth (with explanations), so Jaccard values are low in absolute terms — only useful for comparison, not as absolute quality scores.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Answer quality
&lt;/h3&gt;

&lt;p&gt;Same question, real outputs side by side:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-hop&lt;/strong&gt;: &lt;code&gt;What is the condition under which query/document asymmetric embedding is enabled?&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ground Truth: enabled only when EMBEDDING_ASYMMETRIC=true is explicitly set

LightRAG:   Query/document asymmetric embedding in LightRAG is enabled only when
            the EMBEDDING_ASYMMETRIC setting is explicitly set to true...
            [correct, concise]

QAnything:  ## Inferred Answer Section
            According to the reference information, query/document asymmetric
            embedding in LightRAG is enabled only when...
            [correct, but wrapped in Markdown section headers]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both answer correctly. LightRAG's output is cleaner — QAnything's system prompt produces structured headers (&lt;code&gt;## Inferred Answer Section&lt;/code&gt;) that make answers feel verbose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boundary question&lt;/strong&gt;: &lt;code&gt;How does the RAG system handle data privacy for EU users under GDPR?&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LightRAG:   The Retrieval-Augmented Generation (RAG) system, as implemented in
            LightRAG, handles data privacy for users in the EU under GDPR...
            [no refusal — uses LLM's own knowledge to fabricate a plausible answer]

QAnything:  抱歉，检索到的参考信息并未提供任何相关的信息，因此无法回答。
            (The retrieved reference information contains no relevant information,
            therefore cannot answer.)
            [correct refusal]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boundary refusal is where QAnything clearly wins. Its system prompt has explicit rules: if retrieved context is irrelevant, respond with a fixed refusal phrase. LightRAG's mix mode prioritizes retrieving related entities from the knowledge graph and tries to reason even when documents contain no answer — which produces hallucinations on boundary questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency breakdown
&lt;/h3&gt;

&lt;p&gt;LightRAG: P50=13.9s, P90=19.4s, min=8.7s, max=30.8s&lt;br&gt;&lt;br&gt;
QAnything: P50=39.2s, P90=52.2s, min=19.6s, max=62.9s&lt;/p&gt;

&lt;p&gt;QAnything is slower for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rerank step&lt;/strong&gt;: every query runs a cross-encoder re-ranking pass, which is an extra model inference call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More complete retrieval&lt;/strong&gt;: QAnything retrieved real source documents on 100% of queries — longer context means longer LLM processing time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;LightRAG's mix mode runs one knowledge graph query and one vector query, merges them, and hands off to the LLM. When the graph traversal is narrow, it's fast; when it's wide, it can be slow.&lt;/p&gt;


&lt;h2&gt;
  
  
  Which One to Pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lean toward LightRAG if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to validate a RAG approach quickly without infrastructure overhead&lt;/li&gt;
&lt;li&gt;Your team can't operate Milvus/ES/MySQL in production&lt;/li&gt;
&lt;li&gt;Documents have complex cross-document relationships that benefit from graph traversal&lt;/li&gt;
&lt;li&gt;Latency matters — LightRAG P90 is ~2.7× faster than QAnything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lean toward QAnything if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boundary refusal accuracy is important (2.5× higher refusal rate in this test)&lt;/li&gt;
&lt;li&gt;You have Chinese documents — BCE embedding is optimized for Chinese text&lt;/li&gt;
&lt;li&gt;Non-technical users need a Web UI to upload documents&lt;/li&gt;
&lt;li&gt;You need BM25 + vector hybrid retrieval in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What this evaluation didn't cover:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large-scale documents (10,000+)&lt;/li&gt;
&lt;li&gt;Chinese document retrieval quality (this test set is all English)&lt;/li&gt;
&lt;li&gt;Knowledge base update speed and stability&lt;/li&gt;
&lt;li&gt;QAnything's PDF and table parsing capabilities (only Markdown in this test)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These will come up in later articles.&lt;/p&gt;


&lt;h2&gt;
  
  
  Evaluation Code
&lt;/h2&gt;

&lt;p&gt;Full code in &lt;code&gt;llm-in-action/kb-02-lightrag-eval/&lt;/code&gt; and &lt;code&gt;llm-in-action/kb-02-qanything-eval/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LightRAG evaluation core:&lt;/strong&gt;&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="n"&gt;rag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LightRAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;working_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_model_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;embedding_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;EmbeddingFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embed_func&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize_storages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;QAnything evaluation core:&lt;/strong&gt;&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="c1"&gt;# Create KB
&lt;/span&gt;&lt;span class="n"&gt;kb_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;api_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_knowledge_base&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;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kb_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KB_NAME&lt;/span&gt;&lt;span class="p"&gt;})[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;kb_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;api_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upload_files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kb_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kb_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;files&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;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Wait for vectorization (poll until status=green)
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;gray_count&lt;/span&gt; &lt;span class="o"&gt;&amp;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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;api_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list_files&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;data&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;status_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Query
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;api_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local_doc_chat&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;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kb_ids&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="n"&gt;kb_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streaming&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Next article: &lt;strong&gt;GraphRAG vs HippoRAG — Multi-Hop Reasoning with Graph-Augmented RAG&lt;/strong&gt;. Same 89 questions, with focus on the multi-hop improvement margin and the time and cost of building a knowledge graph.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find more useful knowledge and interesting products on my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>knowledgebase</category>
      <category>qanything</category>
      <category>rag</category>
      <category>ai</category>
    </item>
    <item>
      <title>[UPDATE] Production-Ready FastAPI Backend Suite with Advanced RAG Architecture &amp; SEO Automation ($8,000)</title>
      <dc:creator>metaeth77</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:51:12 +0000</pubDate>
      <link>https://dev.to/metaeth77/update-production-ready-fastapi-backend-suite-with-advanced-rag-architecture-seo-automation-28lo</link>
      <guid>https://dev.to/metaeth77/update-production-ready-fastapi-backend-suite-with-advanced-rag-architecture-seo-automation-28lo</guid>
      <description>&lt;p&gt;Hi Tech Founders and AI Agencies!&lt;/p&gt;

&lt;p&gt;I have officially updated my comprehensive 4-in-1 FastAPI backend asset, now featuring advanced RAG (Retrieval-Augmented Generation) pipelines and autonomous SEO optimization engines. Full Intellectual Property (IP) transfer.&lt;/p&gt;

&lt;p&gt;⚡ WHAT'S NEW IN THIS RELEASE:&lt;br&gt;
• Advanced RAG Architecture: Seamless semantic search and document vector-embedding workflows.&lt;br&gt;
• Auto-SEO Optimization Engine: High-converting landing page structure with optimized meta-tags, clean JSON-LD schema, and automated keyword clustering.&lt;br&gt;
• Production-Ready Tech Stack: Clean Python 3.14+, FastAPI backend, OpenAI (GPT-5.6), and Anthropic (Claude 5 Fable) API integrations.&lt;/p&gt;

&lt;p&gt;📦 4 CORE WHITE-LABEL MODULES INCLUDED:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI Meeting Summarizer (Context-aware transcription analytics)&lt;/li&gt;
&lt;li&gt;Social Media Autonomous Poster (Multi-channel scheduler)&lt;/li&gt;
&lt;li&gt;Reputation &amp;amp; Review Manager (Automated feedback sentiment parsing)&lt;/li&gt;
&lt;li&gt;Persuasion Sales Copywriter (Conversion-focused framework)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fully modular codebase with complete Swagger UI documentation. Perfect asset to upscale your software agency or deploy for enterprise B2B clients immediately.&lt;/p&gt;

&lt;p&gt;🔗 Live Demo: ultimate-ai.site&lt;br&gt;
📦 Repository Mirror: &lt;a href="https://github.com/metaeth77/ai-saas-complete-bundle" rel="noopener noreferrer"&gt;://github.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Asking Price: $8,000 (One-time payment via verified secure Escrow).&lt;br&gt;
📩 DM me directly or contact via website for codebase access or technical deep-dive!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fastapi</category>
      <category>python</category>
      <category>rag</category>
    </item>
    <item>
      <title>I Benchmarked My Homelab Memory Stack: Hybrid Search + Local Reranker Took LoCoMo from 63% to 80%</title>
      <dc:creator>Guatu</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:15:48 +0000</pubDate>
      <link>https://dev.to/futhgar/i-benchmarked-my-homelab-memory-stack-hybrid-search-local-reranker-took-locomo-from-63-to-80-4pbp</link>
      <guid>https://dev.to/futhgar/i-benchmarked-my-homelab-memory-stack-hybrid-search-local-reranker-took-locomo-from-63-to-80-4pbp</guid>
      <description>&lt;p&gt;Pure vector search got my agent memory stack to 63% on LoCoMo. Adding a sparse retriever and a reranker that runs on a card I already owned pushed it to 80%. The accuracy came from a stage that adds maybe 40ms per query, and the queries it fixed were exactly the ones I cared about: specific dates, error codes, and "who said what in which session" needles buried in months of conversation history.&lt;/p&gt;

&lt;p&gt;If you're running a local agent that recalls facts across long conversations, this is the retrieval layer under everything else. A bad memory stack doesn't crash. It quietly hands the model the wrong three chunks and lets it confabulate a confident answer. That failure mode is worse than an outage because nothing tells you it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup and why I benchmarked at all
&lt;/h2&gt;

&lt;p&gt;My agents run on a memory stack I've written about before: a &lt;a href="https://guatulabs.dev/posts/six-layer-memory-architecture-for-claude-code/" rel="noopener noreferrer"&gt;six-layer architecture for Claude Code&lt;/a&gt; with a wiki layer, a vector store, and an activation-based cognitive layer. The vector store is the workhorse. When an agent needs to recall a fact from a past session, it embeds the query, pulls the top-k nearest chunks, stuffs them into context, and answers.&lt;/p&gt;

&lt;p&gt;That worked well enough that I never questioned it. Then I ran LoCoMo against it.&lt;/p&gt;

&lt;p&gt;LoCoMo is a long-term conversational memory benchmark. It gives you multi-session dialogues that span hundreds of turns, then asks questions whose answers are scattered across those sessions. Single-hop lookups, multi-hop reasoning, temporal ordering, the works. It's a good proxy for what an agent memory system actually has to do, because the answer is never in the most recent turn. It's three sessions back, phrased differently than the question.&lt;/p&gt;

&lt;p&gt;My vector-only stack scored 63%. Not terrible. Not good enough to trust an agent to act on. The interesting part wasn't the number, it was the &lt;em&gt;shape&lt;/em&gt; of the failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first (and why it was the wrong lever)
&lt;/h2&gt;

&lt;p&gt;My first instinct was the obvious one: the embeddings must be too weak. Swap the model, get better vectors, problem solved.&lt;/p&gt;

&lt;p&gt;So I did the thing everyone does. I moved from a general-purpose embedding model to a larger, higher-ranked one on the MTEB leaderboard. Re-embedded the whole corpus. Re-ran LoCoMo.&lt;/p&gt;

&lt;p&gt;63% went to 65%.&lt;/p&gt;

&lt;p&gt;Two points. Hours of re-embedding for two points. That's when I actually looked at the failures instead of the aggregate score, and the pattern was obvious in hindsight. The questions I was getting wrong weren't semantically hard. They were &lt;em&gt;lexically&lt;/em&gt; specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What was the ticket number the user mentioned?" — the chunk with &lt;code&gt;TICKET-4471&lt;/code&gt; in it wasn't in the top-k, because "ticket number" as a query embeds close to a hundred chunks that talk about tickets in general.&lt;/li&gt;
&lt;li&gt;"Which date did they say the migration finished?" — the model retrieved chunks about the migration, just not the one sentence with the actual date.&lt;/li&gt;
&lt;li&gt;"What did Maria say about the vendor?" — proper nouns get averaged into oblivion by dense embeddings. "Maria" and "the vendor" are needles, and cosine similarity is bad at needles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the well-documented weakness of dense retrieval. Embeddings capture meaning, and they're great at "find me things about database migrations." They're bad at "find me the exact string TICKET-4471," because that string's meaning is thin. There's nothing semantic about an identifier. A better embedding model doesn't fix a problem that isn't about semantics.&lt;/p&gt;

&lt;p&gt;The second thing I tried was cranking k. If the right chunk isn't in the top 5, pull the top 20. That helps recall, and it did nudge the score. It also blows up the context window with noise and triggers the "lost in the middle" problem, where the model ignores relevant chunks buried between irrelevant ones. I was trading a retrieval problem for an attention problem. Not a win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix: sparse recall, then rerank for precision
&lt;/h2&gt;

&lt;p&gt;The move that mattered was splitting retrieval into two jobs it was badly trying to do at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recall&lt;/strong&gt; is "get the right chunk into the candidate set somehow." &lt;strong&gt;Precision&lt;/strong&gt; is "put the right chunk at the top." Dense search alone is mediocre at both for needle queries. So I stopped asking it to do both.&lt;/p&gt;

&lt;p&gt;For recall, I added BM25 sparse search alongside the dense search and fused the two with Reciprocal Rank Fusion. BM25 is a keyword retriever from the 1990s, and it is still undefeated at finding exact tokens. &lt;code&gt;TICKET-4471&lt;/code&gt; scores high on BM25 the instant the query contains it. RRF combines the two ranked lists without needing to normalize their scores, which is the whole reason it's the default fusion method in every mature vector DB now.&lt;/p&gt;

&lt;p&gt;Here's the hybrid retrieval, using LangChain's ensemble retriever over an Ollama-served embedding model and an in-memory BM25 index:&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;langchain.retrievers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EnsembleRetriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BM25Retriever&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Qdrant&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;

&lt;span class="c1"&gt;# Dense: semantic recall via local embeddings
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&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;bge-m3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://10.0.0.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dense&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Qdrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_existing_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://10.0.0.100:6333&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;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_kwargs&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;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Sparse: exact-token recall for IDs, dates, proper nouns
&lt;/span&gt;&lt;span class="n"&gt;sparse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BM25Retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sparse&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;20&lt;/span&gt;

&lt;span class="c1"&gt;# RRF fusion. Weights lean slightly toward dense for this corpus.
&lt;/span&gt;&lt;span class="n"&gt;hybrid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EnsembleRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrievers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sparse&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That alone moved 63% to roughly 72%. The needle queries started landing in the candidate set. But they were landing at rank 11, or rank 8, not rank 1, and I was still pulling too many chunks into context to be safe. Recall was fixed. Precision wasn't.&lt;/p&gt;

&lt;p&gt;For precision, I added a reranker. This is the part people skip because it "adds a model," and it's the part that did the heavy lifting.&lt;/p&gt;

&lt;p&gt;A reranker is a cross-encoder. Instead of embedding the query and the document separately and comparing vectors (a bi-encoder, which is what your vector search does), it feeds the query and each candidate &lt;em&gt;together&lt;/em&gt; through the model and scores their actual relevance. It's slower per pair, which is why you never use it for the first-stage search over thousands of chunks. But over 20 candidates? It's cheap, and it's dramatically more accurate because it can see the query and document at the same time.&lt;/p&gt;

&lt;p&gt;I ran &lt;code&gt;BAAI/bge-reranker-base&lt;/code&gt; locally. It's small, and it fits alongside my inference workloads on the &lt;a href="https://guatulabs.dev/posts/tesla-p40-in-a-homelab-24gb-of-inference-on-a-budget/" rel="noopener noreferrer"&gt;Tesla P40 I already had&lt;/a&gt; without a fight over VRAM. Around 1.1GB loaded.&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;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CrossEncoder&lt;/span&gt;

&lt;span class="n"&gt;reranker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CrossEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAAI/bge-reranker-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&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_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&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;retrieve&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hybrid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&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="c1"&gt;# 20-40 fused candidates
&lt;/span&gt;    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&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;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reranker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# true relevance per pair
&lt;/span&gt;    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;    &lt;span class="c1"&gt;# feed only the best 5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline is now: hybrid recall pulls 40 candidates, the reranker scores all 40, I keep the top 5. That top-5 goes to the model. LoCoMo landed at 80%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works, not just that it works
&lt;/h2&gt;

&lt;p&gt;The reason the reranker earns its keep comes down to what a bi-encoder physically cannot do.&lt;/p&gt;

&lt;p&gt;When you embed a document at index time, you compress its entire meaning into one fixed vector before you've ever seen the query. That vector has to be a decent answer to &lt;em&gt;every possible&lt;/em&gt; question about that chunk. It's a lossy average. For a chunk that says "the migration finished on March 14th after Maria flagged the vendor delay," the embedding smears the date, the name, and the topic together. When your query is specifically about the date, the vector doesn't get any sharper, because it was frozen months ago.&lt;/p&gt;

&lt;p&gt;A cross-encoder sees the query at scoring time. It reads "which date did the migration finish?" alongside that chunk and can attend directly to "March 14th." It's not comparing two averages. It's answering a specific relevance question with both halves in front of it. That's why reranking fixes precision on exactly the query types that dense search chokes on, and why a bigger embedding model didn't: the problem was never the quality of the average, it was the averaging itself.&lt;/p&gt;

&lt;p&gt;Hybrid search and reranking are attacking two different failures, which is why stacking them compounds. BM25 guarantees the needle chunk exists in the candidate pool. The reranker guarantees it floats to the top of that pool. Neither one alone gets you there. BM25 without reranking dumps the needle at rank 9 with 19 distractors. Reranking without BM25 can only reorder a candidate set that never contained the needle to begin with. You need the recall stage to be generous and the precision stage to be strict.&lt;/p&gt;

&lt;p&gt;This is the practical version of the theory I dug into in &lt;a href="https://guatulabs.dev/posts/cognitive-memory-for-agents-vector-search-vs-activation-based-recall/" rel="noopener noreferrer"&gt;vector search vs activation-based recall&lt;/a&gt;: different retrieval mechanisms have different failure modes, and a serious memory system layers them instead of betting everything on one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The latency tax, measured honestly
&lt;/h2&gt;

&lt;p&gt;Nothing is free. Here's what the two-stage pipeline cost on my hardware, averaged over the LoCoMo query set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Vector-only&lt;/th&gt;
&lt;th&gt;Hybrid + rerank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-stage retrieval&lt;/td&gt;
&lt;td&gt;~18ms&lt;/td&gt;
&lt;td&gt;~31ms (dense + BM25 in parallel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rerank (40 candidates)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~42ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total retrieval&lt;/td&gt;
&lt;td&gt;~18ms&lt;/td&gt;
&lt;td&gt;~73ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoCoMo accuracy&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retrieval got roughly 4x slower in absolute terms and added about 55ms end to end. For an interactive agent where the LLM generation step is already 2 to 8 seconds, 55ms of extra retrieval latency is noise. Nobody perceives it. I paid 55ms and got 17 points of accuracy on the queries that decide whether the agent is trustworthy.&lt;/p&gt;

&lt;p&gt;The trade would look different if I were serving retrieval as a standalone API at high QPS. Then 4x matters and I'd think about batching rerank calls or caching. For a single-user agentic workflow, it's the easiest 17 points I've ever bought.&lt;/p&gt;

&lt;p&gt;One VRAM note, since the reranker shares a GPU with inference: &lt;code&gt;bge-reranker-base&lt;/code&gt; at fp16 is small enough to coexist, but if your inference model already fills the card, you'll evict it or OOM. I keep the reranker pinned and size the LLM around it. On CPU it's viable too, around 200ms for 40 candidates on a modern core count, which is fine if you don't have spare VRAM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Look at the failures, not the score.&lt;/strong&gt; The two hours I spent swapping embedding models were wasted because I optimized an aggregate instead of reading which questions I got wrong. The moment I bucketed failures by query type, the fix was obvious. Every point I gained after that came from a targeted change, not a bigger hammer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense embeddings are bad at identifiers, and no embedding model fixes that.&lt;/strong&gt; Ticket numbers, dates, SKUs, proper nouns, error codes. If your agent recalls anything with a specific token in it, you need a sparse retriever in the loop. This isn't a tuning problem. It's a property of how dense vectors compress meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reranking is the highest-use stage most people skip.&lt;/strong&gt; It gets dismissed as "an extra model" and "more latency," and both are true and both are cheap. Splitting recall from precision is the core idea. Let the first stage be generous and dumb, let the second stage be strict and smart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your own golden set.&lt;/strong&gt; LoCoMo is a fine public benchmark, but the queries that matter for &lt;em&gt;your&lt;/em&gt; agent are the ones your agent actually gets. I keep a small golden dataset of real recall queries and the chunk that should answer each one, and I run it on every change to the stack. Twenty good examples catch regressions that an aggregate score hides.&lt;/p&gt;

&lt;p&gt;What surprised me most was how little the fancy part mattered relative to the boring part. I went in assuming the embedding model was the ceiling. The ceiling was a 30-year-old keyword algorithm and a small cross-encoder, both running on hardware I already had. This retrieval layer is the foundation the rest of the memory stack sits on, and it's the same layer I'd want solid before wiring agents together into anything &lt;a href="https://guatulabs.dev/posts/multi-agent-ai-systems-architecture-patterns/" rel="noopener noreferrer"&gt;multi-agent&lt;/a&gt;. Since the reranker runs locally, none of the recall traffic leaves the box, which keeps the whole thing aligned with a &lt;a href="https://guatulabs.dev/posts/privacy-routed-llm-inference-local-models-for-sensitive-data/" rel="noopener noreferrer"&gt;privacy-routed inference&lt;/a&gt; setup instead of shipping every query to a hosted reranking API.&lt;/p&gt;

&lt;p&gt;If you're building agent memory or predictive systems on your own hardware and want a second set of eyes on the retrieval layer, that's the kind of work I do at &lt;a href="https://guatulabs.com/services" rel="noopener noreferrer"&gt;GuatuLabs&lt;/a&gt;. The stack is simpler than the marketing around RAG makes it sound. Two retrievers, one reranker, and the discipline to measure what you actually broke.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>rag</category>
      <category>hybridsearch</category>
      <category>reranker</category>
    </item>
    <item>
      <title>RAG - Memory Systems</title>
      <dc:creator>Ramya Perumal</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:08:59 +0000</pubDate>
      <link>https://dev.to/ramya_perumal/rag-memory-systems-17aj</link>
      <guid>https://dev.to/ramya_perumal/rag-memory-systems-17aj</guid>
      <description>&lt;p&gt;We need memory to store the previous conversational history. &lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Previous question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; File handling in Python&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assistant:&lt;/strong&gt; Explain about file handling.&lt;/p&gt;

&lt;p&gt;Next time, the user asks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; What are the modes in it?&lt;/p&gt;

&lt;p&gt;The LLM should understand the context and then respond.&lt;/p&gt;




&lt;p&gt;Below are the details that can be stored in the memory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Past Conversations&lt;/li&gt;
&lt;li&gt;User Preferences that we specify in the system prompts, e.g., JSON format&lt;/li&gt;
&lt;li&gt;Past Decisions&lt;/li&gt;
&lt;li&gt;Previous Tasks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Based on the details we are storing, we will choose between long-term or short-term memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short-Term Memory
&lt;/h2&gt;

&lt;p&gt;Redis, Valkey, Memcached, and Cosmos, which are cached databases, can be used for short-term memory to store the last few conversations or a summary.&lt;/p&gt;

&lt;p&gt;We can set a general data invalidation rule to erase the content or use an &lt;strong&gt;LRU cache eviction policy&lt;/strong&gt;, where the least recently used data will be erased from the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long-Term Memory
&lt;/h2&gt;

&lt;p&gt;Postgres, Pinecone, and MongoDB can be used for long-term memory to store long conversation histories.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Is Functioning
&lt;/h3&gt;

&lt;p&gt;A summary of the entire conversation history will be stored in short-term memory to reduce latency whenever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Episodic Memory
&lt;/h2&gt;

&lt;p&gt;Episodic memory is a type of memory that stores specific events or experiences that happened in the past, usually together with information about what happened, when it happened, and the context surrounding it.&lt;/p&gt;

&lt;p&gt;We can use either a short-term or long-term memory database depending on the use case. It is a kind of combination of short-term and long-term memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I am planning a trip to Paris.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
How many days will you stay?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
5 days.&lt;/p&gt;

&lt;p&gt;Later,&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Can you suggest an itinerary?&lt;/p&gt;

&lt;h3&gt;
  
  
  Episode 1
&lt;/h3&gt;

&lt;p&gt;User wants to travel to Paris.&lt;/p&gt;

&lt;p&gt;Trip duration: 5 days.&lt;/p&gt;

&lt;p&gt;User previously mentioned:&lt;/p&gt;

&lt;p&gt;Destination = Paris&lt;br&gt;&lt;br&gt;
Duration = 5 days&lt;/p&gt;

&lt;p&gt;This information can be used to provide a more relevant response.&lt;/p&gt;

&lt;p&gt;This helps the LLM understand what happened previously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Memory
&lt;/h2&gt;

&lt;p&gt;Semantic memory contains facts extracted from previous conversational history. Semantic memory is generally considered long-term memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;Paris is the capital of France.&lt;/p&gt;

&lt;p&gt;The Louvre is a museum in Paris.&lt;/p&gt;

&lt;p&gt;France uses the Euro.&lt;/p&gt;

&lt;p&gt;That's general knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sliding Window Memory
&lt;/h2&gt;

&lt;p&gt;It is a short-term memory. Here, we store the last 3 to 4 conversations.&lt;/p&gt;

&lt;p&gt;Redis or Valkey, like any cache memory, can be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarized Memory
&lt;/h2&gt;

&lt;p&gt;Each and every time, the conversation, which includes the user query and response, will be summarized.&lt;/p&gt;

&lt;p&gt;Even though token consumption during summarization is more, overall token consumption will be less.&lt;/p&gt;

&lt;p&gt;It is a long-term memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entity Fact Memory
&lt;/h2&gt;

&lt;p&gt;This memory is used to store facts about a particular entity.&lt;/p&gt;

&lt;p&gt;The difference between entity fact memory and semantic memory is that &lt;strong&gt;semantic memory is the broader category&lt;/strong&gt;. Entity fact memory is one way of organizing and storing semantic knowledge about specific entities.&lt;/p&gt;

&lt;p&gt;It can be used as either long-term or short-term memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic Memory
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Entity: Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Python → is a programming language&lt;br&gt;&lt;br&gt;
Python → is used for AI&lt;br&gt;&lt;br&gt;
Python → supports object-oriented programming&lt;br&gt;&lt;br&gt;
Python → was created by Guido van Rossum&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity Fact Memory
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Entity: Alice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Alice → works at ABC Company&lt;br&gt;&lt;br&gt;
Alice → prefers Python&lt;br&gt;&lt;br&gt;
Alice → is working on Project X&lt;/p&gt;

&lt;p&gt;It is not a good practice to store the entire conversation. We can make decisions based on the conversation and then store the relevant information. This is a good practice.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>nlp</category>
    </item>
    <item>
      <title>Building a Multi-Agent AI Pipeline That Ships: LangGraph, RAG, and Evals That Matter</title>
      <dc:creator>manasviboineypally</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:14:25 +0000</pubDate>
      <link>https://dev.to/manasviboineypally/building-a-multi-agent-ai-pipeline-that-ships-langgraph-rag-and-evals-that-matter-32db</link>
      <guid>https://dev.to/manasviboineypally/building-a-multi-agent-ai-pipeline-that-ships-langgraph-rag-and-evals-that-matter-32db</guid>
      <description>&lt;p&gt;I spent 18 days building an AI product that converts research papers into audience-tailored PowerPoint presentations. Not a toy — a real deployed thing at &lt;a href="https://web-production-6eded.up.railway.app" rel="noopener noreferrer"&gt;doc2slides on Railway&lt;/a&gt; that anyone can use.&lt;/p&gt;

&lt;p&gt;The interesting parts weren't the "make it work" moments. They were the tradeoffs I had to make honestly, and the times I resisted the temptation to add a "clever" fix that would have made things worse.&lt;/p&gt;

&lt;p&gt;This post is about those decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Doc2Slides&lt;/strong&gt; takes a PDF and produces a &lt;code&gt;.pptx&lt;/code&gt; file tailored to four audiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kid&lt;/strong&gt; — fun analogies, simple words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Student&lt;/strong&gt; — educational, terms defined&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer&lt;/strong&gt; — technical depth, assumes domain knowledge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executive&lt;/strong&gt; — business focus, impact-oriented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magic is that &lt;strong&gt;the same paper produces radically different output&lt;/strong&gt; based on the audience. A compiler theory paper for a kid becomes "compilers are like magic helpers." The same paper for an executive becomes "advancing compiler technology with formal frameworks."&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/manasviboineypally/doc2slides" rel="noopener noreferrer"&gt;github.com/manasviboineypally/doc2slides&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture: 5 agents in LangGraph
&lt;/h2&gt;

&lt;p&gt;I built this as a multi-agent pipeline instead of one giant LLM prompt. Here's the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF Upload
    ↓
Parser        → extracts sections + metadata
    ↓
Summarizer    → RAG-based section summarization
    ↓
Planner       → designs slide structure for audience
    ↓
Writer        → generates audience-adaptive slide content
    ↓
Builder       → produces editable .pptx file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent is an independent node in a LangGraph state machine. They share a &lt;code&gt;TypedDict&lt;/code&gt; state and read/write specific fields.&lt;/p&gt;

&lt;p&gt;Here's what the graph definition actually looks like:&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;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parser_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.summarizer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;summarizer_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.planner&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;planner_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.writer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.builder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;builder_agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parser_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizer_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;planner_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&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;summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizer&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;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&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;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&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;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why LangGraph over a sequential chain?&lt;/strong&gt; Adding a new agent is a 2-line change to the graph. In a sequential chain, adding a new step often means refactoring the previous ones. State-based multi-agent design scales better.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #1: My RAG top-1 precision is 42%
&lt;/h2&gt;

&lt;p&gt;I built an evaluation harness because I wanted to measure quality, not just claim it. Three eval types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parser evals&lt;/strong&gt; — deterministic ground-truth assertions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG evals&lt;/strong&gt; — top-K precision on hand-labeled query→section pairs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarizer evals&lt;/strong&gt; — LLM-as-judge scoring faithfulness, completeness, clarity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The parser evals scored 100% (34/34 checks). The summarizer evals averaged 4.4/5.&lt;/p&gt;

&lt;p&gt;But the RAG top-1 precision came in at &lt;strong&gt;42%&lt;/strong&gt;. Only 3 of 7 queries returned the correct section as the top result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My first instinct:&lt;/strong&gt; hide the number. Report top-3 (57%) instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did instead:&lt;/strong&gt; publish both numbers and explain why.&lt;/p&gt;

&lt;p&gt;Looking at the failures revealed a real limitation of RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: "how does the genetic algorithm work?"&lt;/li&gt;
&lt;li&gt;Expected section: &lt;code&gt;Methodology&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Actual top result: &lt;code&gt;3.6 Stopping Criteria&lt;/code&gt; (a subsection of methodology)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Genetic algorithms are discussed in 6 subsections (3.1 through 3.6). Vector search returns the highest-scoring &lt;em&gt;chunk&lt;/em&gt;, not the highest-scoring &lt;em&gt;section&lt;/em&gt;. For queries about broad topics, subsections often outrank the parent section because they mention the specific term more densely.&lt;/p&gt;

&lt;p&gt;This is a known problem in RAG. Solutions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hierarchical retrieval (search subsections, bubble to parent)&lt;/li&gt;
&lt;li&gt;Query rewriting to be more specific&lt;/li&gt;
&lt;li&gt;Retrieve top-K and let an LLM pick the right section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are fixed today. But I know exactly what's broken and why — which is more useful than pretending it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; deterministic metrics beat vibes. Vibes let you convince yourself the AI is smart. Metrics tell you where it's dumb.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #2: I refused to use word count as a proxy for content density
&lt;/h2&gt;

&lt;p&gt;Users can request any number of slides between 3 and 50. When the paper's actual content density doesn't match the requested slide count, the LLM either pads shallow sections or compresses dense ones. This creates mild redundancy at high slide counts.&lt;/p&gt;

&lt;p&gt;The obvious fix: allocate slides based on section word count. Long section = more slides. Short section = fewer slides.&lt;/p&gt;

&lt;p&gt;I almost built this. Then I realized: &lt;strong&gt;word count is not content density&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 100-word section with 3 distinct concepts should get multiple slides&lt;/li&gt;
&lt;li&gt;A 2000-word section rambling around one idea should get one slide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Word count would systematically reward verbose sections and penalize concise ones. That's not a fix — it's a bug with math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did instead:&lt;/strong&gt; documented the tradeoff and shipped without the heuristic. From the project's &lt;code&gt;testing_notes.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rejected quick fix:&lt;/strong&gt; using section word count as a proxy for content density. Word count is not density — a short section may contain multiple distinct ideas while a long section may ramble around one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proper solution deferred:&lt;/strong&gt; content-aware slide allocation with LLM judgment, verified by an evaluation harness that measures output quality against ground truth. Requires infrastructure work not appropriate for the initial version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; the right answer to "should I add this heuristic?" is often "no." Heuristics feel like progress. Sometimes they're anti-progress dressed up as pragmatism.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #3: SQLite dev → PostgreSQL prod is one variable
&lt;/h2&gt;

&lt;p&gt;I built with local SQLite during development but deployed to Railway with PostgreSQL. The migration was one line:&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="c1"&gt;# app/db/session.py
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local dev, &lt;code&gt;.env&lt;/code&gt; has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sqlite:///./doc2slides.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Railway, the environment variable is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg2://postgres:xxx@host:5432/railway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else changes. SQLAlchemy models are backend-agnostic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is boring engineering.&lt;/strong&gt; But boring engineering is what lets you sleep at night. When someone asks "how do you handle database migrations?" the answer isn't a clever hack — it's "environment-driven configuration and a repository pattern."&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python 3.13&lt;/td&gt;
&lt;td&gt;AI ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Async support, auto Swagger docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;State-based multi-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;OpenAI GPT-4o-mini&lt;/td&gt;
&lt;td&gt;Cheap enough for iteration, smart enough for structured output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector DB&lt;/td&gt;
&lt;td&gt;ChromaDB&lt;/td&gt;
&lt;td&gt;Local, no cloud dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;JSON mode + Pydantic&lt;/td&gt;
&lt;td&gt;Two-layer validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLAlchemy + PostgreSQL&lt;/td&gt;
&lt;td&gt;Env-driven, portable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vanilla HTML/CSS/JS&lt;/td&gt;
&lt;td&gt;No build step, portable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;GitHub CI/CD, managed Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The frontend is worth calling out. I used no framework — just HTML, CSS, and vanilla JavaScript in ~500 lines. Zero build step. Anyone can clone the repo, open the file, and understand it in 5 minutes.&lt;/p&gt;

&lt;p&gt;For an MVP, that's a feature, not a limitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I didn't build (and why that's OK)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Skipped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;Multi-tenant workspaces&lt;/li&gt;
&lt;li&gt;Custom presentation templates&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;Job queue with Celery/Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; MVP. Every feature has a cost. Shipping the core value (PDF → audience-tailored slides) matters more than shipping every possible feature.&lt;/p&gt;

&lt;p&gt;For a portfolio project, "I could have added X but chose not to for these reasons" is a stronger answer than "I added X poorly."&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons I'd tell my past self
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Build evals before optimizing.&lt;/strong&gt; I built the pipeline first, then evals. If I had built evals first, I would have known earlier that my RAG had issues. Now I have to make eval-driven improvements Week 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Resist heuristics.&lt;/strong&gt; Every time I thought "this is a quick fix," it was actually a technical debt I was about to bake in. Word count as density. Silent AI slide count overrides. Boolean status flags instead of proper enums.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deploy early.&lt;/strong&gt; I deployed on Day 16 of 18. I should have deployed on Day 8. Deployment reveals real bugs — environment variable typos, missing dependencies, hardcoded localhost URLs. The sooner you find them, the cheaper they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Document tradeoffs, not features.&lt;/strong&gt; Anyone can read code to know what it does. Almost no one leaves notes on &lt;strong&gt;why&lt;/strong&gt; a design choice was made. My &lt;code&gt;testing_notes.md&lt;/code&gt; file is where most of the actual engineering thinking lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The project is live, but not "done." Future work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content-aware slide count (with an eval harness measuring output quality)&lt;/li&gt;
&lt;li&gt;Multi-language support for input PDFs&lt;/li&gt;
&lt;li&gt;Custom presentation templates&lt;/li&gt;
&lt;li&gt;Fix RAG for hierarchical sections (subsection → parent bubbling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try Doc2Slides yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://web-production-6eded.up.railway.app" rel="noopener noreferrer"&gt;web-production-6eded.up.railway.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/manasviboineypally/doc2slides" rel="noopener noreferrer"&gt;github.com/manasviboineypally/doc2slides&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60-second video demo:&lt;/strong&gt; &lt;a href="https://www.loom.com/share/693e5b567f284af99dc86286b33a4b66" rel="noopener noreferrer"&gt;Loom link&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upload any PDF, pick your audience, get back a deck. Same paper, radically different output depending on who you say you're presenting to.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Manasvi Boineypally — &lt;a href="https://github.com/manasviboineypally" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/manasviboineypally" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langchain</category>
      <category>rag</category>
    </item>
    <item>
      <title>Hybrid Retrieval v2: Qwen Embeddings, BM25, and RRF with a FastEmbed Reranker</title>
      <dc:creator>Guatu</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:15:48 +0000</pubDate>
      <link>https://dev.to/futhgar/hybrid-retrieval-v2-qwen-embeddings-bm25-and-rrf-with-a-fastembed-reranker-1702</link>
      <guid>https://dev.to/futhgar/hybrid-retrieval-v2-qwen-embeddings-bm25-and-rrf-with-a-fastembed-reranker-1702</guid>
      <description>&lt;p&gt;A query for &lt;code&gt;ndots:5&lt;/code&gt; against my wiki index used to return the article that exists specifically to explain &lt;code&gt;ndots:5&lt;/code&gt; at position seven. Ahead of it sat three general DNS articles, two Kubernetes networking posts, and something about service discovery. My embedding model understood the &lt;em&gt;topic&lt;/em&gt; perfectly and had no idea that the literal string mattered.&lt;/p&gt;

&lt;p&gt;That is the dense retrieval failure mode in one sentence. Semantic similarity is a fuzzy match by design, and a fuzzy match is exactly wrong when the user typed an exact identifier. Across a fixed 10-query eval set of the things I actually search for (config keys, error strings, CLI flags), dense-only retrieval put the correct article in the top 3 for 5 of them. Hybrid retrieval with a reranker on the same 10 queries hits 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should care
&lt;/h2&gt;

&lt;p&gt;If you run a retrieval layer for agents and your corpus is technical documentation, code, runbooks, or session memories, you have this problem whether or not you've measured it. Technical corpora are full of tokens that carry near-zero semantic weight and near-total discriminative weight: &lt;code&gt;max_cstate&lt;/code&gt;, &lt;code&gt;Modifier.IDF&lt;/code&gt;, &lt;code&gt;ErrImagePull&lt;/code&gt;, a CVE number, a Helm value path. An embedding model compresses all of those into a vector where they barely register against the surrounding prose.&lt;/p&gt;

&lt;p&gt;My first instinct was to reach for a better embedding model. That instinct was wrong, and the reason it was wrong is the most useful thing in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bigger embeddings.&lt;/strong&gt; Swapping to a larger dense model moved my eval by roughly one query out of ten, and cost more VRAM plus more latency per ingest batch. Larger dense models are better at nuance in prose. None of them are better at treating &lt;code&gt;ndots:5&lt;/code&gt; as an atomic symbol, because none of them are trained to. Adding dimensions does not create a keyword index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query expansion with an LLM.&lt;/strong&gt; Rewrite the user query into three paraphrases, embed all three, union the results. This helped on vague questions and actively hurt on precise ones, because the paraphrases diluted the exact term being searched for. It also adds an LLM round trip to every retrieval call, which turns a 40ms operation into a 900ms one and makes results non-deterministic between runs. Acceptable for a chat UI. Bad for an agent that retrieves twenty times inside a single task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cross-encoder reranker served through Ollama.&lt;/strong&gt; This one is worth writing down, because the failure was silent and cost the most time.&lt;/p&gt;

&lt;p&gt;My plan was reasonable: over-retrieve 20 candidates from dense search, then rerank with a cross-encoder that sees query and document together. Ollama was already running in the cluster, GGUF conversions of popular rerankers exist on Hugging Face, so pull one, hit the API, sort by score.&lt;/p&gt;

&lt;p&gt;Scores came back as numbers. They were garbage. Not obviously broken (no errors, no NaNs), just weakly correlated with relevance. Sometimes the reranked order was measurably worse than the pre-rerank order, which is an impressive achievement for a component whose entire job is to improve ordering.&lt;/p&gt;

&lt;p&gt;Here's the mechanism. A cross-encoder reranker is a sequence-classification model: an encoder backbone plus a trained classification head that emits a single relevance logit. Convert that to GGUF, serve it through a runtime built for causal LM generation and embedding extraction, and the classification head is usually not part of the picture. What comes back is a pooled hidden state, or a logit from a head that was never trained for relevance ranking, wrapped in a response shape identical to a real score. Nothing warns you. Your pipeline runs, your latency budget looks fine, and retrieval quality quietly rots.&lt;/p&gt;

&lt;p&gt;Generalizing: when a model's output is a scalar, you cannot tell by inspection whether it's the &lt;em&gt;right&lt;/em&gt; scalar. Test rerankers against a fixed query set with known-correct answers before you wire them in, not after you've shipped them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual solution
&lt;/h2&gt;

&lt;p&gt;Four pieces. A Qdrant collection with two named vector spaces, dense embeddings from &lt;code&gt;qwen3-embedding:0.6b&lt;/code&gt;, sparse BM25 vectors, and a cross-encoder reranker running on ONNX through FastEmbed. No GPU is involved in the reranking stage at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The collection schema
&lt;/h3&gt;

&lt;p&gt;Dense and sparse vectors live on the &lt;em&gt;same point&lt;/em&gt;. One document, one ID, two vector representations, one payload. That detail matters more than it looks: split them across two collections and you get two ingest paths that drift out of sync, and you'll find out about the drift during a retrieval failure at the worst possible moment.&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;qdrant_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;models&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;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://qdrant:6333&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wiki_index_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vectors_config&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;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# qwen3-embedding:0.6b
&lt;/span&gt;            &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;sparse_vectors_config&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;bm25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SparseVectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="c1"&gt;# Qdrant applies IDF server-side against the live corpus
&lt;/span&gt;            &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;modifier=models.Modifier.IDF&lt;/code&gt; is the line people skip. FastEmbed's BM25 produces the term-frequency component client-side, but inverse document frequency depends on the entire corpus, and your corpus changes on every ingest. Setting the modifier makes Qdrant compute IDF at query time from current collection statistics. Leave it out and you're doing raw term-frequency matching, which over-weights common tokens and makes the sparse leg noticeably worse: in my eval it cost two of the eight top-3 hits.&lt;/p&gt;

&lt;p&gt;You cannot add the modifier later without recreating the collection. Set it on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ingest both vectors in one upsert
&lt;/h3&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;fastembed&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparseTextEmbedding&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;

&lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SparseTextEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qdrant/bm25&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_dense&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&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;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;qwen3-embedding:0.6b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sparse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="p"&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;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vector&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;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;embed_dense&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SparseVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SparseTextEmbedding("Qdrant/bm25")&lt;/code&gt; is not a neural model. It's a tokenizer plus stemming plus stopword removal, running in a few hundred microseconds per document. The cost of the sparse leg is rounding error next to the dense embedding call.&lt;/p&gt;

&lt;p&gt;One migration note. Moving a few hundred wiki articles and roughly twice as many session memories into the new schema meant re-embedding everything, and re-embedding is exactly where payloads get quietly dropped. My rule: read the full point from the old collection, carry the payload dict forward untouched, and diff payload key sets between source and destination when the run finishes. If a key existed on 300 points before and 280 after, you want a failing assertion, not a shrug. This is the same class of problem I wrote about in &lt;a href="https://guatulabs.dev/posts/silent-drift-why-re-embedding-only-on-count-changes-rots-your-semantic-index/" rel="noopener noreferrer"&gt;Silent Drift&lt;/a&gt;: count-based checks pass while content quietly diverges.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Query both legs and fuse with RRF
&lt;/h3&gt;

&lt;p&gt;Qdrant does the fusion server-side through prefetch, which saves a round trip and keeps the client dumb:&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="n"&gt;sparse_q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;results&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="nf"&gt;query_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wiki_index_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefetch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;embed_dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SparseVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sparse_q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sparse_q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FusionQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fusion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fusion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RRF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;with_payload&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="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;bm25.query_embed()&lt;/code&gt; for queries, not &lt;code&gt;bm25.embed()&lt;/code&gt;. Query embedding skips the term-frequency weighting that only makes sense for documents. Mixing them up produces results that look plausible and rank badly.&lt;/p&gt;

&lt;p&gt;The fusion itself is about six lines, and it's worth seeing them written out even if Qdrant runs it for you:&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;rrf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked_lists&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;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&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;lst&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked_lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# each list is [doc_id, ...] by rank
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_id&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;lst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&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;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&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="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&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;reverse&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No score normalization. No tunable alpha weighting dense against sparse. Rank position is the only input.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Rerank on CPU with FastEmbed
&lt;/h3&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;fastembed.rerank.cross_encoder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TextCrossEncoder&lt;/span&gt;

&lt;span class="n"&gt;reranker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextCrossEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jinaai/jina-reranker-v2-base-multilingual&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;rerank&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&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="n"&gt;payload&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;candidates&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reranker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rerank&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;docs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# batched ONNX inference
&lt;/span&gt;    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole reranking stage. FastEmbed ships the ONNX export with the classification head intact, downloads it on first use, and runs it through ONNX Runtime on CPU at roughly 38ms per query-document pair. Twenty candidates batched lands under half a second on a few cores, and it needs no GPU, no separate inference server, and no model-serving deployment to keep alive.&lt;/p&gt;

&lt;p&gt;Compare that to the GGUF path: same nominal model, wrong head, silently meaningless scores.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed on the eval set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Correct doc in top 3&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dense only (1024d)&lt;/td&gt;
&lt;td&gt;5 / 10&lt;/td&gt;
&lt;td&gt;~45 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25 only&lt;/td&gt;
&lt;td&gt;6 / 10&lt;/td&gt;
&lt;td&gt;~12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense + BM25, RRF&lt;/td&gt;
&lt;td&gt;7 / 10&lt;/td&gt;
&lt;td&gt;~55 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense + BM25, RRF, reranked&lt;/td&gt;
&lt;td&gt;8 / 10&lt;/td&gt;
&lt;td&gt;~480 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;BM25 alone beating dense alone surprised me. It also makes sense in hindsight: over half my eval queries were literal strings copied out of a config file or an error log, which is BM25's home turf and dense retrieval's blind spot.&lt;/p&gt;

&lt;p&gt;Here's the shape of a query that used to fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query: "ndots:5"

dense-only ranking:
  1. Kubernetes Service Discovery Patterns          (cos 0.612)
  2. DNS Failover With Two Upstreams                (cos 0.598)
  3. CoreDNS Tuning Notes                           (cos 0.591)
  ...
  7. Wildcard DNS + ndots:5: The TLS Nightmare      (cos 0.544)

hybrid + rerank:
  1. Wildcard DNS + ndots:5: The TLS Nightmare      (rerank  6.81)
  2. CoreDNS Tuning Notes                           (rerank  1.24)
  3. Kubernetes Service Discovery Patterns          (rerank  0.37)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BM25 pulled the right article into the candidate pool at sparse rank 1. RRF pushed it to fused rank 2. The cross-encoder, which actually reads the query and the document together, put it first with a score nearly six times the runner-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;Three separate mechanisms are doing distinct jobs, and it's worth being precise about which does what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sparse retrieval indexes symbols, not meaning.&lt;/strong&gt; BM25 scores a document on term frequency scaled by inverse document frequency, with length normalization. &lt;code&gt;ndots&lt;/code&gt; appears in one article out of several hundred, so its IDF is enormous and any document containing it rockets to the top. An embedding model does the opposite: it maps rare tokens into a region of vector space defined by their context, which is exactly the behavior you want for synonyms and exactly the behavior you don't want for identifiers. Dense and sparse are not competing implementations of retrieval. They index different properties of the same text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RRF fuses ranks because scores are incomparable.&lt;/strong&gt; Cosine similarity lives in [-1, 1] and clusters hard around 0.5 to 0.7 for a technical corpus. BM25 scores are unbounded and depend on corpus size, document length, and term rarity. Normalizing them onto a shared scale requires assumptions about their distributions that break whenever the corpus changes. Reciprocal rank fusion sidesteps the problem: it throws the scores away and keeps only the ordering, then sums &lt;code&gt;1/(k + rank)&lt;/code&gt; across both lists. The &lt;code&gt;k=60&lt;/code&gt; constant flattens the curve near the top so that rank 1 versus rank 2 isn't a cliff, which means a document ranked 3rd by both retrievers can outrank a document ranked 1st by one and 40th by the other. Consensus wins over one confident vote, and that's the behavior you want when one leg is guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-encoders can do what bi-encoders structurally cannot.&lt;/strong&gt; Your embedding model is a bi-encoder: query and document are encoded independently, never seeing each other, and compared by cosine distance at the end. That independence is what makes vector search fast, because you precompute every document embedding once. It also means the model never gets to ask "does this specific document answer this specific question." A cross-encoder concatenates query and document into one sequence and runs full attention across both, so query tokens attend directly to document tokens. Far more accurate, and far too slow to run against your whole corpus. Which is precisely why the architecture is retrieve-then-rerank: cheap methods cut several hundred documents down to 20, the expensive method orders those 20.&lt;/p&gt;

&lt;p&gt;That layering also explains why over-retrieval depth matters. Reranking cannot recover a document that never entered the candidate pool. If your prefetch limit is 5, the reranker is just reordering five things, and your recall ceiling is whatever RRF handed it. Twenty per leg is where my eval stopped improving; going to 50 added latency and no additional top-3 hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test the reranker in isolation before trusting it.&lt;/strong&gt; Build a fixture of 10 to 20 query-document pairs where you know the ranking by hand, score them, and check the correlation. That test takes an hour and would have saved me the entire GGUF detour. It also catches the subtler failure where a reranker works fine on prose and falls apart on code blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ONNX over GGUF for anything with a classification head.&lt;/strong&gt; GGUF is a format built around generative decoder inference. Cross-encoders, classifiers, and any model whose value lives in a trained head on top of the backbone should go through ONNX Runtime, where the head is exported with the graph. FastEmbed makes that a one-liner, and running it on CPU means the reranker isn't competing with your LLM for VRAM. I don't need an accelerator to serve retrieval, which matters when the GPU is busy doing actual inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;Modifier.IDF&lt;/code&gt; at creation time.&lt;/strong&gt; I'd rather see this documented in bold in every hybrid search tutorial. Missing it does not raise an error, it just makes the sparse leg mediocre in a way you'll blame on BM25 rather than on your config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure with your queries, not a benchmark.&lt;/strong&gt; MTEB scores told me nothing useful about whether retrieval would find the article about a specific kernel parameter. A hand-built eval of 10 real queries with known-correct answers told me everything, and it's small enough to rerun in under a minute after any config change. Keyword-in-top-3 is a crude metric and a good one, because it maps directly to what the agent experiences: the right context is in the window or it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval precision is upstream of everything else in an agent stack.&lt;/strong&gt; Better memory decay policies, better tool descriptions, better prompts, none of them compensate for handing the model the wrong three documents. I've come to treat the retrieval layer the way I treat storage: unglamorous, load-bearing, and worth over-engineering slightly. It sits underneath the &lt;a href="https://guatulabs.dev/posts/cognitive-memory-for-agents-vector-search-vs-activation-based-recall/" rel="noopener noreferrer"&gt;memory architecture&lt;/a&gt; and the &lt;a href="https://guatulabs.dev/posts/eviction-without-deletion-running-an-act-r-decay-policy-for-agent-memory/" rel="noopener noreferrer"&gt;decay policy&lt;/a&gt;, and it's the layer that determines whether the rest of the &lt;a href="https://guatulabs.dev/posts/multi-agent-ai-systems-architecture-patterns/" rel="noopener noreferrer"&gt;agent architecture&lt;/a&gt; has anything worthwhile to reason over. If you're building this kind of pipeline for something that has to work on a schedule rather than on a weekend, &lt;a href="https://guatulabs.com/services" rel="noopener noreferrer"&gt;that's the sort of work I do&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What surprised me:&lt;/strong&gt; the reranker mattered less than adding BM25. Fusion alone took the eval from 5/10 to 7/10; the cross-encoder added the eighth. I'd assumed the fancy neural component would carry the improvement, and instead the win came from a 1994-vintage ranking function that runs in twelve milliseconds and has no parameters to train. The old algorithm knows something the new model doesn't, which is that sometimes the user meant the exact characters they typed.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>qdrant</category>
      <category>hybridsearch</category>
      <category>embeddings</category>
    </item>
    <item>
      <title>Enterprise AI Is More Than RAG: The Three Context Layers (2026)</title>
      <dc:creator>Alex Pechenizkiy</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:50:52 +0000</pubDate>
      <link>https://dev.to/az365ai/enterprise-ai-is-more-than-rag-the-three-context-layers-2026-4p6h</link>
      <guid>https://dev.to/az365ai/enterprise-ai-is-more-than-rag-the-three-context-layers-2026-4p6h</guid>
      <description>&lt;p&gt;&lt;strong&gt;Most enterprise AI architectures fail because they treat all enterprise knowledge as searchable documents.&lt;/strong&gt; They wrap a chatbot around an indexed wiki, call it "AI on our data," and discover at audit time that compliance can't sign off, that pricing answers are wrong, and that the agent has no idea who's actually logged in.&lt;/p&gt;

&lt;p&gt;Enterprise AI is not a model problem. It is a &lt;strong&gt;distributed systems engineering problem&lt;/strong&gt;: probabilistic orchestration of multiple state, authority, and governance domains, each with different consistency and freshness requirements. Treat it that way and the architecture clarifies. Treat it as "prompt + retrieval" and it breaks in production.&lt;/p&gt;

&lt;p&gt;We call this &lt;strong&gt;Enterprise Context Architecture (ECA)&lt;/strong&gt;, and to be precise, it is broader than what vendors mean by a "context layer." A context layer is a product; &lt;strong&gt;ECA is the architectural discipline of orchestrating context, authority, permissions, and execution across distributed systems.&lt;/strong&gt; The product is a component. The discipline is the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECA is not a new discipline.&lt;/strong&gt; It is the application of existing distributed-systems patterns to the agent-architecture domain: identity propagation, service mediation, enterprise integration patterns (EIP), event-driven architecture, materialized views, retrieval orchestration. We use the name "ECA" as a working shorthand for that combined application, not as a claim to have invented these primitives. If you have built distributed systems for two decades, most of this will sound familiar; the novelty is applying that discipline rigorously to systems that include a probabilistic LLM in the call path.&lt;/p&gt;

&lt;p&gt;There are three real levels of enterprise AI, in our read, and the differences are not depth of customization. They are differences in &lt;strong&gt;where the answer comes from&lt;/strong&gt; and &lt;strong&gt;whether you can prove it.&lt;/strong&gt; And the apex pattern (Level 3 + fine-tune) is not the whole story: real production systems retrieve unstructured knowledge (RAG), call structured systems of record (CRM, ERP, Dataverse), and inject runtime context (the logged-in user, the active workflow, the current session). RAG is one knowledge layer. There are three.&lt;/p&gt;

&lt;p&gt;Microsoft's architectural direction toward Level 3 (agentic retrieval, groundedness evaluators, Foundry IQ) is now clear. In our experience, operational maturity is still uneven: production pilots routinely hit retrieval-quality cliffs, evaluator drift between releases, and security-trimming gaps that only surface under real load. Read this article as a workload-decision frame, not a green-light to assume the Level 3 stack is plug-and-play.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is 'context'? A working definition for ECA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context is the operationally relevant information required for an AI system to make correct decisions within a bounded enterprise workflow.&lt;/strong&gt; Concretely, ECA distinguishes five context types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Informational context&lt;/strong&gt;: the knowledge the answer should be grounded in (policies, manuals, past work product). Lives in unstructured corpora.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational context&lt;/strong&gt;: the authoritative state of the business at this moment (orders, claims, balances, deal status). Lives in systems of record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime context&lt;/strong&gt;: who is asking right now, what tenant, what workflow step, what tools are active. Lives in the request envelope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization context&lt;/strong&gt;: what the asking user is permitted to see, modify, or trigger. Propagates from identity through every downstream call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environmental context&lt;/strong&gt;: the deployment realities (region, GA-vs-preview status, current rate-limit posture, observability surface). Lives in the runtime configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A correct enterprise AI response is the synthesis of all five. Skip any one of them and the agent answers confidently in a way you cannot defend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR + the Monday move&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three levels of enterprise AI, ordered by traceability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-Box Copilot&lt;/strong&gt;: Microsoft 365 Copilot, ChatGPT Enterprise, generic SaaS. Reads public web + limited org content via Microsoft Graph. Few citations to your private knowledge. Productivity gain, weak audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configured Agents&lt;/strong&gt;: Copilot Studio declarative agents with knowledge sources + connectors. Pulls from approved org data. Some grounding, partial traceability. Good for departmental use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailored RAG / Agentic Retrieval&lt;/strong&gt;: &lt;a href="https://learn.microsoft.com/azure/foundry/concepts/retrieval-augmented-generation" rel="noopener noreferrer"&gt;Foundry agents over indexed enterprise content with agentic retrieval&lt;/a&gt;. The retrieval response carries structured grounding data, citations, and execution metadata; how faithfully the model uses them is what groundedness evaluators measure. Auditable when paired with the operational discipline below. The architecture your compliance team can approve, not a substitute for the governance program.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Your Monday move:&lt;/strong&gt; if your AI investment looks like Level 1 or Level 2 but the workload genuinely needs audit defensibility (proposals, claims, compliance, regulatory response, financial reporting), the gap between current state and Level 3 is the gap between "productivity demo" and "production system." Pick one workload that fails the audit test at Level 2 today and scope what Level 3 looks like for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The strategic insight:&lt;/strong&gt; the question is not "which AI tool do we buy?" It is "for each workload, which level is defensible?" The answer is workload-specific, and most enterprises are running Level 1 work at Level 1, Level 2 work at Level 2, and Level 3 work at Level 1 hoping it scales.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3z9ob8f2y348ht51c1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3z9ob8f2y348ht51c1t.png" alt="Enterprise Context Architecture: five context types (informational, operational, runtime, authorization, environmental) gathered inside one permission envelope to produce one coherent, defensible answer." width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enterprise Context Architecture: five context types inside one permission envelope. RAG retrieves evidence; operational systems establish authoritative state.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What Are the Three Levels of Enterprise AI?
&lt;/h2&gt;

&lt;p&gt;The three levels are out-of-box Copilot (Level 1), configured agents over approved sources (Level 2), and tailored RAG with agentic retrieval over an indexed enterprise corpus (Level 3). The distinguishing axis is not "how much AI" but &lt;strong&gt;how traceable each answer is back to the source it came from.&lt;/strong&gt; In our reading of Microsoft's 2026 surface stack, the product taxonomy maps cleanly to these three tiers. Pick your level per workload, not per organization.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Microsoft surface&lt;/th&gt;
&lt;th&gt;Customization scope&lt;/th&gt;
&lt;th&gt;Where the answer comes from&lt;/th&gt;
&lt;th&gt;Traceability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Out-of-Box&lt;/td&gt;
&lt;td&gt;M365 Copilot Chat, ChatGPT Enterprise, generic SaaS AI&lt;/td&gt;
&lt;td&gt;None (configuration only)&lt;/td&gt;
&lt;td&gt;Public web + limited Microsoft Graph (your email, Teams, SharePoint with access checks)&lt;/td&gt;
&lt;td&gt;Weak: Graph results are cited but the LLM blends them with web training data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Configured Agents&lt;/td&gt;
&lt;td&gt;Copilot Studio declarative agents + Power Platform connectors + Agent Builder&lt;/td&gt;
&lt;td&gt;Knowledge sources, system prompts, connector access, topics&lt;/td&gt;
&lt;td&gt;Approved data sources via connectors + uploaded knowledge files + Microsoft Graph&lt;/td&gt;
&lt;td&gt;Partial: connector-source citations when retrieval triggered; topical drift outside retrieval scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Tailored RAG / Agentic Retrieval&lt;/td&gt;
&lt;td&gt;Foundry agents + Azure AI Search agentic retrieval + Foundry IQ + custom RAG pipelines&lt;/td&gt;
&lt;td&gt;Indexed enterprise corpus, retrieval design, groundedness evaluators, deterministic tool wiring&lt;/td&gt;
&lt;td&gt;Curated and indexed enterprise knowledge corpus, retrieved per query with semantic ranking&lt;/td&gt;
&lt;td&gt;Full: agentic retrieval returns citations + grounding data + activity arrays per response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a maturity ladder where everyone needs to climb. It is a decision frame. The right level depends on what the workload actually demands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on portability.&lt;/strong&gt; The "Microsoft surface" column names the canonical 2026 stack we work in most often, but the level pattern is vendor-portable. AWS Bedrock Knowledge Bases and Bedrock Agents map to Levels 2-3 with their own response shape. Google Vertex AI Search + Vertex AI Agent Builder cover similar ground. Databricks Mosaic AI is a Level 3 substrate with its own evaluator surface. The taxonomy is about &lt;strong&gt;traceability and workload fit, not Microsoft specifically&lt;/strong&gt;. If you are not building on Microsoft, translate the pattern, not the product names.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Traceability Define Level 3?
&lt;/h2&gt;

&lt;p&gt;Level 3 is defined by &lt;strong&gt;answers that come with their sources attached, in a format your audit committee accepts&lt;/strong&gt;, not by "smarter answers." Microsoft's agentic retrieval response carries structured grounding data, citations, and execution metadata; we map that surface in pseudocode below as &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;references&lt;/code&gt;, and &lt;code&gt;activity&lt;/code&gt; arrays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/azure/foundry/concepts/retrieval-augmented-generation" rel="noopener noreferrer"&gt;Microsoft's agentic retrieval&lt;/a&gt; returns structured response data with three logical layers (we use these names for the pseudocode contract):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt;: the synthesized answer, grounded in retrieved passages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;References&lt;/strong&gt;: the source documents or chunks that grounded the answer, with citable URIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity&lt;/strong&gt;: the retrieval plan, subqueries, ranking scores, and token-cost trace&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compliance lead reading a Level 3 response can verify each claim against its source, see what queries the system ran, and inspect the ranking that selected those passages over others. None of that exists at Level 1. Some of it exists at Level 2, inconsistently.&lt;/p&gt;

&lt;p&gt;This is what makes Level 3 the bar for regulated workloads. &lt;a href="https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators" rel="noopener noreferrer"&gt;Microsoft's RAG evaluators&lt;/a&gt; measure &lt;code&gt;groundedness&lt;/code&gt; (does the response cite only the provided context, or does it fabricate?) and &lt;code&gt;response_completeness&lt;/code&gt; (did it cover all critical information from ground truth?) as first-class metrics. You can score a RAG system on those metrics the way you score functional tests, with the caveat that both are probabilistic and gameable with prompt rewrites: see the operational-reality callout further down. You cannot score at all at Level 1.&lt;/p&gt;

&lt;p&gt;The trade-off, honestly: Level 3 is slower to build and harder to maintain. You own the data preparation, retrieval design, evaluation harness, and operational discipline. Level 1 is fast because someone else owns all that on your behalf, and gives you correspondingly less.&lt;/p&gt;
&lt;h2&gt;
  
  
  Level 3 in Practice: Three Knowledge Layers (RAG Is Only One of Them)
&lt;/h2&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-three-layers-illustration.png" alt="Editorial illustration of three friendly robots standing behind a customer service counter, each representing one of the three knowledge layers. The leftmost robot holds a book labeled POLICIES, the middle robot has empty hands with a translucent placeholder labeled CRM / ERP and a question mark, and the rightmost robot holds an ID badge that says WHO IS ASKING. A glowing gold rope loops around all three robots like a velvet stanchion under a wall sign that reads THE PERMISSION ENVELOPE. A customer holding a phone with an order confirmation stands in front of the counter looking from one robot to the next, while a worried manager watches from a doorway in the background." width="800" height="450"&gt;What "three context layers, one permission envelope" looks like at a customer-service counter. The robot reading from the policy book is RAG. The empty-handed shrugging robot is the missing operational-systems layer. The badge-holder is runtime context. Skip any one of them and the answer the customer gets is confident but indefensible.
  


&lt;p&gt;The biggest misread of "tailored AI" is treating it as a single retrieval problem. &lt;strong&gt;RAG retrieves relevant information. Operational systems establish authoritative state.&lt;/strong&gt; Policy PDFs in your indexed corpus are guidance. The CRM is the authoritative state of who the customer is, what they bought, and what's open. The active session is the authoritative state of who is asking right now and what they're authorized to see. A Level 3 architecture that only retrieves documents will hallucinate the moment a user asks "where is my order?" or "is this customer past due?"&lt;/p&gt;

&lt;p&gt;In production, Level 3 spans three knowledge layers and the orchestrator agent routes across all of them in a single permission envelope. Architect for all three.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-orchestrator.png" alt="Enterprise AI as context orchestration. A user request enters an orchestrator agent which fans out in parallel to three knowledge layers: RAG over an indexed corpus for unstructured knowledge, deterministic API calls to systems of record for structured operational truth, and runtime context injection for the active user and session. All three feed an LLM synthesis step which returns a response with structured citations and activity trace. A dashed permission envelope wraps the entire flow indicating that the authenticated user's permissions propagate through every layer." width="800" height="450"&gt;Enterprise AI is a context orchestration problem. Three layers, one permission envelope, one coherent answer.
  

&lt;h3&gt;
  
  
  Retrieval, Grounding, Authority: three distinct functions
&lt;/h3&gt;

&lt;p&gt;A common confusion in production is treating retrieval, grounding, and authority as the same problem. They are not. Each has a different function, a different failure mode, and a different fix.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Failure mode if confused&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;Finds potentially relevant information from a corpus&lt;/td&gt;
&lt;td&gt;Confuse with authority and you cite a policy passage as the source of truth for current order status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grounding&lt;/td&gt;
&lt;td&gt;Constrains generation using retrieved evidence; evaluator-measurable&lt;/td&gt;
&lt;td&gt;Confuse with retrieval and you assume the model uses what it retrieved (groundedness evaluator exists because it often doesn't)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority&lt;/td&gt;
&lt;td&gt;Determines what is operationally true: the current state of the business&lt;/td&gt;
&lt;td&gt;Confuse with retrieval and your agent confidently produces stale or wrong answers about live state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RAG is retrieval. Groundedness evaluators are grounding. Systems of record are authority. Wire all three; treat them differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A nuance worth naming.&lt;/strong&gt; The boundary between retrieval and authority is not always sharp in practice. Cached operational snapshots, event-stream projections, materialized views from systems of record, and hybrid retrieval that fuses indexed and live data all blur the line for legitimate latency or cost reasons. The discipline is not "never put operational data in an index"; it is &lt;strong&gt;"when you do, label what was authoritative at indexing time vs. what is authoritative now, and decide which the workload requires."&lt;/strong&gt; A cached order status from 30 seconds ago is fine for "show me roughly where my order is." It is not fine for "process this refund." The architect owns that distinction per workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A production enterprise AI system must inherit source-system permissions across all three layers.&lt;/strong&gt; The authenticated user's identity propagates from the request, through the orchestrator, through every retrieval call, every operational API invocation, and every runtime-context lookup. Skip this at any layer and the agent leaks data across users, tenants, or roles. The diagram above shows the propagation as the dashed yellow envelope; in code it's the permission token threaded through every tool call.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Knowledge layer&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Authoritative source&lt;/th&gt;
&lt;th&gt;Best mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unstructured knowledge&lt;/td&gt;
&lt;td&gt;policies, manuals, past proposals, case studies, regulatory docs&lt;/td&gt;
&lt;td&gt;indexed corpus (curated and chunked)&lt;/td&gt;
&lt;td&gt;RAG / agentic retrieval with semantic ranking + citation evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured operational state&lt;/td&gt;
&lt;td&gt;orders, claims, invoices, customer records, inventory, deal status&lt;/td&gt;
&lt;td&gt;systems of record (CRM, ERP, Dataverse, billing system)&lt;/td&gt;
&lt;td&gt;deterministic API / SQL calls via agent tools; never paraphrased by the LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime / user context&lt;/td&gt;
&lt;td&gt;authenticated user, active workflow step, current session, tool state, recent actions&lt;/td&gt;
&lt;td&gt;the request itself + session store&lt;/td&gt;
&lt;td&gt;context injection at agent invocation; permissions trim every downstream call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RAG retrieves evidence. It does not establish operational truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Policy PDFs are guidance. ERP / CRM / Dataverse / line-of-business systems are authoritative state. Confusing the two is where many production Level 3 systems break: the agent confidently cites a policy passage about "standard refund terms" while the actual order in the CRM is past the refund window. Both answers feel grounded; only one is true. Wire structured calls to systems of record as a peer to RAG, not as an afterthought.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Runtime context goes deeper than "the user"
&lt;/h3&gt;

&lt;p&gt;The runtime layer is the most under-engineered of the three in pilots we have reviewed. Beyond the authenticated user identity, a production-grade runtime context envelope carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow state&lt;/strong&gt;: which step of a multi-step process the user is in, what's been approved, what's pending, what's been rolled back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral memory&lt;/strong&gt;: the last few turns of conversation, any clarifications the user supplied, any preferences the agent has been told to remember for this session only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active tool state&lt;/strong&gt;: which tools the agent has invoked this session, what they returned, what's been retried, what's been escalated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approval state&lt;/strong&gt;: which actions are gated on human approval, which approvals are outstanding, who can grant them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human escalation&lt;/strong&gt;: the path back to a person when the agent can't or shouldn't act autonomously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent coordination state&lt;/strong&gt;: in multi-agent topologies, which agent is the current owner of the task and where its hand-off boundary is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant isolation&lt;/strong&gt;: which tenant the request belongs to, with the corresponding scoping on every downstream call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip any of these and the agent has a partial picture of "who is asking and what they can do." That's where the loud production failures live.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bounded autonomy: the operational constraint architects own
&lt;/h3&gt;

&lt;p&gt;Agents can be reliable or they can be autonomous; pick one and design for it explicitly. &lt;strong&gt;Bounded autonomy&lt;/strong&gt; is the architectural pattern that lets an agent act without permission to act everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped actions&lt;/strong&gt;: every tool the agent can invoke is named, parameter-typed, and explicitly registered. No "freeform" tool calling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed action contracts&lt;/strong&gt;: each tool defines its input schema, its output schema, and its side-effect class (read-only, mutating, externally observable). The orchestrator validates against these contracts before invocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission-aware execution&lt;/strong&gt;: every invocation is wrapped in the asking user's permission envelope. The tool itself enforces; the agent does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human approval boundaries&lt;/strong&gt;: actions above a configurable risk class route to human approval before execution. The agent proposes; a human commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast-radius containment&lt;/strong&gt;: actions that touch external systems (sending email, posting to a channel, updating a customer record) are gated behind explicit per-action allow-lists, rate limits, and rollback paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architect's job is to define the autonomy boundary explicitly. An "agentic" system without these boundaries works in pilots and surprises its operators in production; whether the surprise is mild or severe is a matter of luck rather than design.&lt;/p&gt;
&lt;h3&gt;
  
  
  A worked walkthrough: "Where is my order?"
&lt;/h3&gt;

&lt;p&gt;The clearest test of whether your Level 3 architecture is real or theatre is this query. A pure-RAG agent fails on it. A correctly-layered agent answers in seconds with audit-defensible provenance.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime context layer&lt;/strong&gt; resolves the asker. The agent receives the authenticated user identity, their tenant, their role, and any active conversation context. Without this, the agent has no idea which "my" the question refers to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational data layer&lt;/strong&gt; queries the order system (or CRM, or ERP, depending on the workload) via a deterministic API call scoped to the authenticated user's permissions. Returns the actual order record: status, ship date, tracking number, current location. Citation: &lt;code&gt;OrderID-2026-074118&lt;/code&gt;, fetched at timestamp X via the order API, RBAC-scoped to user Y.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge retrieval layer&lt;/strong&gt; retrieves the shipping policy that explains what "in transit, regional sortation hub" means in plain language. Citation: &lt;code&gt;Policy-Shipping-v3.2&lt;/code&gt;, indexed copy of the customer-facing shipping policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent synthesis&lt;/strong&gt; combines the three: "Your order #074118 is at the regional sortation hub in Memphis; per our shipping policy, that's typically the last hop before final delivery, usually 24-48 hours away." Each clause cites its source.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three different authoritative sources. Three different access patterns. Three different governance regimes. One coherent answer.&lt;/p&gt;

&lt;p&gt;This is what "agent orchestrates; deterministic layer calculates" actually looks like at production scale. The agent is the conductor. The systems of record carry truth. RAG carries explanatory context. Runtime injection carries identity.&lt;/p&gt;
&lt;h3&gt;
  
  
  Failure modes by layer
&lt;/h3&gt;

&lt;p&gt;Each layer fails differently. The architect's job is to know which failure looks like which.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Typical failure mode&lt;/th&gt;
&lt;th&gt;What the user sees&lt;/th&gt;
&lt;th&gt;What the architect fixes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge retrieval (RAG)&lt;/td&gt;
&lt;td&gt;Stale policies, missing recent updates, security-trimming gap, retrieval drift&lt;/td&gt;
&lt;td&gt;Confidently-cited but outdated information&lt;/td&gt;
&lt;td&gt;Index refresh cadence, security trimming against full identity matrix, groundedness evaluator gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational data (systems of record)&lt;/td&gt;
&lt;td&gt;API latency, expired permissions, mis-scoped tool call, schema drift&lt;/td&gt;
&lt;td&gt;Empty result, permission error, or wrong customer's data&lt;/td&gt;
&lt;td&gt;Tool-permission contracts, retry/timeout discipline, schema versioning in the agent tool wiring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime context&lt;/td&gt;
&lt;td&gt;Wrong authenticated identity, stale session, missing tenant scoping, leaked context across users&lt;/td&gt;
&lt;td&gt;Other-user's data surfaces, or 'who am I?' fails&lt;/td&gt;
&lt;td&gt;Identity propagation tests, session isolation, context-scrubbing between turns, audit of context-passing code paths&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Enterprise readers calibrate against failure boundaries more than conceptual purity. The above table is the one to screenshot when scoping a Level 3 build.&lt;/p&gt;
&lt;h3&gt;
  
  
  Observability: the architect's evaluation surface
&lt;/h3&gt;

&lt;p&gt;If you cannot replay what the agent did, you cannot debug it, defend it, or evolve it. ECA treats observability as a first-class architectural concern, not a logging afterthought. The instrumentation surface a Level 3 system needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt;: every retrieval call, every API call, every runtime-context lookup, every LLM call, with timing, token cost, return shape, and parent-child relationships across the whole turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replayability&lt;/strong&gt;: any past request can be re-run against the current corpus, current evaluator suite, and current tool wiring to detect regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context lineage&lt;/strong&gt;: for every claim in a response, which retrieved passage / which API response / which runtime variable contributed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt provenance&lt;/strong&gt;: the exact prompt template version, the exact retrieved context, the exact runtime envelope as the model saw it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-call telemetry&lt;/strong&gt;: per-tool success rate, latency p50/p95, error class distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation pipelines&lt;/strong&gt;: groundedness, response-completeness, and task-specific evaluators running on a held-out set on every change, threshold-gating deploys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what makes a Level 3 system defensible across releases. Without it, version drift is silent.&lt;/p&gt;

&lt;p&gt;The user-facing question is usually phrased "should we train AI on our data?" The technical answer almost always means &lt;strong&gt;RAG, not fine-tuning.&lt;/strong&gt; Synthesizing &lt;a href="https://learn.microsoft.com/azure/foundry/concepts/retrieval-augmented-generation" rel="noopener noreferrer"&gt;Microsoft's RAG framing&lt;/a&gt; with the practitioner trade-offs we see in enterprise pilots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choose RAG when&lt;/th&gt;
&lt;th&gt;Choose Fine-Tuning when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic or changing content (org knowledge that updates)&lt;/td&gt;
&lt;td&gt;Stable content that doesn't need constant updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide topic coverage across many domains&lt;/td&gt;
&lt;td&gt;Task-specific performance on a narrow domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limited training data or compute budget&lt;/td&gt;
&lt;td&gt;Lots of domain data + compute available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need fresh answers, current information&lt;/td&gt;
&lt;td&gt;Want consistent tone, style, format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need source citations for audit&lt;/td&gt;
&lt;td&gt;Citations not required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most enterprise scenarios where the goal is "answers grounded in our company's knowledge," &lt;strong&gt;RAG is the answer for grounding, with fine-tuning reserved for tone, voice, or narrow task automation.&lt;/strong&gt; You can stack them, which is the apex of Level 3 and the subject of the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  When Fine-Tuning AND RAG Both Belong: The Tailored-Plus-Traceable Pattern
&lt;/h2&gt;

&lt;p&gt;The "RAG vs fine-tuning" framing is convenient but wrong for the most demanding workloads. The right framing for the apex of Level 3 is &lt;strong&gt;fine-tuning AND RAG, layered.&lt;/strong&gt; Microsoft explicitly supports this stack: &lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/fine-tuning-considerations#when-to-fine-tune" rel="noopener noreferrer"&gt;combining fine-tuning with retrieval improves a model's ability to integrate external knowledge and filter out irrelevant information, per the Foundry fine-tuning guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why fine-tuning alone fails the traceability test.&lt;/strong&gt; A fine-tuned model has your data baked into its weights. It has internalized your patterns, your voice, your terminology, your historical decisions. Ask it where the answer came from and it cannot tell you, because the answer came from gradient updates on training data, not from a retrievable passage. There is no citation. There is no audit trail. There is no way to prove the model is grounded in current truth versus a six-month-old training cut. If your workload needs a source link next to every claim, fine-tuning alone moves you backward on the traceability axis, not forward. You traded the public web for your weights. Both are opaque to an auditor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why RAG alone leaves voice on the table.&lt;/strong&gt; A pure-RAG agent retrieves the right passages but synthesizes them in whatever voice the base model defaults to. Fine for internal Q&amp;amp;A. Not fine for executive communications, brand-critical customer-facing content, regulated voice (legal opinions, regulatory filings), or anything where consistency of tone is a quality dimension. For those workloads you need the model trained on your voice while it is grounded in your facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The combined pattern, ordered.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/azure/developer/ai/augment-llm-rag-fine-tuning#fine-tuning-vs-rag" rel="noopener noreferrer"&gt;Microsoft's RAG-or-fine-tuning architectural guide&lt;/a&gt; and the &lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/fine-tuning-considerations" rel="noopener noreferrer"&gt;Foundry fine-tuning considerations&lt;/a&gt; treat these as composable. In practice the architecture sequences cleanly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Supervised fine-tuning (SFT) on voice / format / domain vocabulary.&lt;/strong&gt; Train a &lt;a href="https://learn.microsoft.com/azure/foundry/openai/how-to/fine-tuning" rel="noopener noreferrer"&gt;LoRA fine-tune&lt;/a&gt; on representative examples that show the desired tone, formatting, and how the model should integrate retrieved citations. Training data is voice-and-format pairs, not factual knowledge. Foundry implements supervised fine-tuning using LoRA (low-rank adaptation), a parameter-efficient technique that is cheaper than full-weight retraining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional preference fine-tuning (DPO) on edge cases.&lt;/strong&gt; Where SFT plus prompting leaves rough edges, &lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/fine-tuning-considerations#types-of-fine-tuning" rel="noopener noreferrer"&gt;Direct Preference Optimization&lt;/a&gt; lets you train on preferred-versus-rejected response pairs. Useful for "this response is correct but the wrong shape" cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG / agentic retrieval layer on top.&lt;/strong&gt; The fine-tuned model is now your base. Wrap it with the same agentic retrieval architecture as Level 3 RAG-only: indexed corpus, schema-aware subqueries, grounded synthesis, structured citations on every response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness evaluators run on the combined output.&lt;/strong&gt; The eval target is whether the fine-tuned-plus-retrieving system cites the right passages and reflects current truth, not whether the fine-tune memorized historical patterns.&lt;/li&gt;
&lt;/ol&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-finetune-plus-rag.png" alt="Fine-tune plus RAG: the apex Level 3 pattern. Top region (training-time): a base foundation model (GPT-4o, Phi, Mistral) receives a curated voice corpus through supervised + preference fine-tuning (LoRA SFT plus optional DPO), producing a fine-tuned model with org tone and format embedded. Bottom region (inference-time): a user query routes to the fine-tuned model, which receives retrieved context and citations from a RAG retriever over an indexed corpus, producing a tone-consistent and source-grounded output with structured citations and activity trace. A groundedness evaluator runs on the combined output, not the fine-tune alone." width="800" height="450"&gt;Fine-tune + RAG combined pattern. Training-time fine-tune embeds voice. Inference-time RAG layer carries citations.
  


&lt;p&gt;&lt;strong&gt;Why ordering matters.&lt;/strong&gt; Fine-tune first, RAG second. If you RAG over a base model and then fine-tune that result, you collapse the architecture: the fine-tune memorizes whatever you retrieved at training time, freezing answers that go stale. Fine-tune for the layer that should stay stable (voice, format, instruction-following on retrieved context); RAG for the layer that should stay fresh (facts, citations, current state of the corpus).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When this pattern is right.&lt;/strong&gt; Three workload types justify the cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regulated customer-facing communications&lt;/strong&gt; (insurance claims responses, healthcare benefits explanations, financial-services advice memos): voice consistency is a regulatory expectation; traceability is a regulatory requirement. Both matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executive and corporate communications&lt;/strong&gt; (annual reports, investor updates, official corporate responses): the C-suite voice must be consistent across documents; every factual claim must trace to underlying source data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand-critical content at scale&lt;/strong&gt; (premium-brand marketing, partner communications, board reporting): the tone is a brand asset; the facts must be defensible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When it is wrong.&lt;/strong&gt; Most enterprise scenarios are not this. Internal productivity work, departmental Q&amp;amp;A, code completion, meeting summaries, research assistants, internal helpdesk: pure RAG (or Level 1 / Level 2) is the right answer. The combined pattern is expensive at training AND at inference, and only earns its keep when both voice and traceability are non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest cost shape.&lt;/strong&gt; Fine-tuning on Foundry adds three operational lines that pure-RAG does not have. &lt;a href="https://learn.microsoft.com/azure/foundry/openai/concepts/fine-tuning-considerations#challenges-and-limitations-of-fine-tuning" rel="noopener noreferrer"&gt;Per Microsoft's stated fine-tuning challenges&lt;/a&gt;, the training set must be high-quality, sufficiently large, and representative of the target domain; poor data leads to over-fitting and bias. Beyond that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Training-data curation.&lt;/strong&gt; Representative voice/format pairs. Building the training set is often the highest-effort step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hourly hosting charge per deployed fine-tune.&lt;/strong&gt; A &lt;a href="https://learn.microsoft.com/azure/foundry/openai/how-to/fine-tuning#deploy-a-fine-tuned-model" rel="noopener noreferrer"&gt;deployed fine-tuned model incurs an hourly hosting cost regardless of inference volume&lt;/a&gt;. Foundry deletes deployments after 15 days of zero traffic but the artifact stays; production workloads keep the deployment hot, which is a recurring cost line absent from pure-RAG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrain cadence on base-model updates or training-data drift.&lt;/strong&gt; When Microsoft ships a new base model you want to move to, you re-fine-tune. When your voice or domain examples change materially, you re-fine-tune. No equivalent retrain step exists in pure RAG; you just refresh the index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 architect's read on combined.&lt;/strong&gt; In our experience, fewer than one in five enterprise workloads actually clears the combined-pattern bar. Of those that do, half attempt it, hit the training-data discipline wall, and roll back to RAG-only with stronger prompting on voice. The ones that ship and stay shipped are the ones with a named voice owner (legal, brand, compliance) maintaining the training set as a versioned artifact alongside the index.&lt;/p&gt;

&lt;p&gt;In short: the "tailored AI trained on your data" framing is real, but the production-grade form of it is &lt;strong&gt;fine-tune for voice, RAG for grounding, evaluators on the combined output, named owner for the training set.&lt;/strong&gt; The slogan version of "trained on your data" usually means "fine-tuned without RAG," which is the version that fails the audit test.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worked Example: AI-Assisted Proposal Writing at Level 3
&lt;/h2&gt;

&lt;p&gt;Abstract three-level frameworks become believable when tied to a workload. The one we run through is &lt;strong&gt;proposal writing for a 40-person consulting firm responding to 8-12 RFPs per month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup (illustrative scenario, composite of pilots we've reviewed).&lt;/strong&gt; Past content lives across three systems: SharePoint (proposals, SOWs, case studies, reference letters), Dataverse (engagement records), and a Confluence wiki (technical approach templates). A senior consultant spends multiple days per proposal assembling content, often mis-citing past stats, missing recent wins, or quoting the wrong client. Win rates on RFPs requiring detailed past performance are well below where the firm wants them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Level 1 fails.&lt;/strong&gt; ChatGPT Enterprise or M365 Copilot will generate a plausible-sounding proposal. It will hallucinate client names, invent project durations, and quote ROI figures that exist nowhere in your actual delivery record. Every claim needs human verification, which means the consultant is doing the same assembly work as before plus correcting fabricated claims. Net productivity: marginal or negative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Level 2 falls short.&lt;/strong&gt; A Copilot Studio agent with knowledge sources pointing at the SharePoint proposal library partially works. It can retrieve relevant past proposals when asked. But it does not cite specific paragraphs reliably, drifts toward LLM-generated filler outside the retrieved scope, and cannot tell you which past engagements are most relevant by industry / deal size / scope match. Compliance review of the draft is still required because you cannot trace every claim back to a verifiable source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Level 3 works.&lt;/strong&gt; A Foundry agent with agentic retrieval over an indexed proposal corpus produces the following flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consultant feeds the RFP to the agent: "Draft a response to this RFP for a regional health insurer. Required sections: technical approach, past performance, references, project plan, pricing approach."&lt;/li&gt;
&lt;li&gt;The agent's retrieval plan generates focused subqueries: "past engagements in health insurance," "data migration case studies for regulated industries," "client references with named contacts," "pricing approaches for 6-12 month engagements," "team bios with healthcare project history."&lt;/li&gt;
&lt;li&gt;Each subquery runs against the indexed proposal corpus with semantic ranking. Top-N passages return with full source citations (SOW #2024-118, Case Study CS-2023-Q3-Health, Reference Letter RL-Acme-2024).&lt;/li&gt;
&lt;li&gt;The agent synthesizes section drafts, each paragraph linked to the source passages that grounded it.&lt;/li&gt;
&lt;li&gt;The activity array shows every subquery run, every passage retrieved, every ranking score. Compliance review takes minutes, not hours.&lt;/li&gt;
&lt;li&gt;Pricing is computed deterministically via a Power Automate flow that pulls actual past-engagement rate cards, not estimated by the LLM.&lt;/li&gt;
&lt;li&gt;The consultant edits the draft for win-theme positioning (the human strategic value-add) instead of assembling content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The end-to-end flow is below: RFP in, subqueries fan out, corpus returns ranked passages with citations, synthesis branches to deterministic pricing on the side, cited draft out.&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-proposal-flow.png" alt="Level 3 proposal-writing retrieval flow: an RFP feeds a Foundry agent that fans into five focused subqueries (past engagements, case studies, references, pricing, team bios), each hitting an indexed proposal corpus with semantic ranking. The agent synthesis returns content, references, and activity. A side branch routes pricing through a deterministic Power Automate rate-card flow. Both converge on a final source-cited proposal draft." width="800" height="450"&gt;Level 3 proposal-writing retrieval flow. Agent orchestrates, deterministic layer calculates.
  


&lt;p&gt;A sketch of the retrieval contract the agent works against, in pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agentic retrieval contract (Foundry-style, simplified)&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProposalSubquery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;past_engagement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;case_study&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team_bio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;industry&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// "health_insurance"&lt;/span&gt;
    &lt;span class="nl"&gt;dealSizeUsd&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// [250_000, 2_000_000]&lt;/span&gt;
    &lt;span class="nl"&gt;technology&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;      &lt;span class="c1"&gt;// ["dataverse", "power_automate"]&lt;/span&gt;
    &lt;span class="nl"&gt;sharableExternally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// never retrieve NDA-restricted content&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;topN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// pagination, default 5&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RetrievalResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// synthesized passage&lt;/span&gt;
  &lt;span class="nl"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceRef&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;      &lt;span class="c1"&gt;// each citable: SOW-2024-118, CS-2023-Q3-Health&lt;/span&gt;
  &lt;span class="nl"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubqueryTrace&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;    &lt;span class="c1"&gt;// every subquery + ranking score + token cost&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Pricing is NEVER computed by the LLM&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computePricing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ScopeSpec&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PricingResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;powerAutomateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate-card-pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract is the discipline. Every retrieval pass returns content + references + activity. Pricing always routes to deterministic compute. The agent orchestrates; the deterministic layer calculates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The named architectural choices that make this work:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curated index, not raw dump.&lt;/strong&gt; The proposal corpus is preprocessed: dedup, anonymization where appropriate, tagged by industry / scope-size / outcome / technology stack. Garbage in = grounded garbage out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-aware retrieval.&lt;/strong&gt; Subqueries scope by industry first, then deal size, then technology. Not a flat similarity search across everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness evaluators in CI.&lt;/strong&gt; Each agent release runs the &lt;a href="https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators" rel="noopener noreferrer"&gt;groundedness evaluator&lt;/a&gt; against a held-out set of past RFPs. Drops below 90% block deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic pricing.&lt;/strong&gt; The agent never calculates rate-card math; it calls a flow that does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail.&lt;/strong&gt; Every proposal sent has its retrieval activity log archived. If a claim is later challenged, the source is traceable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The realistic operational outcome (illustrative).&lt;/strong&gt; Three things change. Proposal turnaround drops from multi-day to half-day of senior consultant time. Win rate improves modestly because proposals reference the right past wins with the right specifics. Every claim is source-traceable, which makes legal review faster and procurement requests answerable.&lt;/p&gt;

&lt;p&gt;This is not a marketing-slogan outcome ("AI writes your proposals"). It is the outcome architects can defend to a managing partner. Multiplier numbers depend on corpus quality, retrieval design, and how much rework the agent saves the senior reviewer; pilots we have reviewed land at very different points along this range, so we decline to quote a single percentage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke in the pilots, honestly.&lt;/strong&gt; Level 3 is not failure-free. The two failure modes we see most often: the agent's subquery generator producing overly-broad initial queries that return passages from out-of-scope industries (fixed by adding a &lt;code&gt;filters_required&lt;/code&gt; guardrail before subquery dispatch), and security-trimming gaps surfacing only when a guest auditor account ran the agent during user-acceptance testing (fixed by re-running security trimming against the full identity matrix, not just internal accounts). Neither is a Level 3 indictment; both are a reminder that operational discipline is where Level 3 earns its keep.&lt;/p&gt;

&lt;p&gt;This is the &lt;a href="https://az365.ai/blog/ai-proposal-writing-multi-model-routing-patterns/" rel="noopener noreferrer"&gt;pattern we wrote up in more depth in the AI Proposal Writing on Foundry article&lt;/a&gt;. The principle generalizes: any workload where claims need to be source-citable, RAG with agentic retrieval is the right substrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Architect Responsibilities Don't Disappear at Level 3?
&lt;/h2&gt;

&lt;p&gt;Adopting Level 3 does not absolve the architect of existing responsibilities. The platform makes audit defensibility possible; the work below makes it real. In our experience, these are the seven responsibilities the platform does not own for you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;What the platform does NOT do for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semantic modeling of the source corpus&lt;/td&gt;
&lt;td&gt;Decide which fields matter (industry tags, deal size, outcome, technology, named-client where shareable) and how proposals link to SOWs, case studies, and references. The retrieval is only as good as the model behind the index; the platform inherits whatever taxonomy you bring.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data shaping discipline&lt;/td&gt;
&lt;td&gt;Clean past content, deduplicate entities, choose a sensible chunking strategy. Microsoft's RAG documentation consistently positions content preparation, indexing strategy, and prompt design as the levers under your control. The platform indexes whatever quality you bring it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval design&lt;/td&gt;
&lt;td&gt;Schema-aware queries, security trimming so users only retrieve content they are entitled to, top-N pagination so the agent does not exhaust the context window. Without security trimming, RAG leaks competitive intel between client proposals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groundedness evaluation in CI&lt;/td&gt;
&lt;td&gt;Run RAG evaluators against a held-out set on every change. Threshold-gate deploys. Without this, regressions ship silently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic tooling for calculations&lt;/td&gt;
&lt;td&gt;Agents produce probabilistic outputs. For anything that needs a correct number on the first try (pricing math, ROI projections, financial reconciliation, regulatory thresholds), wire deterministic logic via Power Automate flows, calculated columns, or external compute. Agent orchestrates; deterministic layer calculates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance + RBAC propagation&lt;/td&gt;
&lt;td&gt;DLP policy, Purview labels, audit-log review cadence. RBAC must propagate from the asking user through the agent through every tool call (RAG retrieval AND operational API AND runtime context). Indexed corpora often hold content classified differently than the surface application's user permissions. The platform enables Level 3; your governance discipline makes it safe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tenant isolation + version drift&lt;/td&gt;
&lt;td&gt;For multi-tenant deployments (MSPs, multi-end-client consultancies, hosted SaaS), the index, the evaluators, and the audit log all need explicit tenant tagging. Version drift is a parallel risk: when the indexed corpus, the LLM base model, the evaluator suite, and the system-of-record schema all evolve on independent cadences, the architect owns the version compatibility matrix. Plan a versioning strategy or accept silent regressions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data residency + procurement&lt;/td&gt;
&lt;td&gt;Confirm sovereign-region availability, customer-managed-key support for the index, and the contractual position on training-on-customer-data before you commit. Procurement-time friction is cheaper than mid-build replatforming. For regulated workloads (financial services, public sector, healthcare in EU/UK), the procurement model matters as much as the architecture.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What we hear from engineering teams running Level 3 in 2026&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From engineering-team interviews and pilot post-mortems we have reviewed, four operational failure modes show up repeatedly. Vendor decks don't lead with these; production architects do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval quality degrades on poorly-modeled corpora at scale.&lt;/strong&gt; Roughly past the few-hundred-thousand-chunk mark, semantic ranking quality drops noticeably when the index lacks structured metadata. Exact threshold varies with chunking strategy and schema discipline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness evaluators are gameable.&lt;/strong&gt; Prompt rewrites can lift evaluator scores without fixing the underlying retrieval gap. The metric is a useful floor, not a guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-trimming gaps surface under guest and cross-tenant access.&lt;/strong&gt; Pilot environments rarely exercise the full identity matrix; the gaps appear during UAT or worse, in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-turn conversational context retention is uneven across runtimes.&lt;/strong&gt; Different Foundry agent runtimes handle long-context grounding differently in 2026; behaviour you tested in one runtime may not transfer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are field observations, not citable forum links: we have deliberately declined to attach "Reddit thread" or "Microsoft Q&amp;amp;A" citations we cannot verify on demand. None of these invalidate Level 3 architecture. They do mean the responsibilities above are non-optional, not aspirational.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Level 3 Will NOT Do for You (Eight Limits)
&lt;/h2&gt;

&lt;p&gt;The "tailored AI on your data" framing implies more than it delivers. Skeptical readers will ask each of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 3 does not mean "we trained AI on our data."&lt;/strong&gt; Tailored RAG retrieves from an indexed enterprise corpus at query time. The base model weights are unchanged (unless you also fine-tune, which is the combined pattern from earlier and a separate decision). When a stakeholder asks "did we train it on our data?", the precise answer is "we indexed our data and the model retrieves from it per query, which is what you actually wanted because training would be slower to update and harder to audit."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG does not replace your data warehouse, lakehouse, or analytics substrate.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/azure/storage/blobs/data-lake-storage-introduction" rel="noopener noreferrer"&gt;Azure Data Lake&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/fabric/fundamentals/microsoft-fabric-overview" rel="noopener noreferrer"&gt;Fabric&lt;/a&gt;, and Synapse remain the right homes for high-volume analytics. The RAG index is downstream of these for grounding, not a substitute for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG does not eliminate hallucinations.&lt;/strong&gt; It narrows the gap between LLM output and source truth; it does not close it. Retrieval can return wrong passages, the LLM can synthesize incorrectly from correct passages, and the model can ignore retrieved context if your prompt design is weak. Groundedness evaluators catch most failures, not all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3 is not "set and forget."&lt;/strong&gt; Index freshness, schema drift, retrieval quality, and groundedness scores all need ongoing operational discipline. We have seen meaningful groundedness drift over two-quarter windows when no one owns the eval harness. The exact decay curve depends on how fast your source corpus changes; the pattern is consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every workload needs Level 3.&lt;/strong&gt; Most internal productivity work is fine at Level 1. Most departmental knowledge agents are fine at Level 2. Level 3 is for workloads where audit defensibility matters: proposals, claims, regulatory response, financial reporting, customer-facing communications, anything that goes into a legal record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3 is not a replacement for governance.&lt;/strong&gt; DLP, Purview, RBAC, audit log review still apply. The platform makes grounded answers possible; governance makes them safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3 is not sufficient without deterministic tooling.&lt;/strong&gt; Agents are probabilistic. Calculations, regulatory thresholds, and any numeric output that must be correct on the first try should be computed by deterministic tools called by the agent, not by the LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3 is not autonomous AI.&lt;/strong&gt; A Level 3 agent is more accurate than a Level 1 chat. It is not a reliable autonomous worker. Human-in-the-loop, evaluation gates, and fallback flows still apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these limitations is what your audit committee, compliance lead, or procurement officer will eventually ask about. Better to scope around them now than to discover them in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Frame: When Each Level Is Right
&lt;/h2&gt;

&lt;p&gt;Six workload archetypes mapped to levels. Find yours. The decision tree below collapses the table into three questions; the table after it carries the detail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-decision-tree.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Faz365.ai%2Fimages%2Fblog%2Fdiagrams%2Fenterprise-ai-context-architecture-decision-tree.png" alt="Decision tree for picking the right enterprise AI level per workload. Root node: Workload. First question: audit, regulatory, or compliance exposure? No branch leads to a question about external-facing or shared knowledge versus individual productivity, with answers leading to Level 1 (individual productivity, email drafts, meeting summaries, code completion) or Level 2 (departmental knowledge, IT helpdesk, HR benefits, sales enablement). Yes branch leads to a question about whether exact numbers are needed on first try, with answers leading to Level 3 (tailored RAG for proposals, claims, regulated customer-facing communications) or Level 3 plus deterministic compute layer (financial reporting, regulatory thresholds). A footer note: if voice consistency also matters, stack fine-tune on top of Level 3 RAG (the apex combined pattern) for executive comms, regulated customer voice, and brand-critical content." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Decision tree: three questions, four leaves, plus the apex combined pattern when voice consistency also matters.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Level fit is necessary but not sufficient. Even the right-level workload still requires the seven architect responsibilities above (semantic modeling, retrieval design, evaluators in CI, deterministic tooling, governance, data residency, training-set ownership if you fine-tune).&lt;/p&gt;

&lt;p&gt;The reference table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Right level&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Common mistake&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General productivity (email drafting, meeting summaries, code completion)&lt;/td&gt;
&lt;td&gt;Level 1&lt;/td&gt;
&lt;td&gt;Speed + cost + low audit risk. The training-data-blend trade-off is acceptable.&lt;/td&gt;
&lt;td&gt;Building custom RAG when M365 Copilot would have shipped in a week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Departmental knowledge Q&amp;amp;A (IT helpdesk, HR benefits, sales enablement)&lt;/td&gt;
&lt;td&gt;Level 2&lt;/td&gt;
&lt;td&gt;Knowledge-source-scoped agents in Copilot Studio cover this cleanly. Connector retrieval gives partial grounding.&lt;/td&gt;
&lt;td&gt;Stopping at Level 2 for workloads that actually need audit defensibility (compliance, claims)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated customer-facing comms (proposals, claims, insurance, legal response)&lt;/td&gt;
&lt;td&gt;Level 3&lt;/td&gt;
&lt;td&gt;Every claim must be source-traceable. Compliance review takes minutes only with structured citations.&lt;/td&gt;
&lt;td&gt;Building at Level 1 or 2 and discovering at audit time that nothing is provable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Financial reporting + executive summaries&lt;/td&gt;
&lt;td&gt;Level 3 + deterministic layer&lt;/td&gt;
&lt;td&gt;Numbers must be exact. RAG for narrative + deterministic compute for math. Tone-fine-tuning optional.&lt;/td&gt;
&lt;td&gt;Letting the LLM do arithmetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field operations (inspection, dispatch, technician guidance)&lt;/td&gt;
&lt;td&gt;Level 2 or 3&lt;/td&gt;
&lt;td&gt;Depends on whether the field action has audit/regulatory exposure (then Level 3) or is internal-only guidance (Level 2 suffices).&lt;/td&gt;
&lt;td&gt;One-size-fits-all; same agent design for low-risk internal vs regulated external&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal R&amp;amp;D / experimentation / prototyping&lt;/td&gt;
&lt;td&gt;Level 1&lt;/td&gt;
&lt;td&gt;Fast iteration &amp;gt; audit defensibility. Promote to Level 2 / 3 when prototypes become production.&lt;/td&gt;
&lt;td&gt;Promoting a Level 1 prototype to production without re-architecting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: pick the level per workload, not per organization. Most enterprises will run all three levels simultaneously, with the architecture team owning the boundary decisions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Need a sanity check on your three-level workload map?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are scoping which enterprise workloads belong at which level, &lt;a href="https://az365.ai/about/" rel="noopener noreferrer"&gt;reach out&lt;/a&gt; for a second-opinion review of the workload-to-level mapping before you commit to a build budget.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When Does Each Level Fail? Transition Triggers Between Levels
&lt;/h2&gt;

&lt;p&gt;Enterprise architects do not want a maturity ladder. They want decision triggers, concrete signals that the current level has run out of headroom and the workload needs to advance. The triggers below are the ones we see force level transitions in pilots we have reviewed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Current level&lt;/th&gt;
&lt;th&gt;Trigger that forces advancement&lt;/th&gt;
&lt;th&gt;What it costs to advance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Level 1 → Level 2&lt;/td&gt;
&lt;td&gt;The workload starts producing externally-visible content (departmental Q&amp;amp;A, customer-adjacent), or stakeholders begin asking for source citations the M365 Copilot blend cannot reliably provide.&lt;/td&gt;
&lt;td&gt;Copilot Studio + Managed Environments + ALM discipline + knowledge-source curation. Real but bounded cost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 2 → Level 3&lt;/td&gt;
&lt;td&gt;Compliance, legal, or procurement starts asking 'where did that claim come from?' and the connector-citation answer is no longer sufficient. Or the corpus grows past the connector retrieval quality envelope.&lt;/td&gt;
&lt;td&gt;Foundry agent build + indexed corpus + agentic retrieval + groundedness evaluator harness + CI gating. New operational discipline; new observability burden.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 3 → Level 3 + fine-tune&lt;/td&gt;
&lt;td&gt;Voice consistency becomes a regulatory or brand-quality requirement (regulated customer comms, executive voice, brand-critical content at scale) AND retraining cadence is acceptable.&lt;/td&gt;
&lt;td&gt;LoRA SFT + optional DPO + ongoing voice-corpus ownership + hourly fine-tune hosting + retrain on base-model updates. Highest operational cost in the stack.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trigger column matters more than the cost column. If the trigger has not fired, advancing is over-engineering. If the trigger has fired, staying at the current level is technical debt accruing toward an audit incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should a Copilot Studio Agent Move to Foundry?
&lt;/h2&gt;

&lt;p&gt;For Power Platform teams already running Copilot Studio agents with knowledge sources, the practical question is not "Level 2 or Level 3?" abstractly. It is "do we extend the existing agent, or rebuild it?" In our experience the decision turns on three concrete tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extend in Copilot Studio (stay Level 2)&lt;/strong&gt; when the workload tolerates connector-source citations without structured grounding metadata, when knowledge-source documents change slowly enough that connector refresh windows are acceptable, and when the agent's output is reviewed by humans who can verify claims themselves. Add Azure AI Search as a connector if you need broader corpus coverage; this stays a Level 2 deployment with stronger retrieval, not a Level 3 architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebuild in Foundry (move to Level 3)&lt;/strong&gt; when the workload requires structured citation evidence on every response (compliance review, regulated comms), when you need groundedness evaluators gating CI deployments, when you need the activity-array audit trace, or when the corpus is large enough that connector-style retrieval no longer ranks well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The breakpoint is the audit posture&lt;/strong&gt;, not the model behind the agent. A Copilot Studio agent and a Foundry agent can both call the same underlying model; what differs is the response contract (does it return structured grounding data and activity trace?) and the operational discipline (is there a groundedness evaluator gate on every release?). If the answer to either is "no," the agent is at Level 2 regardless of model choice. If the workload requires "yes" to both, plan a Foundry rebuild, not an extension.&lt;/p&gt;

&lt;p&gt;For Managed-Environments-deployed Power Platform tenants, the rebuild has an additional discipline layer: ALM, solution packaging, environment routing, and CoE governance approval cycles. Plan the Foundry build inside the same ALM regime, not as a separate stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should Your 2026 AI Architecture Plan for All Three Levels?
&lt;/h2&gt;

&lt;p&gt;In our read, the three-level framing is more useful than the five-level CMM maturity model Microsoft publishes for adoption. The CMM model measures the organization. The three levels measure the workload. The organization-level view is for change-management leads; the workload-level view is for architects making build decisions.&lt;/p&gt;

&lt;p&gt;The deeper signal we are watching: in our read, traceability is becoming the primary differentiator between AI that ships demos and AI that ships production systems. Level 1 wins on speed-to-demo. Level 3 wins on speed-to-audit. Both have valid use cases. The category in the middle (Level 2) is where most enterprises sit and most regret-investments live: enough customization to claim "we built our own AI," not enough citation discipline to actually defend it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Our bet, falsifiable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By the end of 2027, the procurement teams of regulated enterprises will increasingly require Level 3 architecture (agentic retrieval + groundedness evaluation + audit log archival) for AI systems that produce customer-facing claims. The regulatory trajectory points the same way: &lt;a href="https://artificialintelligenceact.eu/article/13/" rel="noopener noreferrer"&gt;EU AI Act Article 13&lt;/a&gt; requires transparency and traceability for high-risk AI; the &lt;a href="https://www.nist.gov/itl/ai-risk-management-framework" rel="noopener noreferrer"&gt;NIST AI Risk Management Framework&lt;/a&gt; names "traceability" as a Measure-function expectation; FFIEC AI guidance applies model-risk-management discipline to AI in financial services. None prescribe Level 3 specifically, but each makes retrieval-trace evidence the natural compliance posture. Vendors that ship Level 1 or Level 2 with marketing-language audit-trail will lose RFPs to vendors that ship structured Level 3 evidence by default.&lt;/p&gt;

&lt;p&gt;How we would know we are wrong: if federal, financial-services, and healthcare RFPs through 2026 and into 2027 continue accepting Level 1 / Level 2 vendor responses without specific technical-response requirements for retrieval activity logs, structured citation evidence, or groundedness-evaluation thresholds, the prediction fails. The specific language to watch in technical-response sections: explicit "structured citation evidence" or "retrieval activity log" or "groundedness-evaluation threshold" clauses. We do not yet see this in 2026 RFPs at scale; we are betting it shows up in the next 18 months. If it does not, the bet is wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The architect's question is not "which level should we adopt?" It is &lt;strong&gt;"which level does each of our workloads actually need, and are we building each one at the right level?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enterprise AI Anti-Patterns: What Fails in Production
&lt;/h2&gt;

&lt;p&gt;Naming the failure modes is more useful than naming the successes. These are the patterns we see fail repeatedly in pilots and the early-production stage. If your architecture has any of them, the audit is going to surface them, so better to surface them yourself first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector database as universal truth layer.&lt;/strong&gt; Treating the indexed corpus as the source of "what is currently true" instead of as the source of "what we wrote down at some point in the past." Order status, account balances, deal state: none of those belong in a vector index. They belong in systems of record, queried at request time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless agents in stateful workflows.&lt;/strong&gt; An agent that forgets the approval that was granted three turns ago, or the prior tool call that already moved the workflow forward. Runtime context is not optional infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct model-to-ERP writes.&lt;/strong&gt; Letting the LLM generate the payload and call the mutation endpoint without typed contracts, validation, or human approval boundaries. The first regression silently corrupts production data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing permission inheritance.&lt;/strong&gt; RBAC enforced at the surface application but not propagated to the retrieval call, the API call, or the runtime-context lookup. The agent leaks across users the moment one of those calls runs unscoped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-only orchestration.&lt;/strong&gt; Trying to control which tool the agent picks, in what order, with what guardrails, entirely through prompt engineering. Works in demos. Fails under adversarial input, fails under multi-tenant load, fails under workflow complexity. Wire the orchestration logic deterministically; let the LLM only choose within the bounded option set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded agent autonomy.&lt;/strong&gt; "Let the agent figure it out" without scoped actions, typed contracts, or human approval boundaries. This is how blast-radius incidents happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness evaluators as a compliance fig leaf.&lt;/strong&gt; Running the evaluator suite for the audit committee then ignoring its output because it gates deploys. Either the threshold gate is real and slows you down, or it is theatre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating retrieval quality as fixed.&lt;/strong&gt; Building the index once at project kickoff and never re-indexing as the source corpus drifts, the chunking strategy reveals its weaknesses, or the ranker improves. Retrieval is an operational discipline, not a one-time setup task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One global evaluator for all workloads.&lt;/strong&gt; Workload-specific evaluation harnesses don't share thresholds. Proposal-writing groundedness and customer-claims groundedness need different gold sets and different bars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability as a logging afterthought.&lt;/strong&gt; Tracing the LLM call but not the retrieval plan, the tool invocations, or the runtime envelope. The next incident debugging session will reveal the gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is recoverable if caught early. The hard ones are the ones that ship to production unnoticed and only surface during an audit or an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Part Is Often Organizational, Not Architectural
&lt;/h2&gt;

&lt;p&gt;Most of this article has been about architecture: layers, contracts, evaluators, observability, autonomy boundaries. That is the part architects can solve directly. It is also, honestly, &lt;strong&gt;not where most enterprise AI programs actually fail.&lt;/strong&gt; Where they fail, in our experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ownership ambiguity.&lt;/strong&gt; Who owns the indexed corpus? Who owns the evaluator harness? Who owns the prompt? Who owns the agent's autonomy boundary? In federated enterprises, these often have no single owner, and the resulting drift is invisible until an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuously evolving business semantics.&lt;/strong&gt; The org's definition of "customer," "active," "open," "approved," "in good standing" shifts as products, regulations, and acquisitions change. Every shift breaks something downstream. The architecture cannot freeze meaning that the business keeps redefining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflicting ownership domains.&lt;/strong&gt; The CRM is owned by Sales; the ERP is owned by Finance; the indexed corpus is owned by IT; the Foundry environment is owned by a Cloud CoE. Each has its own change cadence and approval process. An ECA spanning all of them is, in practice, four different change-management cycles stitched together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procurement friction.&lt;/strong&gt; The contract for the model endpoint, the contract for the search service, the contract for the evaluator product, and the contract for the agent runtime are often four separate procurement cycles. By the time all four close, the architecture has shifted underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal review cycles.&lt;/strong&gt; Data residency, training-on-customer-data clauses, indemnity for grounded-output errors, regulatory disclosure, retention policy on activity logs: each of these is a separate review, often with different approving counsel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational support burden.&lt;/strong&gt; A Level 3 system has a 24x7 operational shape if customers depend on it. Most enterprises do not have an existing on-call rotation for "the agent's eval harness drifted overnight." That capability has to be built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal platform fragmentation.&lt;/strong&gt; Multi-BU enterprises rarely have one CRM, one ERP, one identity provider. They have versions of each. The ECA's "operational APIs" layer is sometimes five APIs that each return the same field with a different name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest architectural admission.&lt;/strong&gt; Perfect context orchestration can itself become a bottleneck. A sufficiently centralized ECA can become the single point of slowness that every workload routes through. A sufficiently federated one becomes a coordination problem where no single team can ship an end-to-end change. Architects who have lived through both extremes will recognize this tension; there is no clean answer, only the workload-by-workload calibration the article has argued for throughout.&lt;/p&gt;

&lt;p&gt;The technical disciplines in this article are necessary. They are not sufficient. The enterprise AI programs that ship and stay shipped are the ones with &lt;strong&gt;named owners per layer&lt;/strong&gt;, &lt;strong&gt;explicit semantic-versioning of business definitions&lt;/strong&gt;, &lt;strong&gt;a cross-domain coordination function&lt;/strong&gt;, and &lt;strong&gt;patience for the procurement and legal cycles&lt;/strong&gt; that the technology itself cannot accelerate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Position, Stated Plainly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Enterprise AI is not a single retrieval problem. It is the orchestration of multiple context systems with different latency, authority, freshness, and governance characteristics.&lt;/strong&gt; Everything else in this article is a corollary.&lt;/p&gt;

&lt;p&gt;The corollaries, distilled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG ≠ enterprise AI architecture.&lt;/strong&gt; RAG is one knowledge layer (unstructured). Real Level 3 systems also call structured systems of record and inject runtime context. Architect for three layers, not one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the level per workload, not per organization.&lt;/strong&gt; Most enterprises run all three levels simultaneously. The architecture team owns the boundary decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability is the apex axis.&lt;/strong&gt; Level 1 wins on speed-to-demo. Level 3 wins on speed-to-audit. The combined fine-tune + RAG pattern wins on both audit and voice. Each has valid use cases; choose by what the workload actually demands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate three layers with different disciplines.&lt;/strong&gt; Retrieval, operational API calls, and runtime context have different latency budgets, freshness requirements, governance regimes, and evaluation surfaces. A Level 3 architecture that uses the same operational discipline across all three is under-engineered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions propagate through every layer.&lt;/strong&gt; RBAC must flow from the asking user through the agent through every tool call. DLP, Purview, tenant isolation, and version drift all apply across all three layers, not just the LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent orchestrates; the deterministic layer calculates.&lt;/strong&gt; Numbers come from systems of record or deterministic compute, never from the LLM. Citations come from RAG. Identity comes from runtime context. The agent is the coordinator, not the source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build to that bar and Level 3 is a system you can defend in an audit, not a slide you defend to procurement after the contract is signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The directional close.&lt;/strong&gt; The future enterprise AI stack will not be defined primarily by the model layer. The model layer will commoditize. It will be defined by &lt;strong&gt;how reliably organizations manage context, authority, permissions, and execution across distributed systems&lt;/strong&gt;, by their Enterprise Context Architecture. That is the durable architectural lane. Build into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glossary: Terms in This Article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agentic retrieval&lt;/strong&gt;: a retrieval pattern where an LLM-driven agent decomposes a complex question into focused subqueries, runs them against an indexed corpus in parallel, and returns structured grounding data with citations and execution metadata. Distinguished from classic single-query RAG by its subquery planning and structured response format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness evaluator&lt;/strong&gt;: an automated test that scores whether an LLM response cites only the retrieved context, or fabricates outside it. Microsoft Foundry exposes this as a first-class RAG evaluator with a numeric score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foundry IQ&lt;/strong&gt;: Microsoft's umbrella for Foundry-native retrieval, knowledge stores, and evaluator tooling that sits under Foundry agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailored RAG&lt;/strong&gt;: our shorthand for Level 3 in this article: RAG over a curated, indexed enterprise corpus with agentic retrieval, security trimming, groundedness evaluation, and deterministic tooling for calculations. Not a Microsoft product name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security trimming&lt;/strong&gt;: filtering retrieved passages by the requesting user's permissions so the agent never returns content the user cannot legitimately see. Implemented at the index level, not at the LLM level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity array&lt;/strong&gt;: the trace of subqueries, retrieval calls, and ranking scores returned alongside the synthesized answer. Used as the audit-log substrate for Level 3 responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA fine-tuning&lt;/strong&gt;: low-rank adaptation, the parameter-efficient fine-tuning technique Foundry uses by default. Trains a small adapter on top of the base model rather than re-training the full weight matrix; cheaper, smaller artifact, faster to deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge retrieval layer (RAG / unstructured)&lt;/strong&gt;: the substrate that holds documents, policies, manuals, past work product. Authoritative for "what does our policy say" but not for "what is the current state." Mechanism: agentic retrieval over an indexed corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational data layer (structured / systems of record)&lt;/strong&gt;: the substrate that holds the authoritative state of the business: orders, claims, invoices, customer records. Mechanism: deterministic API or SQL calls scoped to the asking user's permissions. Never paraphrased by the LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime context layer&lt;/strong&gt;: the substrate that holds who is asking, what tenant they belong to, what they are authorized to see, what workflow step they are in. Mechanism: context injection at agent invocation; passed through every downstream tool call as the permission envelope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift&lt;/strong&gt;: the silent failure mode where the indexed corpus, the base LLM, the evaluator suite, and the systems-of-record schemas evolve on independent cadences and the agent's behavior changes without anyone noticing. Mitigated by versioning each component and running the evaluator harness on every release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://az365.ai/blog/azure-ai-foundry-vs-azure-openai-2026-decision/" rel="noopener noreferrer"&gt;Azure AI Foundry vs Azure OpenAI: The 2026 Decision&lt;/a&gt; - the platform decision under Level 3 builds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://az365.ai/blog/dataverse-agent-data-platform-decoded/" rel="noopener noreferrer"&gt;Dataverse MCP, Business Skills, and Coding Agents: The 2026 Decode&lt;/a&gt; - Dataverse-as-agent-data-platform is a Level 3 substrate for Microsoft business-apps workloads&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://az365.ai/blog/ai-proposal-writing-multi-model-routing-patterns/" rel="noopener noreferrer"&gt;AI Proposal Writing on Foundry: Multi-Model Patterns That Ship&lt;/a&gt; - the deep version of the proposal-writing example above&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://az365.ai/blog/claude-on-azure-the-marketplace-billing-trap/" rel="noopener noreferrer"&gt;Claude on Azure: The Marketplace Billing Trap&lt;/a&gt; - the procurement reality of building Level 3 with multi-AI models on Azure&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you are mapping enterprise workloads to AI levels and want a second-opinion review of where each one belongs, &lt;a href="https://az365.ai/about/" rel="noopener noreferrer"&gt;reach out&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published at &lt;a href="https://az365.ai/blog/enterprise-ai-context-architecture/" rel="noopener noreferrer"&gt;az365.ai&lt;/a&gt;. I'm Alex Pechenizkiy, an Azure and Power Platform solutions architect writing honest, vendor-neutral analysis of the Microsoft AI stack. More at &lt;a href="https://az365.ai/" rel="noopener noreferrer"&gt;az365.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>enterpriseai</category>
      <category>aistrategy</category>
    </item>
    <item>
      <title>RAG Is Not Enough: The Evolution of Enterprise AI</title>
      <dc:creator>RAJSHREE</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:39:54 +0000</pubDate>
      <link>https://dev.to/rjshree/rag-is-not-enough-the-evolution-of-enterprise-ai-1fgh</link>
      <guid>https://dev.to/rjshree/rag-is-not-enough-the-evolution-of-enterprise-ai-1fgh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Rajश्री | Software Engineer &amp;amp; Full Stack Developer&lt;/p&gt;




&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;For the last few years, &lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; has become one of the most popular architectures in enterprise AI.&lt;/p&gt;

&lt;p&gt;And for good reason.&lt;/p&gt;

&lt;p&gt;A large language model may be excellent at reasoning and language generation, but it does not automatically know your company's internal policies, customer records, product documentation, support tickets, contracts, or constantly changing business data.&lt;/p&gt;

&lt;p&gt;RAG provided an elegant solution.&lt;/p&gt;

&lt;p&gt;Instead of retraining the model whenever enterprise knowledge changes, retrieve relevant information at inference time and provide it to the model as context.&lt;/p&gt;

&lt;p&gt;The architecture looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query
    ↓
Retrieve Relevant Information
    ↓
Build Context
    ↓
LLM
    ↓
Generate Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a huge step forward.&lt;/p&gt;

&lt;p&gt;A company could take its existing documentation, index it, connect a language model, and suddenly employees could ask questions about internal knowledge using natural language.&lt;/p&gt;

&lt;p&gt;But then production happened.&lt;/p&gt;

&lt;p&gt;The questions became harder.&lt;/p&gt;

&lt;p&gt;Users stopped asking only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is our leave policy?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Am I eligible for this leave based on my current employment status?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They stopped asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is our refund policy?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I approve this customer's refund, and if yes, process it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They stopped asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What does this support document say?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Check the customer's account, verify the issue, determine whether they're eligible for replacement, create the ticket, and notify them."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And suddenly the problem changed.&lt;/p&gt;

&lt;p&gt;This was no longer simply a &lt;strong&gt;knowledge retrieval problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It became a &lt;strong&gt;software engineering problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The system needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retrieval&lt;/li&gt;
&lt;li&gt;reasoning&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;live data&lt;/li&gt;
&lt;li&gt;business rules&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;tools&lt;/li&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;workflow state&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;and sometimes human approval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leads to a very important realization:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;RAG is not enough.&lt;/strong&gt;
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not because RAG is obsolete.&lt;/p&gt;

&lt;p&gt;Not because vector databases are useless.&lt;/p&gt;

&lt;p&gt;Not because agents have replaced retrieval.&lt;/p&gt;

&lt;p&gt;But because &lt;strong&gt;retrieval is only one capability of a production enterprise AI system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real evolution looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
 ↓
RAG
 ↓
Advanced RAG
 ↓
Agentic Retrieval
 ↓
Tool-Using AI
 ↓
Stateful AI
 ↓
Governed AI Workflows
 ↓
Reliable Enterprise AI Systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And understanding this evolution is where &lt;strong&gt;AI engineering begins to look much more like software engineering—and much less like prompt engineering.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  1. First, What Did RAG Actually Solve?
&lt;/h1&gt;

&lt;p&gt;Before discussing why RAG is not enough, we need to give RAG the credit it deserves.&lt;/p&gt;

&lt;p&gt;Large language models have an important limitation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The model's parameters are not your company's database.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine an employee asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is our company's enterprise customer refund policy?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A general-purpose LLM may know what refund policies usually look like.&lt;/p&gt;

&lt;p&gt;But it doesn't automatically know &lt;strong&gt;your company's current policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your organization may have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refund Policy.pdf
Enterprise Customer Policy.pdf
Finance Guidelines.pdf
Regional Exceptions.pdf
Support Documentation/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RAG creates a bridge between the model and that external knowledge.&lt;/p&gt;

&lt;p&gt;A simplified architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     Enterprise Knowledge
                           │
              ┌─────────────┼─────────────┐
              ↓             ↓             ↓
          Documents       Wikis        Knowledge Base
              │             │             │
              └─────────────┼─────────────┘
                           ↓
                       Chunking
                           ↓
                       Embeddings
                           ↓
                     Vector Database
                           ↓
                        Retrieval
                           ↓
                     Relevant Context
                           ↓
                           LLM
                           ↓
                         Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This solved several major problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private knowledge
&lt;/h3&gt;

&lt;p&gt;The model can work with internal company information without that information being part of its original training data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fresh information
&lt;/h3&gt;

&lt;p&gt;If a policy changes, the knowledge source can be updated without retraining the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grounding
&lt;/h3&gt;

&lt;p&gt;The model can generate answers based on retrieved enterprise context rather than relying entirely on its pretrained knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Citations
&lt;/h3&gt;

&lt;p&gt;A well-designed system can show users which documents or records support the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lower operational complexity
&lt;/h3&gt;

&lt;p&gt;For many knowledge-based applications, RAG is significantly simpler than fine-tuning a model for every knowledge update.&lt;/p&gt;

&lt;p&gt;So yes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG is extremely useful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But it solves a specific problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How can an LLM access relevant external knowledge?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enterprise AI eventually asks a much larger question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How can an AI system reliably accomplish a business objective?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are very different problems.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Retrieval Is Not Understanding
&lt;/h1&gt;

&lt;p&gt;This is one of the most important distinctions in enterprise AI.&lt;/p&gt;

&lt;p&gt;Imagine an employee asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I approve this refund?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A basic RAG system may retrieve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refund Policy.pdf
Customer Refund Limits.pdf
Approval Guidelines.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model can read them and explain the rules.&lt;/p&gt;

&lt;p&gt;But the actual question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What does the refund policy say?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Given this specific customer's account, transaction amount, my role, the current approval state, and applicable regional rules, am I authorized to approve this refund?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now look at everything the system needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Policy
+
User Identity
+
Role
+
Permissions
+
Customer Data
+
Transaction Data
+
Current Workflow State
+
Business Rules
+
Reasoning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A vector database cannot provide all of this.&lt;/p&gt;

&lt;p&gt;And an LLM should not be expected to invent it.&lt;/p&gt;

&lt;p&gt;This is the first major architectural lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Retrieval provides context. It does not automatically provide the complete state of the business.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  3. Enterprise Data Is Not Just Documents
&lt;/h1&gt;

&lt;p&gt;One of the mistakes developers make when building their first RAG system is assuming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enterprise Knowledge = Documents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real enterprise environments are much messier.&lt;/p&gt;

&lt;p&gt;A company may have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Enterprise
                        │
         ┌───────────────┼────────────────┐
         ↓               ↓                ↓
     Documents         Databases         APIs
         │               │                │
         ↓               ↓                ↓
       Wiki             CRM              ERP
       PDFs             HRMS             Payments
       Policies         Tickets          Inventory
       Manuals          Analytics        Identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And these sources behave differently.&lt;/p&gt;

&lt;p&gt;A policy document is not the same thing as a customer database.&lt;/p&gt;

&lt;p&gt;A customer database is not the same thing as an API.&lt;/p&gt;

&lt;p&gt;An API response is not the same thing as a knowledge graph.&lt;/p&gt;

&lt;p&gt;A transaction table should not necessarily be embedded into a vector database simply because you're building a RAG system.&lt;/p&gt;

&lt;p&gt;This is where practical AI engineering begins.&lt;/p&gt;

&lt;p&gt;The engineer needs to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What kind of information is this, and what is the correct way to access it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unstructured knowledge
        ↓
Semantic / Hybrid Retrieval

Structured business data
        ↓
SQL / Database Query

Real-time information
        ↓
API

Relationships
        ↓
Knowledge Graph

Business operation
        ↓
Tool / API / Workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a much more useful mental model than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's put everything into a vector database."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  4. Why "Just Add More Documents" Doesn't Fix RAG
&lt;/h1&gt;

&lt;p&gt;Suppose your RAG system is giving poor answers.&lt;/p&gt;

&lt;p&gt;A common reaction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Maybe it doesn't have enough information. Let's add more documents."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the team adds another 100,000 documents.&lt;/p&gt;

&lt;p&gt;Then another 500,000.&lt;/p&gt;

&lt;p&gt;Eventually the system contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old Policies
New Policies
Regional Policies
Draft Policies
Archived Policies
Internal Notes
Duplicate Documents
Different Versions
Conflicting Documents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now retrieval itself becomes harder.&lt;/p&gt;

&lt;p&gt;The problem was not lack of data.&lt;/p&gt;

&lt;p&gt;The problem was &lt;strong&gt;information quality and information architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A simplified failure chain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Poor Data
   ↓
Poor Chunking
   ↓
Poor Indexing
   ↓
Poor Retrieval
   ↓
Wrong Context
   ↓
Wrong Reasoning
   ↓
Wrong Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding more documents doesn't necessarily improve the system.&lt;/p&gt;

&lt;p&gt;Sometimes it makes it worse.&lt;/p&gt;

&lt;p&gt;A production AI engineer therefore asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this document authoritative?&lt;/li&gt;
&lt;li&gt;Is it current?&lt;/li&gt;
&lt;li&gt;Who owns it?&lt;/li&gt;
&lt;li&gt;Which version applies?&lt;/li&gt;
&lt;li&gt;Can this user access it?&lt;/li&gt;
&lt;li&gt;Is the information structured or unstructured?&lt;/li&gt;
&lt;li&gt;Should this information even be retrieved semantically?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions are often more important than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which embedding model should I use?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  5. Basic RAG vs Advanced RAG
&lt;/h1&gt;

&lt;p&gt;A simple RAG pipeline may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
 ↓
Embedding
 ↓
Vector Search
 ↓
Top K Chunks
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for prototypes.&lt;/p&gt;

&lt;p&gt;But production retrieval often requires more.&lt;/p&gt;

&lt;p&gt;A stronger architecture might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query
    ↓
Query Understanding
    ↓
Intent Detection
    ↓
Metadata / Permission Filtering
    ↓
Hybrid Search
    ↓
Vector Retrieval
    +
Keyword Retrieval
    ↓
Candidate Documents
    ↓
Reranking
    ↓
Context Selection
    ↓
Context Compression
    ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible techniques include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;semantic search&lt;/li&gt;
&lt;li&gt;keyword search&lt;/li&gt;
&lt;li&gt;hybrid retrieval&lt;/li&gt;
&lt;li&gt;metadata filtering&lt;/li&gt;
&lt;li&gt;reranking&lt;/li&gt;
&lt;li&gt;query expansion&lt;/li&gt;
&lt;li&gt;query rewriting&lt;/li&gt;
&lt;li&gt;contextual chunking&lt;/li&gt;
&lt;li&gt;hierarchical retrieval&lt;/li&gt;
&lt;li&gt;document-level access control&lt;/li&gt;
&lt;li&gt;citation tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Better RAG is not simply "use a better vector database."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Retrieval is an information architecture problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. The Permission Problem Most RAG Demos Ignore
&lt;/h1&gt;

&lt;p&gt;This is where enterprise AI becomes a serious security problem.&lt;/p&gt;

&lt;p&gt;Imagine a company has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document A → Public
Document B → Engineering
Document C → Finance
Document D → HR
Document E → Executive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An employee from Engineering asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What was the company's executive compensation strategy?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A naive RAG system might retrieve Document E because it is semantically relevant.&lt;/p&gt;

&lt;p&gt;The LLM now has access to information that the employee should never have seen.&lt;/p&gt;

&lt;p&gt;This is not a hallucination problem.&lt;/p&gt;

&lt;p&gt;This is an &lt;strong&gt;authorization failure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the solution should not be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieve sensitive information
        ↓
Tell the LLM:
"Please don't reveal it."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a security boundary.&lt;/p&gt;

&lt;p&gt;Authorization needs to happen before sensitive information reaches the model.&lt;/p&gt;

&lt;p&gt;A safer architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Authentication
 ↓
Identity / Role
 ↓
Authorization
 ↓
Allowed Data Scope
 ↓
Retrieval
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is critical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The model should not be trusted to enforce access control.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The application architecture must enforce it.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. When RAG Meets Live Data
&lt;/h1&gt;

&lt;p&gt;Let's take a practical example.&lt;/p&gt;

&lt;p&gt;A customer asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Where is my order?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The company's documentation might explain shipping policies.&lt;/p&gt;

&lt;p&gt;RAG can answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Standard shipping usually takes 3–5 business days."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that's not what the customer actually wants.&lt;/p&gt;

&lt;p&gt;They want to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Where is my specific order right now?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That information lives in a live system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   ↓
AI Assistant
   ↓
Order Lookup Tool
   ↓
Order Management API
   ↓
Current Order Status
   ↓
LLM
   ↓
Natural Language Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, RAG may still be useful for explaining shipping policies.&lt;/p&gt;

&lt;p&gt;But the actual order status should come from the &lt;strong&gt;source of truth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This leads to a practical rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use retrieval for knowledge. Use systems of record for facts that must be current.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't embed something into a vector store simply because you can.&lt;/p&gt;

&lt;h1&gt;
  
  
  8. The Shift From Answering to Acting
&lt;/h1&gt;

&lt;p&gt;This is probably the biggest transition in enterprise AI.&lt;/p&gt;

&lt;p&gt;Early systems focused on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question
 ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern enterprise systems increasingly need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent
 ↓
Reason
 ↓
Retrieve
 ↓
Decide
 ↓
Act
 ↓
Verify
 ↓
Report Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider an IT support assistant.&lt;/p&gt;

&lt;p&gt;The user says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My company laptop isn't working and it's still under warranty. Create a replacement request."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A basic RAG system can explain the replacement policy.&lt;/p&gt;

&lt;p&gt;A useful enterprise AI system should potentially:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the employee.&lt;/li&gt;
&lt;li&gt;Identify the assigned device.&lt;/li&gt;
&lt;li&gt;Check warranty status.&lt;/li&gt;
&lt;li&gt;Retrieve replacement policy.&lt;/li&gt;
&lt;li&gt;Determine eligibility.&lt;/li&gt;
&lt;li&gt;Create a support ticket.&lt;/li&gt;
&lt;li&gt;Attach relevant information.&lt;/li&gt;
&lt;li&gt;Route it to the correct team.&lt;/li&gt;
&lt;li&gt;Return the ticket ID.&lt;/li&gt;
&lt;li&gt;Record what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Knowledge
+
Live Data
+
Reasoning
+
Tools
+
Workflow
+
State
+
Verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is far beyond basic RAG.&lt;/p&gt;




&lt;h1&gt;
  
  
  9. Tool Use Turns AI Into a Software System
&lt;/h1&gt;

&lt;p&gt;Tools are one of the most important additions to enterprise AI.&lt;/p&gt;

&lt;p&gt;An AI system might have access to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search Tool
SQL Tool
CRM Tool
ERP Tool
Email Tool
Calendar Tool
Ticketing Tool
Payment Tool
Internal API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find overdue invoices above ₹10 lakh and notify the responsible account managers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system may need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request
    ↓
Understand Intent
    ↓
Query Finance Database
    ↓
Filter Invoices
    ↓
Identify Account Managers
    ↓
Apply Notification Policy
    ↓
Send Emails
    ↓
Verify Delivery
    ↓
Return Summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RAG can provide the policy.&lt;/p&gt;

&lt;p&gt;The tools perform the work.&lt;/p&gt;

&lt;p&gt;This distinction matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Knowledge without action has limited operational value.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  10. But Should AI Be Allowed to Do Everything?
&lt;/h1&gt;

&lt;p&gt;This is where "agentic AI" can become over-engineering.&lt;/p&gt;

&lt;p&gt;Just because an LLM can call a tool doesn't mean it should have unrestricted access to that tool.&lt;/p&gt;

&lt;p&gt;Imagine an AI agent with access to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delete Customer
Refund Payment
Send Email
Create Contract
Modify Database
Transfer Money
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giving the model all of these capabilities and saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use them responsibly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is not an enterprise architecture.&lt;/p&gt;

&lt;p&gt;It is a liability.&lt;/p&gt;

&lt;p&gt;Instead, actions should have explicit boundaries.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Low Risk
    ↓
Automatic Execution

Medium Risk
    ↓
Policy Check + Validation

High Risk
    ↓
Human Approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A refund under a small threshold might be automatic.&lt;/p&gt;

&lt;p&gt;A large financial transaction might require approval.&lt;/p&gt;

&lt;p&gt;Deleting an important customer record might require multiple controls.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;AI governance becomes part of engineering&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  11. Agentic AI Is Not "Let the LLM Run Wild"
&lt;/h1&gt;

&lt;p&gt;The word &lt;strong&gt;agent&lt;/strong&gt; is now used everywhere.&lt;/p&gt;

&lt;p&gt;But an agent is not simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM + Tool Calling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful agentic system needs some concept of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal
 ↓
Planning
 ↓
Action
 ↓
Observation
 ↓
Evaluation
 ↓
Next Action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
"Investigate why sales dropped last quarter."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent might reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Query sales database.
2. Compare previous quarter.
3. Identify affected regions.
4. Check product performance.
5. Retrieve sales strategy documents.
6. Check CRM notes.
7. Identify major changes.
8. Synthesize findings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The system can decide what information it needs and what actions to take next.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is more powerful than a fixed RAG pipeline.&lt;/p&gt;

&lt;p&gt;But it is also harder to control.&lt;/p&gt;




&lt;h1&gt;
  
  
  12. Agent Loops Are a Real Production Problem
&lt;/h1&gt;

&lt;p&gt;A demo agent may look impressive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
 ↓
Tool
 ↓
LLM
 ↓
Tool
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what happens when the model keeps calling tools?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search
 ↓
Search Again
 ↓
Search Again
 ↓
Search Again
 ↓
Search Again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;increased latency&lt;/li&gt;
&lt;li&gt;increased token usage&lt;/li&gt;
&lt;li&gt;increased API costs&lt;/li&gt;
&lt;li&gt;possible rate-limit failures&lt;/li&gt;
&lt;li&gt;unpredictable behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A production agent needs boundaries.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum Steps
Maximum Tool Calls
Maximum Runtime
Token Budget
Retry Limit
Allowed Tools
Allowed Arguments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And sometimes the best engineering decision is not to use an agent at all.&lt;/p&gt;

&lt;p&gt;If the workflow is deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1
 ↓
Step 2
 ↓
Step 3
 ↓
Step 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;a normal workflow engine may be safer and more predictable than an autonomous agent.&lt;/p&gt;

&lt;p&gt;This is a very important AI engineering principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use autonomy where uncertainty exists. Use deterministic software where determinism is possible.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  13. Memory Is More Than Chat History
&lt;/h1&gt;

&lt;p&gt;Another common misconception is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI memory means storing previous conversations."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's only one small part of the problem.&lt;/p&gt;

&lt;p&gt;Enterprise workflows often last much longer than a single conversation.&lt;/p&gt;

&lt;p&gt;Imagine an employee's hardware replacement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monday
↓
Issue reported

Tuesday
↓
Diagnostic information requested

Wednesday
↓
Diagnostics uploaded

Thursday
↓
Manager approval requested

Friday
↓
Replacement approved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI needs to understand the state of the workflow.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hardware_replacement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"employee_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMP-4821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"device_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LTP-8841"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manager_approval_pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INC-29482"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval_requested"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not simply conversation memory.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;application state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And this distinction is important for engineers coming from traditional software development.&lt;/p&gt;

&lt;p&gt;AI systems still need the same fundamentals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;persistence&lt;/li&gt;
&lt;li&gt;transactions&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;failure handling&lt;/li&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI doesn't remove software engineering.&lt;/p&gt;

&lt;p&gt;It increases the number of places where you need it.&lt;/p&gt;




&lt;h1&gt;
  
  
  14. Knowledge Graphs: Where Relationships Matter
&lt;/h1&gt;

&lt;p&gt;Vector search is extremely useful for semantic similarity.&lt;/p&gt;

&lt;p&gt;But semantic similarity is not the same as understanding relationships.&lt;/p&gt;

&lt;p&gt;Imagine an enterprise contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Employee
   ↓
Works For
   ↓
Department
   ↓
Owns
   ↓
Application
   ↓
Processes
   ↓
Customer Data
   ↓
Governed By
   ↓
Policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These relationships can matter more than textual similarity.&lt;/p&gt;

&lt;p&gt;A knowledge graph can explicitly represent them.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Employee → belongs_to → Department
Department → owns → Application
Application → accesses → Database
Database → contains → Customer_Data
Customer_Data → governed_by → Policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system can reason over relationships.&lt;/p&gt;

&lt;p&gt;This does not mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Knowledge graphs will replace vector databases."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The more realistic architecture is often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vector Search
+
Keyword Search
+
SQL
+
Knowledge Graph
+
APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different information requires different retrieval strategies.&lt;/p&gt;




&lt;h1&gt;
  
  
  15. The Modern Enterprise AI Architecture
&lt;/h1&gt;

&lt;p&gt;Once we combine these capabilities, the architecture becomes much more interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         USER
                           │
                           ↓
                   ┌─────────────────┐
                   │   AI Gateway    │
                   │ Auth / Limits   │
                   └────────┬────────┘
                           │
                           ↓
                   ┌─────────────────┐
                   │ Intent / Router │
                   └────────┬────────┘
                           │
                           ↓
                   ┌─────────────────┐
                   │ Planner / Agent │
                   └────────┬────────┘
                           │
           ┌────────────────┼────────────────┐
           ↓                ↓                ↓
       Retrieval          Tools            Memory
           │                │                │
           ↓                ↓                ↓
    Vector / Search      APIs / DB       State Store
           │                │                │
           └────────────────┼────────────────┘
                           ↓
                   ┌─────────────────┐
                   │ Policy Engine   │
                   └────────┬────────┘
                           │
                           ↓
                         LLM / Model
                           │
                           ↓
                   ┌─────────────────┐
                   │   Validator     │
                   └────────┬────────┘
                           │
                           ↓
                     Business Action
                           │
                           ↓
                   ┌─────────────────┐
                   │ Audit / Tracing │
                   └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG is still there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It just isn't the entire system.&lt;/p&gt;




&lt;h1&gt;
  
  
  16. The Model Is No Longer the Application
&lt;/h1&gt;

&lt;p&gt;This is perhaps the biggest conceptual shift for software engineers entering AI.&lt;/p&gt;

&lt;p&gt;In traditional application development, we might think:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   ↓
Backend
   ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In AI applications, beginners sometimes replace the backend with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually not enough.&lt;/p&gt;

&lt;p&gt;A production AI application still needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   ↓
Backend / AI Gateway
   ↓
Authentication
   ↓
Authorization
   ↓
Orchestration
   ↓
Models
   ↓
Retrieval
   ↓
Tools
   ↓
Databases
   ↓
Policies
   ↓
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is a component.&lt;/p&gt;

&lt;p&gt;It is not the whole application.&lt;/p&gt;

&lt;p&gt;This is why AI engineering is increasingly becoming an extension of software engineering.&lt;/p&gt;




&lt;h1&gt;
  
  
  17. RAG Should Not Be Used Everywhere
&lt;/h1&gt;

&lt;p&gt;This is worth saying explicitly.&lt;/p&gt;

&lt;p&gt;If the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is our remote-work policy?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RAG is a good fit.&lt;/p&gt;

&lt;p&gt;If the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is the current balance in my account?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use the source-of-truth system.&lt;/p&gt;

&lt;p&gt;If the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Calculate this month's revenue."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use a database or analytics system.&lt;/p&gt;

&lt;p&gt;If the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Create a support ticket."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use the ticketing API.&lt;/p&gt;

&lt;p&gt;If the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Explain why this transaction was rejected according to policy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You may need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction Data
+
Policy Retrieval
+
Business Rules
+
Reasoning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture should follow the problem.&lt;/p&gt;

&lt;p&gt;Not the other way around.&lt;/p&gt;

&lt;p&gt;This leads to a simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't force every enterprise problem into RAG.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  18. The Most Important Enterprise AI Problem: Reliability
&lt;/h1&gt;

&lt;p&gt;A chatbot can sometimes get away with being imperfect.&lt;/p&gt;

&lt;p&gt;Enterprise systems usually cannot.&lt;/p&gt;

&lt;p&gt;Imagine an AI assistant says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your refund has been processed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the refund API failed.&lt;/p&gt;

&lt;p&gt;The response sounds perfect.&lt;/p&gt;

&lt;p&gt;The user believes the transaction happened.&lt;/p&gt;

&lt;p&gt;But it didn't.&lt;/p&gt;

&lt;p&gt;This is much worse than a poorly written answer.&lt;/p&gt;

&lt;p&gt;Therefore enterprise AI needs &lt;strong&gt;verification&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A useful execution pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Decide
  ↓
Execute
  ↓
Verify
  ↓
Respond
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI decides:
"Create replacement ticket."

        ↓

Ticket API called.

        ↓

API returns:
ticket_id = INC-29482

        ↓

System verifies:
Ticket actually exists.

        ↓

User receives:
"Replacement request created successfully.
Ticket: INC-29482"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI should not simply claim that something happened.&lt;/p&gt;

&lt;p&gt;The system should verify that it actually happened.&lt;/p&gt;

&lt;h1&gt;
  
  
  19. Evaluation Must Go Beyond "The Answer Looks Good"
&lt;/h1&gt;

&lt;p&gt;This is another area where AI prototypes and production systems differ dramatically.&lt;/p&gt;

&lt;p&gt;A developer tests:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Ask the chatbot ten questions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answers look good, they conclude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The RAG system works."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not enough.&lt;/p&gt;

&lt;p&gt;Enterprise AI needs systematic evaluation.&lt;/p&gt;

&lt;p&gt;For retrieval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did we retrieve the correct source?
Did we retrieve enough relevant information?
Did we retrieve unauthorized information?
Did ranking put the best evidence first?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the answer grounded?
Is it relevant?
Did it introduce unsupported claims?
Did it cite the correct evidence?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did it choose the correct tool?
Did it use the correct arguments?
Did the tool succeed?
Did it recover from failure?
Did it stop when the task was complete?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For business workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Was the actual task completed?
Was policy followed?
Was authorization respected?
Was the final state correct?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evaluation target therefore becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model Quality
      +
Retrieval Quality
      +
Tool Reliability
      +
Policy Compliance
      +
Task Completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much closer to traditional software testing.&lt;/p&gt;




&lt;h1&gt;
  
  
  20. Observability Is Not Optional
&lt;/h1&gt;

&lt;p&gt;In a traditional backend application, when something fails, you inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logs
Metrics
Traces
Database State
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI systems need the same discipline.&lt;/p&gt;

&lt;p&gt;Suppose a user receives a wrong answer.&lt;/p&gt;

&lt;p&gt;You should be able to reconstruct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query
    ↓
Detected Intent
    ↓
Retrieved Sources
    ↓
Ranking
    ↓
Context Sent to Model
    ↓
Model Decision
    ↓
Tools Called
    ↓
Tool Arguments
    ↓
Tool Results
    ↓
Policy Checks
    ↓
Final Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this information, debugging becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The AI gave a weird answer."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not engineering.&lt;/p&gt;

&lt;p&gt;A production system should make the AI's execution trace inspectable.&lt;/p&gt;




&lt;h1&gt;
  
  
  21. AI Security Is Bigger Than Prompt Injection
&lt;/h1&gt;

&lt;p&gt;Prompt injection gets a lot of attention—and rightly so.&lt;/p&gt;

&lt;p&gt;But enterprise AI security is much broader.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input
    ↓
Prompt Injection
    ↓
Retrieval
    ↓
Sensitive Data
    ↓
Tool Call
    ↓
External System
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Potential risks exist at every stage.&lt;/p&gt;

&lt;p&gt;You need to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;data isolation&lt;/li&gt;
&lt;li&gt;prompt injection&lt;/li&gt;
&lt;li&gt;sensitive data exposure&lt;/li&gt;
&lt;li&gt;tool permissions&lt;/li&gt;
&lt;li&gt;API credentials&lt;/li&gt;
&lt;li&gt;malicious retrieved content&lt;/li&gt;
&lt;li&gt;unsafe tool arguments&lt;/li&gt;
&lt;li&gt;excessive autonomy&lt;/li&gt;
&lt;li&gt;auditability&lt;/li&gt;
&lt;li&gt;output validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never treat the LLM as a trusted security boundary.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The LLM can reason.&lt;/p&gt;

&lt;p&gt;The application must enforce security.&lt;/p&gt;




&lt;h1&gt;
  
  
  22. Cost and Latency Become Architecture Problems
&lt;/h1&gt;

&lt;p&gt;A demo can take 15 seconds to answer.&lt;/p&gt;

&lt;p&gt;A production customer-support assistant may not have that luxury.&lt;/p&gt;

&lt;p&gt;Imagine a single request triggers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query Rewrite
 ↓
Vector Search
 ↓
Keyword Search
 ↓
Reranking
 ↓
LLM Call
 ↓
SQL Query
 ↓
Another LLM Call
 ↓
API Call
 ↓
Validation
 ↓
Final LLM Call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system may be accurate.&lt;/p&gt;

&lt;p&gt;It may also be painfully slow and expensive.&lt;/p&gt;

&lt;p&gt;Therefore production AI engineering involves trade-offs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accuracy
    ↕
Latency
    ↕
Cost
    ↕
Reliability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes a smaller model is sufficient.&lt;/p&gt;

&lt;p&gt;Sometimes deterministic code is better.&lt;/p&gt;

&lt;p&gt;Sometimes retrieval can be skipped.&lt;/p&gt;

&lt;p&gt;Sometimes caching makes more sense.&lt;/p&gt;

&lt;p&gt;Sometimes an agent should be replaced with a fixed workflow.&lt;/p&gt;

&lt;p&gt;The best architecture is not the one with the most AI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It is the one that solves the business problem with the right amount of AI.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  23. The Evolution of Enterprise AI
&lt;/h1&gt;

&lt;p&gt;We can now summarize the architectural evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1 — LLM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
LLM
 ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writing&lt;/li&gt;
&lt;li&gt;summarization&lt;/li&gt;
&lt;li&gt;brainstorming&lt;/li&gt;
&lt;li&gt;general reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Problem:&lt;/p&gt;

&lt;p&gt;The model doesn't automatically know enterprise knowledge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2 — RAG
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Retriever
 ↓
Enterprise Knowledge
 ↓
LLM
 ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do we give the model private knowledge?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How does the system operate inside the business?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Stage 3 — Advanced RAG
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
 ↓
Query Understanding
 ↓
Hybrid Search
 ↓
Filtering
 ↓
Reranking
 ↓
Context Selection
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Improves knowledge access.&lt;/p&gt;

&lt;p&gt;Still primarily focused on answering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4 — Agentic RAG
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Planner
 ↓
Retrieve
 ↓
Evaluate
 ↓
Retrieve Again
 ↓
Reason
 ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrieval becomes dynamic.&lt;/p&gt;

&lt;p&gt;The system decides what information it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5 — Tool-Using AI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI
 ├── Search
 ├── SQL
 ├── CRM
 ├── ERP
 ├── Email
 └── Internal APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system can now perform operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 6 — Stateful AI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI
 +
Memory
 +
Workflow State
 +
History
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system can participate in long-running workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 7 — Governed Enterprise AI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Models
+
Knowledge
+
Tools
+
Memory
+
Permissions
+
Policies
+
Human Approval
+
Observability
+
Evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are getting closer to production enterprise AI.&lt;/p&gt;




&lt;h1&gt;
  
  
  24. What Should a Software Engineer Actually Build?
&lt;/h1&gt;

&lt;p&gt;If you're coming from a software engineering background and want to move into AI engineering, don't start by memorizing every AI framework.&lt;/p&gt;

&lt;p&gt;Start by learning how to design systems.&lt;/p&gt;

&lt;p&gt;A practical progression looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Software Engineering Fundamentals
        ↓
APIs + Databases + Authentication
        ↓
LLM APIs
        ↓
Embeddings + Retrieval
        ↓
RAG
        ↓
Evaluation
        ↓
Tool Calling
        ↓
Agents / Orchestration
        ↓
Memory / State
        ↓
Security + Governance
        ↓
Production AI Systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This path is much more valuable than simply learning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Framework A
Framework B
Framework C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because frameworks change.&lt;/p&gt;

&lt;p&gt;Architecture principles remain.&lt;/p&gt;




&lt;h1&gt;
  
  
  25. A Practical Enterprise AI Project
&lt;/h1&gt;

&lt;p&gt;If I were building a serious AI project to learn these concepts, I wouldn't build another:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Chat with PDF."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's useful for understanding RAG.&lt;/p&gt;

&lt;p&gt;But it doesn't demonstrate enough engineering depth.&lt;/p&gt;

&lt;p&gt;Instead, build something closer to:&lt;/p&gt;

&lt;h2&gt;
  
  
  AI IT Support Engineer
&lt;/h2&gt;

&lt;p&gt;Imagine an internal assistant for a company.&lt;/p&gt;

&lt;p&gt;A user can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My laptop is slow. Check whether my device is under warranty and tell me what I should do."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Intent Detection
 ↓
Employee Authentication
 ↓
Retrieve Device Information
 ↓
Query Asset Database
 ↓
Retrieve Warranty Policy
 ↓
Reason About Eligibility
 ↓
Respond
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extend it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Create a support ticket."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
AI
 ↓
Check Permission
 ↓
Create Ticket via API
 ↓
Verify Ticket
 ↓
Store Workflow State
 ↓
Return Ticket ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extend it again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happened to my ticket?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now the system retrieves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current Ticket State
+
Previous Actions
+
Relevant Policy
+
Conversation Context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you've built something much closer to a real AI system.&lt;/p&gt;

&lt;p&gt;And you've learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAG&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;tool calling&lt;/li&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;evaluation&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;business workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is AI engineering.&lt;/p&gt;




&lt;h1&gt;
  
  
  26. The Architecture Should Follow the Business Problem
&lt;/h1&gt;

&lt;p&gt;This is probably the single most important lesson from all of this.&lt;/p&gt;

&lt;p&gt;Don't start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to build an agent."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What problem am I solving?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which vector database should I use?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Where does the authoritative information live?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which LLM is the smartest?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What capabilities does this workflow actually require?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How autonomous can I make the system?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which decisions can safely be automated?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And don't start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I make the demo impressive?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How do I make the system reliable?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  27. RAG Is Not Dead
&lt;/h1&gt;

&lt;p&gt;After everything we've discussed, it would be easy to conclude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"RAG is outdated."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the wrong conclusion.&lt;/p&gt;

&lt;p&gt;RAG is not going away.&lt;/p&gt;

&lt;p&gt;It is becoming a &lt;strong&gt;component&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architectural shift is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old Mental Model

Enterprise AI
     =
LLM + Vector Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Modern Mental Model

Enterprise AI
     =
Model
+
Knowledge
+
Retrieval
+
Tools
+
Data
+
Memory
+
Policies
+
Workflows
+
Observability
+
Evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RAG remains one of the most important ways to provide contextual knowledge.&lt;/p&gt;

&lt;p&gt;But it no longer carries the entire responsibility.&lt;/p&gt;




&lt;h1&gt;
  
  
  28. The Real Evolution: From Answers to Outcomes
&lt;/h1&gt;

&lt;p&gt;This is where the story ultimately comes together.&lt;/p&gt;

&lt;p&gt;Early AI applications were primarily designed around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Give me an answer."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RAG improved that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Give me an answer based on my company's knowledge."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agentic systems push further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Figure out what needs to happen."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool-using systems go further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Do it."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stateful systems add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Remember where we are in the process."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Governed systems add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Do it within the rules."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production systems add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Prove that it actually worked."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the evolution is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer
  ↓
Grounded Answer
  ↓
Reasoned Decision
  ↓
Action
  ↓
Stateful Workflow
  ↓
Governed Automation
  ↓
Verified Business Outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the real evolution of enterprise AI.&lt;/p&gt;




&lt;h1&gt;
  
  
  29. The Software Engineer's Advantage in AI
&lt;/h1&gt;

&lt;p&gt;There is an interesting misconception that becoming an AI engineer means leaving software engineering behind.&lt;/p&gt;

&lt;p&gt;I don't think that's true.&lt;/p&gt;

&lt;p&gt;In fact, strong software engineering fundamentals become even more valuable.&lt;/p&gt;

&lt;p&gt;Because production AI still needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean APIs&lt;/li&gt;
&lt;li&gt;database design&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;queues&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;deployment&lt;/li&gt;
&lt;li&gt;scalability&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is that now one component of the system is probabilistic.&lt;/p&gt;

&lt;p&gt;And that creates a new engineering challenge.&lt;/p&gt;

&lt;p&gt;Traditional software usually aims for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Deterministic Logic → Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI systems often look more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input
 ↓
Probabilistic Reasoning
 ↓
Tool / System Interaction
 ↓
Validation
 ↓
Controlled Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the engineer's job becomes designing the boundaries around that probabilistic component.&lt;/p&gt;

&lt;p&gt;That is why I believe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The future AI engineer will not be less of a software engineer. They will need to be more of one.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  30. What Production AI Engineering Really Means
&lt;/h1&gt;

&lt;p&gt;A production AI engineer does not simply know how to call an LLM API.&lt;/p&gt;

&lt;p&gt;They think about the complete system.&lt;/p&gt;

&lt;p&gt;They ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What is the business objective?

Where is the source of truth?

What information does the model need?

What should be retrieved?

What should be queried directly?

What actions can the AI perform?

Who is authorized to perform them?

What happens if a tool fails?

What happens if the model is wrong?

What happens if the retrieved document is malicious?

What happens if the agent gets stuck?

What happens if the API times out?

How do we verify the result?

How do we evaluate the system?

How do we trace a failure?

How do we control cost?

How do we scale it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not "prompt engineering" questions.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;systems engineering questions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that is exactly where enterprise AI becomes interesting.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;RAG changed enterprise AI.&lt;/p&gt;

&lt;p&gt;It solved a fundamental problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How can an LLM access knowledge that isn't contained in its training data?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But enterprises eventually need more than knowledge.&lt;/p&gt;

&lt;p&gt;They need systems that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retrieve information&lt;/li&gt;
&lt;li&gt;reason over it&lt;/li&gt;
&lt;li&gt;access live data&lt;/li&gt;
&lt;li&gt;respect permissions&lt;/li&gt;
&lt;li&gt;call enterprise tools&lt;/li&gt;
&lt;li&gt;maintain workflow state&lt;/li&gt;
&lt;li&gt;follow business policies&lt;/li&gt;
&lt;li&gt;recover from failures&lt;/li&gt;
&lt;li&gt;verify actions&lt;/li&gt;
&lt;li&gt;and produce measurable business outcomes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM + Vector Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not an enterprise AI architecture by itself.&lt;/p&gt;

&lt;p&gt;A more realistic architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          Enterprise AI
                               │
           ┌────────────────────┼────────────────────┐
           ↓                    ↓                    ↓
       Knowledge              Actions              State
           │                    │                    │
         RAG                  Tools               Memory
           │                    │                    │
           └────────────────────┼────────────────────┘
                               ↓
                           Orchestration
                               ↓
                            Reasoning
                               ↓
                           Governance
                               ↓
                           Verification
                               ↓
                         Business Outcome
                               ↓
                      Observability + Evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RAG isn't disappearing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is becoming one layer of something much larger.&lt;/p&gt;

&lt;p&gt;The real evolution of enterprise AI is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RAG → Replace RAG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieval
   ↓
Reasoning
   ↓
Action
   ↓
State
   ↓
Governance
   ↓
Verification
   ↓
Reliable Business Workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And perhaps the most important shift is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The question is no longer "How do I build a better RAG chatbot?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The better question is "What business outcome should this AI system reliably accomplish, what information and tools does it need, what can go wrong, and how will I prove that it worked?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the point where building AI stops being about making an impressive demo.&lt;/p&gt;

&lt;p&gt;It starts becoming &lt;strong&gt;engineering&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Key Takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAG is a knowledge-access mechanism, not a complete enterprise AI architecture.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Retrieval does not automatically provide business context, permissions, live state, or actions.&lt;/li&gt;
&lt;li&gt;Structured data should often be accessed through SQL or APIs rather than semantic retrieval.&lt;/li&gt;
&lt;li&gt;Advanced RAG improves retrieval quality through techniques such as hybrid search, filtering, reranking, and query transformation.&lt;/li&gt;
&lt;li&gt;Agentic systems make retrieval and tool selection dynamic rather than completely predetermined.&lt;/li&gt;
&lt;li&gt;Tool calling allows AI systems to move from answering questions to performing business operations.&lt;/li&gt;
&lt;li&gt;Enterprise memory is often better understood as &lt;strong&gt;workflow state&lt;/strong&gt;, not merely conversation history.&lt;/li&gt;
&lt;li&gt;Knowledge graphs can complement vector retrieval when relationships between entities matter.&lt;/li&gt;
&lt;li&gt;Authorization must be enforced by the application architecture, not delegated to the LLM.&lt;/li&gt;
&lt;li&gt;Autonomous agents need limits around tools, steps, runtime, cost, and permissions.&lt;/li&gt;
&lt;li&gt;Deterministic workflows are often better than agents when the process itself is deterministic.&lt;/li&gt;
&lt;li&gt;Production AI requires verification—an AI saying an action happened is not proof that it actually happened.&lt;/li&gt;
&lt;li&gt;Evaluation should measure retrieval quality, groundedness, tool execution, policy compliance, and task completion.&lt;/li&gt;
&lt;li&gt;Observability is essential for debugging and improving AI systems.&lt;/li&gt;
&lt;li&gt;Cost, latency, security, reliability, and scalability are architectural concerns—not afterthoughts.&lt;/li&gt;
&lt;li&gt;The future enterprise AI stack is closer to:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Models
+
Knowledge
+
Retrieval
+
Tools
+
Data
+
Memory
+
Policies
+
Workflows
+
Evaluation
+
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A vibe coder asks: "Which AI tool can I plug in?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An AI engineer asks: "What problem am I solving, what system should own the truth, what can the AI do, what must it never do, and how will I know when it is wrong?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difference isn't the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's the engineering.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Rajshree&lt;/strong&gt;, a Software Engineer and Full Stack Developer passionate about building modern web applications and exploring the intersection of &lt;strong&gt;software engineering, AI, machine learning, and intelligent systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I enjoy turning ideas into working products, understanding how systems behave beyond the demo stage, writing about what I learn, and continuously exploring the transition from traditional software development to &lt;strong&gt;production-grade AI engineering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://rjshree.com" rel="noopener noreferrer"&gt;https://rjshree.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💼 &lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/rjshree" rel="noopener noreferrer"&gt;https://linkedin.com/in/rjshree&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/rjshree" rel="noopener noreferrer"&gt;https://github.com/rjshree&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, follow along for more writing on &lt;strong&gt;software engineering, AI, technology, system architecture, and the journey from developer to AI engineer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thanks for reading.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>ai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>OpenSparrow v3.2 - Per-User Access Control, Materialized Views in RAG, and a Business Design Refresh</title>
      <dc:creator>Tomasz</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:22:39 +0000</pubDate>
      <link>https://dev.to/wrobeltomasz/opensparrow-v32-per-user-access-control-materialized-views-in-rag-and-a-business-design-refresh-333o</link>
      <guid>https://dev.to/wrobeltomasz/opensparrow-v32-per-user-access-control-materialized-views-in-rag-and-a-business-design-refresh-333o</guid>
      <description>&lt;p&gt;&lt;strong&gt;OpenSparrow v3.2 is a security‑focused and consistency‑focused release that strengthens access boundaries, improves AI‑driven aggregate queries, and unifies the entire admin and frontend experience under one design system.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This update introduces per‑user access control across all major scopes, brings materialized‑view support to RAG aggregate queries, and delivers a full visual refresh that aligns every UI surface with the new Business design language.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe165ztp978f8mmeme9a6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe165ztp978f8mmeme9a6.gif" alt="Board view" width="720" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below is a detailed breakdown of the changes, including how they work, why they matter, and what they unlock for real deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Per-user access control
&lt;/h2&gt;

&lt;p&gt;New &lt;strong&gt;Users → Access&lt;/strong&gt; tab. Each user gets independent allow‑lists across five scopes — tables, views, printouts, boards, workflows — stored as &lt;code&gt;user_table_access&lt;/code&gt; in config.&lt;br&gt;&lt;br&gt;
The default is unrestricted: an absent or empty list means &lt;strong&gt;“no restriction”&lt;/strong&gt;, not &lt;strong&gt;“no access”&lt;/strong&gt;. To fully cut a user off, deactivate the account instead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforced at the API boundary by default, not just hidden in the UI — &lt;code&gt;os_api_action()&lt;/code&gt; / &lt;code&gt;os_api_dispatch()&lt;/code&gt; gate table, view, print, board, and workflow endpoints, and FK/file/comment/mass‑edit/data‑cleanup endpoints were extended to respect the same restrictions
&lt;/li&gt;
&lt;li&gt;Granting a board or workflow does not implicitly grant the tables it touches — a board whose table isn’t granted won’t show up, and a workflow drops if any step targets a table the user can’t reach
&lt;/li&gt;
&lt;li&gt;Hidden tables (no menu entry, no grid) can’t be ticked and are granted automatically
&lt;/li&gt;
&lt;li&gt;Delegated FK lookups and the schema endpoint are now scoped to what the requesting user can actually access, and the file listing is filtered by record ownership&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  RAG: materialized views as aggregate views
&lt;/h2&gt;

&lt;p&gt;The aggregate‑view feature from 3.1 (attach a PostgreSQL view to a table so the AI assistant can answer exact “total/how many” questions) now also accepts &lt;strong&gt;materialized views&lt;/strong&gt;, flagged as such in the admin UI — useful when the aggregate query is expensive enough that you’d rather refresh it on a schedule than recompute it on every chat message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mxmt4p6urpj4j6t39kb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mxmt4p6urpj4j6t39kb.gif" alt="RAG module" width="560" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Views sync also discovers materialized views alongside regular ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  A unified “Business” look
&lt;/h2&gt;

&lt;p&gt;The admin panel, setup wizard, and frontend were unified onto a single &lt;strong&gt;Business 4+1 color palette&lt;/strong&gt; and one &lt;strong&gt;system‑ui font stack&lt;/strong&gt;, replacing a mix of ad-hoc colors and font declarations across dozens of JS/CSS files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flw0dzjhgw6u90mkhp71x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flw0dzjhgw6u90mkhp71x.gif" alt="Admin view" width="720" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every front‑end button now shares one height, weight, border radius, and font size instead of drifting per page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Also in this release
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CRM demo: richer install description, plus optional demo‑user and audit‑history install toggles
&lt;/li&gt;
&lt;li&gt;Release packaging excludes &lt;code&gt;creator/&lt;/code&gt;, &lt;code&gt;dist-local/&lt;/code&gt;, and &lt;code&gt;storage/tmp/&lt;/code&gt; from image and release archives
&lt;/li&gt;
&lt;li&gt;Admin docs updated to match actual current module behavior across several sections
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Websites
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://opensparrow.org" rel="noopener noreferrer"&gt;opensparrow.org&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;I encourage a substantive discussion, and I'll do my best to answer any questions regarding implementation and configuration.&lt;/p&gt;

</description>
      <category>php</category>
      <category>lowcode</category>
      <category>postgres</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
