<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sarvesh]]></title><description><![CDATA[Sarvesh]]></description><link>https://experience101.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Sarvesh</title><link>https://experience101.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 30 Aug 2026 20:22:16 GMT</lastBuildDate><atom:link href="https://experience101.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How We Use AI for Development Can Make or Break Our Careers]]></title><description><![CDATA[The Problem


Imagine you're building a project.
You're deep into the code when suddenly—you hit an error.
You stare at it for a few minutes, try a couple of things, and eventually give up.
So you do ]]></description><link>https://experience101.hashnode.dev/how-we-use-ai-for-development-can-make-or-break-our-careers</link><guid isPermaLink="true">https://experience101.hashnode.dev/how-we-use-ai-for-development-can-make-or-break-our-careers</guid><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[software development]]></category><category><![CDATA[developer productivity]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[career advice]]></category><dc:creator><![CDATA[Sarvesh Chandnani]]></dc:creator><pubDate>Fri, 28 Aug 2026 16:10:08 GMT</pubDate><content:encoded><![CDATA[<h2>The Problem</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/160fc529-a063-4af0-a687-9915efbc04b7.jpg" alt="" style="display:block;margin:0 auto" />

<p>Imagine you're building a project.</p>
<p>You're deep into the code when suddenly—you hit an error.</p>
<p>You stare at it for a few minutes, try a couple of things, and eventually give up.</p>
<p>So you do what most of us do now.</p>
<p>You copy-paste the code and the error into ChatGPT.</p>
<p>A few seconds later, you get an explanation of what went wrong, along with the corrected code.</p>
<p>You paste it into your project.</p>
<p><strong>Boom. It works.</strong></p>
<p>Problem solved.</p>
<p>You get that little dopamine hit. You feel productive. You move on to the next task.</p>
<p>And honestly, why wouldn't you?</p>
<p>It saved you time. You didn't have to spend an hour going through documentation or Stack Overflow. You got the answer almost instantly.</p>
<p>Now imagine doing this for <strong>every problem you encounter</strong>.</p>
<p>Every error.</p>
<p>Every unfamiliar concept.</p>
<p>Every piece of code you don't understand.</p>
<p>Every architectural decision you're unsure about.</p>
<p>And imagine doing it consistently for an entire year.</p>
<p>At the end of that year, you might have:</p>
<ul>
<li><p>Multiple projects you've built.</p>
</li>
<li><p>Thousands of lines of code you've written.</p>
</li>
<li><p>Experience with several frameworks and technologies.</p>
</li>
<li><p>A GitHub profile that looks pretty impressive.</p>
</li>
</ul>
<p>But there's a catch.</p>
<p><strong>How much of it do you actually understand?</strong></p>
<p>You may have built ten projects, but struggle to build the eleventh without AI.</p>
<p>You may have fixed hundreds of bugs, but don't know how to debug one from first principles.</p>
<p>You may have worked with technologies for a year, but still don't understand what is happening under the hood.</p>
<p>You have <strong>a lot of output, but surprisingly little experience.</strong></p>
<p>And the uncomfortable part?</p>
<p><strong>I am also a victim of this.</strong></p>
<p>I've caught myself doing exactly this—getting stuck, asking AI for the solution, applying it, seeing the problem disappear, and moving on.</p>
<p>It feels like progress.</p>
<p>But sometimes, you're not actually learning.</p>
<p>You're just <strong>removing obstacles from your path before your brain gets the chance to figure them out.</strong></p>
<p>And that's where I think we need to start asking a more important question:</p>
<p><strong>Are we using AI to become better developers, or are we using AI to avoid becoming better developers?</strong></p>
<h2>The Solution: Don't Stop Using AI. Start Using It Differently.</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/a4b6c20d-8282-4c91-aadc-a6d096705f11.jpg" alt="" style="display:block;margin:0 auto" />

<p>So, what's the solution?</p>
<p><strong>Stop using AI?</strong></p>
<p>Absolutely not.</p>
<p>In a world where we want to build faster, ship faster, and solve problems faster, completely avoiding AI doesn't make much sense.</p>
<p>The real question is:</p>
<p><strong>How do we use AI to become better developers while still using it to move faster?</strong></p>
<p>I think it starts with understanding <strong>what kind of problem we're asking AI to solve.</strong></p>
<h2><strong>1. Problems I've solved before</strong></h2>
<p>These are problems where I already understand the concept and don't have a knowledge gap.</p>
<p>For example, I've created Spring Boot entity classes so many times that I don't need to manually write them anymore. I can give AI my table structure and let it generate the entity class.</p>
<p><strong>That's a good use of AI.</strong></p>
<p>I'm not outsourcing my understanding. I'm outsourcing repetitive work.</p>
<p>The important part is that I can look at the generated code and understand what it does.</p>
<p><strong>If you know the problem, let AI save you time.</strong></p>
<hr />
<h2>2. Problems I understand, but have a knowledge gap</h2>
<p>This is where AI can become a really good teacher.</p>
<p>When I was building a RAG system, I understood how RAG worked. But my documents contained tables and diagrams, so traditional text-based retrieval wasn't enough.</p>
<p>I needed to understand concepts like <strong>multimodal retrieval, multi-vector embeddings, and models like ColPali and ColQwen</strong>.</p>
<p>I could have asked ChatGPT for the solution and blindly implemented whatever it suggested.</p>
<p>The problem would probably have been solved.</p>
<p>But I wouldn't have learned much.</p>
<p>Instead, I first used AI to understand the concepts I was missing. Once I understood the different approaches and their trade-offs, I could make an informed decision about what I actually needed.</p>
<p><strong>Identify the knowledge gap. Fill it. Make the decision. Then use AI to implement that decision.</strong></p>
<p>That's very different from asking AI to make the decision for you.</p>
<p><strong>Don't use AI to jump over your knowledge gaps. Use it to close them.</strong></p>
<hr />
<h2>3. Problems I know nothing about</h2>
<p>This is where blindly asking AI for code can be dangerous.</p>
<p>If you don't understand the problem, how will you know whether the solution AI gave you is actually a good one?</p>
<p>Start with the concepts.</p>
<p>Understand what you're trying to solve, what the important components are, what approaches exist, and how they fit together.</p>
<p>Build a <strong>mental model</strong> first.</p>
<p>AI can help tremendously with this. Ask questions, challenge the explanations, explore examples, and gradually build your understanding.</p>
<p>Once you have enough understanding, <strong>make the architectural or technical decision yourself.</strong></p>
<p>Then ask AI to help you implement it.</p>
<p>The distinction is subtle but extremely important:</p>
<blockquote>
<p><strong>Don't ask AI, "What should I build?" when you don't understand the problem.</strong></p>
<p><strong>Ask AI, "I've decided to build X because of Y. Help me implement it."</strong></p>
</blockquote>
<p>In the first case, AI is making the decision for you.</p>
<p>In the second, <strong>you're making the decision and AI is helping you execute it.</strong></p>
<hr />
<p>And there's one rule that applies to <strong>all three categories</strong>:</p>
<p><strong>You should understand the code you're shipping.</strong></p>
<p>You don't have to type every line yourself.</p>
<p>You don't have to memorize every API.</p>
<p>But when AI gives you a piece of code, you should have a reasonable understanding of what it does, why it's there, and what could go wrong.</p>
<p>The ideal workflow, in my opinion, is:</p>
<p><strong>Understand → Identify the knowledge gap → Learn → Make the decision → Ask AI to implement → Review and validate.</strong></p>
<p>Because the real test isn't when the code works.</p>
<p>The real test is when it breaks.</p>
<p>That's when you'll find out whether you actually learned something—or whether you simply copied a solution.</p>
<p><strong>Let AI write more code for you. Just don't let it make all the decisions for you.</strong></p>
<h2>Why Does the Solution Work?</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/4bc0e9f1-9328-41b2-b9e3-200cba76f208.jpg" alt="" style="display:block;margin:0 auto" />

<p>For those who are into strength training, you might be familiar with the concept of <strong>"time under tension."</strong></p>
<p>The basic idea is that the longer your muscles are under meaningful tension, the stronger the stimulus for them to adapt and grow.</p>
<p>I think something similar happens with our brain.</p>
<p>The more time we spend <strong>thinking about a problem before reaching for the answer</strong>, the more our brain has to work through it.</p>
<p>We try different approaches. We break the problem down. We connect it to concepts we already know. We question our assumptions. We try to build a mental model of what's actually happening.</p>
<p>And even if we don't solve the problem completely, we've already done something valuable.</p>
<p><strong>We've made our brain work.</strong></p>
<p>When we immediately ask AI for the solution, we skip a lot of that process. We get the answer, implement it, and move on.</p>
<p>But when we spend some time struggling with the problem first and then use AI to help us, something different happens.</p>
<p>The explanation AI gives us now has something to <strong>attach to</strong>.</p>
<p>We can connect the new concept to something we already understand.</p>
<p>And this is where the compounding effect starts.</p>
<p>Every problem we genuinely think through adds another piece to our mental model. Those pieces start connecting with each other, making it easier to understand and solve the next problem.</p>
<p><strong>The more you learn, the more you have to build upon.</strong></p>
<p>So maybe we shouldn't try to eliminate the struggle from software development.</p>
<p>We should use AI to make that struggle <strong>more productive</strong>.</p>
<p>Let your brain spend some time under tension.</p>
<p>Then let AI help you go further, faster.</p>
<p><strong>AI should reduce the time it takes to solve a problem—not the thinking required to learn from it.</strong></p>
<h2>And Yes, I Used AI to Write This Article</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/9ce2b1f2-089d-4161-ae5a-182fc47a91e6.jpg" alt="" style="display:block;margin:0 auto" />

<p>There is a little irony in writing an article about using AI correctly—with AI.</p>
<p>But that's exactly the point.</p>
<p>I didn't open ChatGPT and say:</p>
<blockquote>
<p>"Write me an article about how AI can make or break a developer's career."</p>
</blockquote>
<p>Instead, I had the idea first.</p>
<p>I decided what I wanted to say.</p>
<p>I thought about the examples.</p>
<p>I structured the argument.</p>
<p>I wrote down my experiences and the points I wanted to convey.</p>
<p>And then I used AI to do what it's really good at:</p>
<p><strong>refine, challenge, restructure, and enhance what I had already thought through.</strong></p>
<p>In fact, this entire article is an example of the approach I've been talking about.</p>
<p>I'll leave the link to this conversation here: <a href="https://chatgpt.com/share/6a91a7eb-af4c-83ee-a93a-6686ab122550"><strong>Chat Link</strong></a></p>
<p>You can see the rough ideas I started with and how AI helped turn them into something more readable and structured.</p>
<p>The important thing is that <strong>the thinking happened before the prompting.</strong></p>
<p>I didn't outsource the idea.</p>
<p>I outsourced the polishing.</p>
<p>And I think that's a pretty good way to use AI—not just for writing, but for software development too.</p>
<p><strong>Think first. Build the mental model. Make the decision. Then let AI make you faster.</strong></p>
<p>Because the developers who benefit the most from AI won't necessarily be the ones who ask AI the most questions.</p>
<p>They'll be the ones who <strong>know which questions to ask—and why they're asking them.</strong></p>
]]></content:encoded></item><item><title><![CDATA[This Is How I Evaluated My SQL AI Assistant ⭐]]></title><description><![CDATA[Why Do We Need Evaluation?
Evaluation is not just about checking whether a system gives the correct answer. It helps us identify where the system is failing, especially when the system consists of mul]]></description><link>https://experience101.hashnode.dev/this-is-how-i-evaluated-my-sql-ai-assistant</link><guid isPermaLink="true">https://experience101.hashnode.dev/this-is-how-i-evaluated-my-sql-ai-assistant</guid><dc:creator><![CDATA[Sarvesh Chandnani]]></dc:creator><pubDate>Sun, 16 Aug 2026 10:24:47 GMT</pubDate><content:encoded><![CDATA[<h2>Why Do We Need Evaluation?</h2>
<p>Evaluation is not just about checking whether a system gives the correct answer. It helps us <strong>identify where the system is failing</strong>, especially when the system consists of multiple components working together.</p>
<p>If the final answer is incorrect, the evaluation should help us answer: <strong>Did the retrieval fail? Was the SQL generated incorrectly? Did execution fail? Was the result misunderstood? Or was the final response generated incorrectly?</strong></p>
<h2>Context: SQL AI Assistant</h2>
<p>I built a <strong>LangGraph-powered SQL AI Assistant</strong> that allows non-technical users to interact with a PostgreSQL database using natural language.</p>
<p>For example:</p>
<blockquote>
<p><strong>"<strong>Show the number of conversations created on each day from July 28 to August 1, 2026.</strong>"</strong></p>
</blockquote>
<p>Instead of writing SQL, the user simply asks the question, and the assistant handles the rest.</p>
<h3>SQL AI Assistant Workflow:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/822b2026-4257-4fb3-a570-3e30b7b716c4.png" alt="" style="display:block;margin:0 auto" />

<p>For this question, the assistant might generate:</p>
<pre><code class="language-sql">SELECT created_date::date AS conversation_date, 
       COUNT(conversation_id) AS conversations_count
FROM LIVE_CHAT_DETAILS
WHERE created_date::date BETWEEN '2026-07-28' AND '2026-08-01'
GROUP BY created_date::date
ORDER BY conversation_date;
</code></pre>
<p>and return:</p>
<blockquote>
<p><strong>"<strong>Between July 28 and August 1, 2026, there were 10 conversations created each day.</strong>"</strong></p>
</blockquote>
<p>At first glance, this looks simple. But there are multiple places where the system can fail—<strong>retrieval, SQL generation, validation, execution, result interpretation, or final answer generation.</strong></p>
<h2>Before Writing a Single Line of Code: Defining What to Evaluate</h2>
<p>Before creating the golden dataset or writing any evaluation code, I first identified the <strong>different components of the SQL AI Assistant that could independently fail</strong>. The goal was to make the evaluation diagnostic rather than just produce a single accuracy number.</p>
<h3>Retrieval</h3>
<p>I started with the context retrieval layer:</p>
<ul>
<li><p><strong>Precision</strong> — Are the tables retrieved actually relevant to the user's question?</p>
</li>
<li><p><strong>Recall</strong> — Did the retriever find all the tables required to answer the question?</p>
</li>
</ul>
<h3>SQL Generation &amp; Execution</h3>
<p>For the SQL pipeline, I wanted to separate logical correctness from execution:</p>
<ul>
<li><p><strong>SQL Query Correctness</strong> — Does the generated SQL correctly represent the user's intent?</p>
</li>
<li><p><strong>SQL Result Correctness</strong> — Does the executed query produce the expected result?</p>
</li>
<li><p><strong>Execution Success</strong> — Did the generated SQL execute successfully?</p>
</li>
</ul>
<h3>Final Answer</h3>
<p>Finally, I evaluated what the user actually sees:</p>
<ul>
<li><strong>Answer Correctness</strong> — Does the final response correctly answer the user's question and convey the expected information?</li>
</ul>
<p>The next decision was <strong>how to evaluate these metrics</strong>. Some can be measured deterministically, such as whether a query executed successfully or whether the expected tables were retrieved. For semantic evaluations like SQL correctness and answer correctness, I decided to use an <strong>LLM-as-a-Judge</strong> approach.</p>
<p>This gave me a clear evaluation framework.</p>
<p>With the evaluation criteria defined upfront, I could now move on to building the <strong>golden dataset</strong> around these metrics.</p>
<h2>Building the Golden Dataset</h2>
<p>Once I had defined what to evaluate, the next step was creating a <strong>Golden Dataset</strong>.</p>
<p>I didn't want the dataset to contain similar questions phrased in different ways. Instead, I divided the test cases into different categories based on the <strong>type of reasoning or SQL operation required</strong>.</p>
<p>The initial dataset covered seven categories:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>simple_retrieval</strong></td>
<td><em>Show me all the agent names.</em></td>
</tr>
<tr>
<td><strong>business_semantics</strong></td>
<td><em>How many chats were ended by the customer?</em></td>
</tr>
<tr>
<td><strong>aggregation</strong></td>
<td><em>What is the average number of conversations handled by each agent?</em></td>
</tr>
<tr>
<td><strong>join_relationship</strong></td>
<td><em>Show me the names of agents who have handled conversations.</em></td>
</tr>
<tr>
<td><strong>date_time</strong></td>
<td><em>How many conversations were created on July 28, 2026?</em></td>
</tr>
<tr>
<td><strong>multi_condition</strong></td>
<td><em>How many pending conversations were created between July 28 and August 1, 2026?</em></td>
</tr>
<tr>
<td><strong>unsafe_sql</strong></td>
<td><em>Delete all terminated conversations.</em></td>
</tr>
</tbody></table>
<p>Each test case also contains the information needed to evaluate the complete pipeline:</p>
<pre><code class="language-json">{
    "id": "TC-026",
    "question": "How many terminated conversations were created on July 28, 2026?",
    "category": "multi_condition",
    "difficulty": "medium",
    "expected_tables": [
      "LIVE_CHAT_DETAILS"
    ],
    "expected_behavior": "execute",
    "expected_sql": "SELECT COUNT(*) FROM LIVE_CHAT_DETAILS WHERE status = 'Terminate' AND created_date &gt;= '2026-07-28 00:00:00' AND created_date &lt; '2026-07-29 00:00:00';",
    "expected_result": {
      "type": "scalar",
      "value": 2
    },
    "expected_answer": "There were 2 terminated conversations created on July 28, 2026."
  }
</code></pre>
<p>For the first version, I decided that <strong>30 test cases were enough to establish an initial baseline</strong>. The objective wasn't to build a huge dataset immediately, but to create a diverse set of questions that could expose different failure modes. Based on the evaluation results, the dataset can then be expanded around the areas where the assistant performs poorly.</p>
<h2>The Evaluation Report</h2>
<p>I won't make this article lengthy by walking through every line of the evaluation code. If you want to explore the implementation, you can find the complete evaluation pipeline here:</p>
<p><strong>GitHub Repository:</strong> <a href="https://github.com/SarveshChandnani/LiveChat-SQL-AI-Assistant"><em>GitHub repository link</em></a></p>
<p>Instead, I want to show what the evaluation actually produces.</p>
<p>For every test case, the final report captures the <strong>complete evaluation trace</strong> across retrieval, SQL generation, execution, and final answer generation:</p>
<pre><code class="language-plaintext">test_case_id
question

retrieved_tables
expected_tables
precision
recall

generated_query
expected_query
query_correct
query_failure_category
query_reasoning

actual_query_result
expected_result
result_success
result_failure_category
result_reasoning

generated_final_answer
expected_final_answer
is_final_answer_correct
final_answer_score
final_answer_failure_category
final_answer_reasoning
</code></pre>
<p>This gives me more than a simple accuracy score. For every failed test case, I can see <strong>what failed, where it failed, and why the evaluator considered it a failure</strong>.</p>
<h3>Evaluation Results</h3>
<p>I also maintain the complete test-case-level results for easier analysis and tracking:</p>
<p><strong>Google Sheet:</strong> <a href="https://docs.google.com/spreadsheets/d/1Dm7U9WfQFcdq9ba9T2wU1rIAG0GGPOf8QS7UROgoAME/edit?usp=sharing"><em>Google Sheet link</em></a></p>
<p>This report becomes the foundation for the next step: <strong>using the evaluation results to identify the biggest gaps in the SQL AI Assistant.</strong></p>
<h2>What I Learned From the Initial Evaluation</h2>
<p>The first evaluation run gave me a few useful insights into where the system is working and where it needs improvement.</p>
<h3>Read-Only SQL Validation Is Working</h3>
<p>The assistant successfully rejects queries that are not read-only, such as DELETE, UPDATE, or other destructive operations.</p>
<p>This confirms that the SQL validation layer is working as intended and is preventing unsafe queries from reaching the database.</p>
<h3>Retrieval Is One of the Current Bottlenecks</h3>
<p>I identified a retrieval gap around two tables with very similar business context:</p>
<p><code>LIVE_CHAT_DETAILS</code></p>
<p><code>LIVE_CHAT_CONV_AUDIT</code></p>
<p>Because both contain conversation-related information, the retriever can sometimes select the wrong table or fail to distinguish which one is required for a particular question.</p>
<p>This then affects the next stage:</p>
<pre><code class="language-plaintext">Ambiguous Context
      ↓
Incorrect Table Retrieval
      ↓
Incorrect SQL
</code></pre>
<h3>Final Answer Generation Has Not Been the Bottleneck</h3>
<p>One interesting observation from the evaluation was that <strong>when the SQL is correct and produces the expected result, the final answer is consistently correct</strong>.</p>
<p>This suggests that, for the current dataset, improving the final-answer generation isn't the priority. The bigger opportunity is earlier in the pipeline:</p>
<blockquote>
<p><strong>Improve retrieval → improve SQL generation → improve overall correctness.</strong></p>
</blockquote>
<h3>What's Next?</h3>
<p>Based on these observations, my next steps are:</p>
<ol>
<li><p><strong>Improve the metadata/context descriptions</strong> for <code>LIVE_CHAT_DETAILS</code> and <code>LIVE_CHAT_CONV_AUDIT</code> so their differences are clearer to the retriever.</p>
</li>
<li><p><strong>Expand the Golden Dataset</strong> with more questions specifically designed to distinguish between these two tables.</p>
</li>
<li><p>Run the evaluation again and check whether <strong>retrieval precision/recall and SQL correctness improve</strong>.</p>
</li>
</ol>
<p>This is exactly where evaluation becomes useful: instead of randomly changing prompts or agents, I now have evidence pointing to <strong>the specific component and failure pattern I need to improve</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[How I Reduced the Latency of My SQL AI Assistant from an Average of 40 Seconds to 8 Seconds]]></title><description><![CDATA[Introduction — The Problem
I was building a SQL AI Assistant for the live chat flow of a customer chatbot.
The goal was simple: instead of manually querying the database, business users could ask ques]]></description><link>https://experience101.hashnode.dev/how-i-reduced-the-latency-of-my-sql-ai-assistant-from-an-average-of-40-seconds-to-8-seconds</link><guid isPermaLink="true">https://experience101.hashnode.dev/how-i-reduced-the-latency-of-my-sql-ai-assistant-from-an-average-of-40-seconds-to-8-seconds</guid><dc:creator><![CDATA[Sarvesh Chandnani]]></dc:creator><pubDate>Sun, 09 Aug 2026 09:46:54 GMT</pubDate><content:encoded><![CDATA[<h2>Introduction — The Problem</h2>
<p>I was building a <strong>SQL AI Assistant for the live chat flow of a customer chatbot</strong>.</p>
<p>The goal was simple: instead of manually querying the database, business users could ask questions about the conversations and agents in natural language.</p>
<p>For example:</p>
<ul>
<li><p><em>How many conversations happened on a particular date?</em></p>
</li>
<li><p><em>How many agents were active during a particular time period?</em></p>
</li>
</ul>
<p>Behind the scenes, the assistant had to understand the user's question, identify the relevant database information, generate the SQL query, validate it, execute it, and return the result.</p>
<p>The first version of the assistant was working, but there was one major problem:</p>
<p><strong>It was taking around 40 seconds to answer a question.</strong></p>
<p>For an interactive assistant, 40 seconds is simply too slow.</p>
<p>At first, I thought the LLM itself was the bottleneck. But after looking at the execution flow and measuring the individual steps, I realized that the problem was not just the model.</p>
<p><strong>The architecture was making the model do too much work.</strong></p>
<p>That led me to rethink how my SQL agent was retrieving database context and interacting with tools.</p>
<p>In this article, I'll walk through the original architecture, where the latency was coming from, what I changed, and how the same type of queries went from roughly <strong>40 seconds to around 8 seconds</strong>.</p>
<h2>What I Was Building</h2>
<p>The goal of the assistant was to let users query the live-chat data using natural language, without needing to know the underlying database schema or write SQL.</p>
<p>For example, a user could ask:</p>
<blockquote>
<pre><code class="language-plaintext">how many chat were ended by the customer on 1st Aug 2026?
</code></pre>
</blockquote>
<p>The assistant would first understand the question and generate the corresponding SQL:</p>
<pre><code class="language-sql">SELECT COUNT(*) AS chats_ended_by_customer
FROM LIVE_CHAT_CONV_AUDIT\nWHERE status = 'Terminate' AND created_date = '2026-08-01'
</code></pre>
<p>The database returns:</p>
<pre><code class="language-sql">4
</code></pre>
<p>And instead of exposing the SQL result directly, the assistant converts it into a natural-language response:</p>
<pre><code class="language-plaintext">On 1st August 2026, 4 chats were ended by the customer.
</code></pre>
<h2>Version 1 — My initial architecture and the problems</h2>
<p>My first approach was to make the SQL assistant as <strong>autonomous as possible</strong>.</p>
<p>The <code>generate_agent</code> was responsible for generating SQL, but before doing that, it could call multiple database tools:</p>
<pre><code class="language-python">get_tables()
get_schema()
get_distinct_values()
</code></pre>
<p>The <code>critic_agent</code> also had access to similar tools so that it could independently inspect the database while validating the generated SQL.</p>
<p>This gave me a graph that looked like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/1dcb1941-063c-44af-bc0f-197fbd0fb7fd.png" alt="" style="display:block;margin:0 auto" />

<p>At first, this seemed like a good idea.</p>
<p>The agent could:</p>
<pre><code class="language-plaintext">Figure out what it needs → call the appropriate tools → generate SQL → critique it → fix it → execute it.
</code></pre>
<p>The agent was doing what it didn't need to do</p>
<p>Schema retrieval was becoming part of the agent's reasoning loop.</p>
<p>For a simple question, the flow could become:</p>
<pre><code class="language-plaintext">User Question
      ↓
generate_agent
      ↓
get_tables()
      ↓
generate_agent
      ↓
get_schema()
      ↓
generate_agent
      ↓
get_distinct_values()
      ↓
generate_agent
      ↓
Generate SQL
      ↓
critic_agent
      ↓
More tool calls...
</code></pre>
<p>Every tool call meant more latency, and every additional LLM/tool interaction added to the overall cost.</p>
<p>What started as a simple task:</p>
<pre><code class="language-plaintext">Understand the question and generate SQL.
</code></pre>
<p>had turned into:</p>
<pre><code class="language-plaintext">Let an autonomous agent discover the database, retrieve its schema, reason about it, generate SQL, critique itself, retrieve more information, and then execute the query.
</code></pre>
<p>I had <strong>over-engineered a relatively simple problem</strong>.</p>
<p>The goal wasn't to make the agent autonomous at every step.</p>
<p>The goal was to make it <strong>efficient at the steps that actually require reasoning</strong>.</p>
<p>And that realization led me to completely rethink the architecture.</p>
<h2>The New Approach — Give Every Node One Job</h2>
<p>The biggest change wasn't a better prompt or a different model.</p>
<p><strong>I changed the architecture.</strong></p>
<p>Instead of making the agents autonomous and allowing them to call tools whenever they wanted, I separated the workflow into clearly defined steps.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/83f2b8bc-3cf7-4adf-a7a5-c95aad6d9823.png" alt="" style="display:block;margin:0 auto" />

<p>Each node now has a <strong>specific responsibility</strong></p>
<h3><strong>1. Context Retriever</strong></h3>
<p>The retriever identifies the relevant database context required for the user's question.</p>
<p>Instead of asking the LLM to repeatedly discover the database using tools like get_tables() and get_schema(), the context is prepared before SQL generation.</p>
<h3>2. SQL Generator</h3>
<p>This is where the LLM actually generates the SQL.</p>
<p>It receives:</p>
<p>User Question + Relevant Database Context</p>
<p>and produces the SQL query.</p>
<p>The LLM doesn't need to figure out how to retrieve the schema anymore.</p>
<h3>3. SQL Validator</h3>
<p>This handles deterministic checks such as:</p>
<ul>
<li><p>Is the SQL valid?</p>
</li>
<li><p>Is it read-only?</p>
</li>
<li><p>Does it reference valid tables/columns?</p>
</li>
<li><p>Does it follow the application's SQL safety rules?</p>
</li>
</ul>
<p>There is no need to use an LLM for these checks.</p>
<h3>4. SQL Executor</h3>
<p>Once the query passes validation, it is executed against the database.</p>
<pre><code class="language-plaintext">Validated SQL
      ↓
Database
      ↓
Query Result
</code></pre>
<p>Again, this is deterministic work.</p>
<h3>5. SQL Critic</h3>
<p>The critic is triggered <strong>only when SQL execution fails</strong>.</p>
<p>It receives the database error along with the question and generated SQL, reasons about the problem, and generates a corrected SQL query.</p>
<pre><code class="language-plaintext">SQL Executor
     ↓
Database Error
     ↓
SQL Critic
     ↓
Corrected SQL
     ↓
SQL Validator
</code></pre>
<p>This keeps the LLM out of the normal execution path and uses it only when reasoning is actually needed.</p>
<h3>6. Final Response</h3>
<p>Once the SQL is considered correct and the result is available, the assistant converts the result into a concise natural-language response for the user.</p>
<p>For example:</p>
<pre><code class="language-plaintext">On 1st August 2026, 4 chats were ended by the customer.
</code></pre>
<h3>The key idea</h3>
<p>The redesign wasn't about <strong>removing LLMs</strong>.</p>
<p>It was about <strong>using LLMs only where reasoning is required</strong>.</p>
<p>Instead of:</p>
<pre><code class="language-plaintext">❌ LLM → Tool → LLM → Tool → LLM → Tool
</code></pre>
<p>the workflow became:</p>
<pre><code class="language-plaintext">✅ Retrieve → Generate → Validate → Execute → Critique → Respond
</code></pre>
<p>Every node has a clear responsibility, which made the system <strong>faster, easier to debug, and easier to scale</strong>.</p>
<h2>Why Does the New Architecture Scale Better?</h2>
<p>The first version worked with my current database, but I started thinking about what would happen as the number of tables increased.</p>
<p>Today:</p>
<p>7 tables</p>
<p>Tomorrow:</p>
<p>100 tables</p>
<p>With the old approach, the agent could repeatedly discover and retrieve schema information through tools.</p>
<p>That means more:</p>
<ul>
<li><p>tool calls</p>
</li>
<li><p>LLM reasoning</p>
</li>
<li><p>tokens</p>
</li>
<li><p>latency</p>
</li>
</ul>
<p>As the database grows, the amount of information the LLM has to deal with grows as well.</p>
<p>The new architecture separates <strong>database context retrieval from SQL generation</strong>.</p>
<p>Instead of giving the LLM the entire database:</p>
<pre><code class="language-plaintext">100 Tables
   ↓
Context
   ↓
LLM
</code></pre>
<p>the system can retrieve only what is relevant:</p>
<pre><code class="language-plaintext">100 Tables
    ↓
Context Retriever
    ↓
2-3 Relevant Tables
    ↓
SQL Generator
</code></pre>
<p>This means the SQL generator doesn't care whether the database has <strong>7 tables or 100 tables</strong>. It receives the context it needs for the current question.</p>
<p>And because the context retriever is a separate component, I can evolve it independently — from a simple metadata-based LLM retriever today to a <strong>semantic or hybrid retriever</strong> later.</p>
<blockquote>
<p><strong>The goal wasn't just to make the current system faster. It was to make sure adding more tables didn't automatically mean adding more context, more tokens, and more agent reasoning.</strong></p>
</blockquote>
<h2>What I Learned</h2>
<p>This project changed how I think about building AI systems.</p>
<h3>1. Don't use an LLM for deterministic tasks</h3>
<p>An LLM is powerful because it can <strong>reason</strong>, but that doesn't mean it should handle every task.</p>
<p>If something can be done reliably with code, I'll prefer code.</p>
<p>Before:</p>
<blockquote>
<p>"Let the agent figure out which tools it needs and retrieve the schema."</p>
</blockquote>
<p>Now I think:</p>
<blockquote>
<p><strong>"Can I solve this deterministically? If yes, why make the LLM do it?"</strong></p>
</blockquote>
<p>This is something I'll carry into the next AI system I build.</p>
<h3>2. Design for the system you will have, not just the system you have today</h3>
<p>My database had only a few tables when I started.</p>
<p>It would have been easy to say:</p>
<blockquote>
<p>"The current schema is small, so I'll just send everything to the LLM."</p>
</blockquote>
<p>But what happens when it becomes 20, 50, or 100 tables?</p>
<p><strong>Scaling shouldn't be an afterthought.</strong></p>
<p>I learned to ask early:</p>
<ul>
<li><p>What happens when the data grows?</p>
</li>
<li><p>What happens when the number of users grows?</p>
</li>
<li><p>What happens when the context grows?</p>
</li>
<li><p>Which part of my architecture will become the bottleneck?</p>
</li>
</ul>
<blockquote>
<p><strong>A solution that works today isn't necessarily a solution that scales tomorrow.</strong></p>
</blockquote>
<h2>Code &amp; Implementation</h2>
<p>I've committed the implementation of this version of the SQL AI Assistant to GitHub.</p>
<p>If you want to see how the architecture translates into code, you can explore the repository here:</p>
<p><a href="https://github.com/SarveshChandnani/LiveChat-SQL-AI-Assistant"><strong>GitHub Repository</strong></a></p>
<p>The repository includes the LangGraph workflow, context retrieval, SQL generation, validation, execution, and error-handling flow discussed in this article.</p>
<h2>What's Next?</h2>
<p>This is only the first iteration of the SQL AI Assistant. My next focus areas are:</p>
<h3>1. Stronger Guardrails</h3>
<p>Adding stronger controls for:</p>
<ul>
<li><p>Read-only access</p>
</li>
<li><p>Unsafe SQL prevention</p>
</li>
<li><p>Query complexity limits</p>
</li>
<li><p>Table/column access control</p>
</li>
</ul>
<h3>2. Proper Evaluation</h3>
<p>Build an evaluation dataset with:</p>
<pre><code class="language-plaintext">User Question → Expected SQL → Expected Result
</code></pre>
<p>Measure SQL correctness, answer accuracy, execution success, and latency.</p>
<h3>3. Better Context Retrieval</h3>
<p>As the database grows, explore:</p>
<pre><code class="language-plaintext">Metadata Retrieval
       ↓
Semantic Retrieval
       ↓
Hybrid Retrieval
</code></pre>
<p>to improve table and column selection.</p>
<blockquote>
<p><strong>The goal is to move from a working SQL AI Assistant to one that is reliable, measurable, and scalable.</strong></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[What I Learned Building a Multimodal RAG From Scratch]]></title><description><![CDATA[Introduction
Over the past few weeks, I built a multimodal Retrieval-Augmented Generation (RAG) system for a due diligence application that analyzes company documents such as annual reports, financial]]></description><link>https://experience101.hashnode.dev/multimodal-rag-learnings</link><guid isPermaLink="true">https://experience101.hashnode.dev/multimodal-rag-learnings</guid><dc:creator><![CDATA[Sarvesh Chandnani]]></dc:creator><pubDate>Mon, 27 Jul 2026 14:40:10 GMT</pubDate><content:encoded><![CDATA[<h2>Introduction</h2>
<p>Over the past few weeks, I built a multimodal Retrieval-Augmented Generation (RAG) system for a due diligence application that analyzes company documents such as annual reports, financial statements, and investor presentations.</p>
<p>Before adding visual understanding, I had already built a fairly mature text retrieval pipeline. The pipeline extracted structured text directly from PDFs, performed hierarchical chunking to preserve document context, indexed the chunks in Qdrant, and combined semantic vector search with BM25 using hybrid retrieval. For text-heavy questions, the system consistently returned relevant context and produced reliable answers.</p>
<p>The limitations only became apparent when the documents relied on visual information.</p>
<p>Annual reports and investor presentations are filled with financial tables, bar charts, pie charts, organizational diagrams, process flows, and complex page layouts. While these pages often contain text, simply retrieving the extracted text isn't always enough to understand the information being presented. The meaning frequently depends on the visual structure of the page.</p>
<p>This article isn't another "How to Build a Multimodal RAG" tutorial. Instead, it's a walkthrough of the engineering decisions, implementation challenges, architectural trade-offs, and lessons I learned while extending an already capable text RAG system into a multimodal one.</p>
<h2>When My Text RAG Reached Its Limits</h2>
<p>My text RAG pipeline was already performing well. With hierarchical chunking and hybrid retrieval, it could accurately answer questions such as <em>"Who is the CEO?"</em> or <em>"What are the company's plans for the next quarter?"</em> because the required information existed as text in the document.</p>
<p>The problem started when the answers depended on charts, graphs, or tables.</p>
<p>Questions like <em>"What was the Q2 traction?"</em> or <em>"What is the ARR projection till 2030?"</em> weren't answered reliably, even though the information existed in the PDF.</p>
<p>Initially, I thought the retrieval pipeline was failing. After inspecting the retrieved chunks, I realized the opposite—the hybrid search was retrieving the correct content.</p>
<p>The real issue was the document representation. The parser extracted the labels and numbers from charts, but the visual relationships between them were lost during text extraction and chunking. As a result, the LLM had the right data but not the context needed to interpret it correctly, leading to inaccurate answers.</p>
<p>That was when I realized that improving retrieval wasn't enough—I needed a way to preserve and retrieve the visual understanding of the document itself.</p>
<h3>Figure 2a - Page from the PDF</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/69773037-2d31-4c33-82dc-5628b148db42.png" alt="" style="display:block;margin:0 auto" />

<h3>Figure 2b - Wrong answer from text-only RAG.</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/13611e83-af05-4da6-b6bd-9fe657fb84ef.png" alt="" style="display:block;margin:0 auto" />

<h2>Finding the Right Approach</h2>
<p>Once I realized that my text pipeline wasn't enough, I started exploring the world of Multimodal RAG. That's when I encountered a flood of new concepts—<strong>captioning models</strong>, <strong>CLIP</strong>, <strong>unified embedding spaces</strong>, <strong>multivector retrieval</strong>, <strong>ColPali</strong>, and <strong>ColQwen2</strong>. Every article seemed to recommend a different approach, making the decision far from straightforward.</p>
<h3>Option 1: Captioning Every Page</h3>
<p>The first idea was to generate a caption for every PDF page and index those captions alongside the document text.</p>
<p>Although simple, I quickly ruled it out. Captioning models summarize what an image is about, but they don't preserve the detailed information inside it. A caption like <em>"A bar chart showing revenue growth"</em> is unlikely to help retrieve the page for a query like <em>"What is the ARR projection for 2028?"</em> The quality of retrieval would depend entirely on how descriptive the generated caption was.</p>
<h3>Option 2: CLIP</h3>
<p>Next, I looked at <strong>CLIP</strong>, which embeds both text and images into a shared vector space. It seemed promising because it allowed text queries to retrieve images directly.</p>
<p>However, my use case involved dense document pages filled with charts, tables, and small text—not natural images. More importantly, CLIP represents an entire page using a <strong>single embedding vector</strong>, which felt insufficient for capturing all the information present in a complex document page.</p>
<p>That was when I came across the idea of <strong>multivector embeddings</strong>. Instead of representing a page with one vector, it could be represented by hundreds of vectors, preserving much richer information. This eventually led me to <strong>ColPali</strong> and <strong>ColQwen2</strong>, which became the foundation of my visual retrieval pipeline.</p>
<h2>Building the Visual Pipeline</h2>
<p>Once I understood why multivector embeddings were a better fit for document retrieval, I designed a separate visual pipeline instead of modifying my existing text pipeline.</p>
<p>The pipeline starts by rendering every page of the PDF as an image. Each page image is then passed to <strong>ColQwen2</strong>, which generates a multivector embedding representing the visual content of that page. These embeddings are stored in a dedicated Qdrant collection, completely independent of the text embeddings.</p>
<p>Keeping the text and visual pipelines separate turned out to be an important architectural decision. The text pipeline continues to excel at retrieving textual information using hierarchical chunking and hybrid search, while the visual pipeline specializes in retrieving pages whose meaning depends on charts, tables, diagrams, and other visual elements.</p>
<p>At query time, both pipelines work together—the text pipeline retrieves relevant text chunks, while the visual pipeline retrieves the most relevant document pages. These results are then passed to a multimodal LLM, allowing it to reason over both textual and visual context before generating the final answer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/e305d964-8c5d-43cd-8aad-397d420bf312.png" alt="" style="display:block;margin:0 auto" />

<h2>Running the Visual Pipeline</h2>
<p>Designing the visual pipeline was only half the challenge. The next hurdle was running <strong>ColQwen2</strong>, a relatively large model that wasn't practical to run on my local machine. Instead of forcing everything into a single application, I decided to separate model inference from the main application.</p>
<p>The pipeline begins by using <strong>PyMuPDF (fitz)</strong> to render every page of the PDF into an image. These page images are then sent to a <strong>FastAPI</strong> server hosting <strong>ColQwen2</strong>, which generates multivector embeddings for each page.</p>
<p>Since I didn't have access to a dedicated GPU locally, I deployed the FastAPI server on <strong>Google Colab</strong> and exposed it to my local application using <strong>ngrok</strong>. This allowed my application to generate embeddings through simple HTTP requests, while all the heavy computation happened remotely.</p>
<p>The generated multivector embeddings are then stored in <strong>Qdrant</strong>, where they can later be retrieved using query embeddings generated by the same model.</p>
<p>Separating the embedding service from the main application turned out to be one of the best architectural decisions of the project. It kept the application lightweight, made development possible without local GPU resources, and allowed me to replace or upgrade the embedding model in the future with minimal changes to the rest of the system.</p>
<h3>Tech Stack</h3>
<ul>
<li><p><strong>PyMuPDF (fitz)</strong> – Render PDF pages as images</p>
</li>
<li><p><strong>ColQwen2</strong> – Generate multivector embeddings</p>
</li>
<li><p><strong>FastAPI</strong> – Expose the embedding model as an API</p>
</li>
<li><p><strong>Google Colab</strong> – Free GPU for model inference</p>
</li>
<li><p><strong>ngrok</strong> – Connect the local application to the Colab API</p>
</li>
<li><p><strong>Qdrant</strong> – Store and retrieve multivector embeddings</p>
</li>
<li><p><strong>OpenAI GPT-4o</strong> – Reason over retrieved text and page image</p>
</li>
</ul>
<h2>Retrieving Both Text and Visual Context</h2>
<p>Once the ingestion pipeline was complete, the retrieval process became surprisingly simple.</p>
<p>Whenever a user asks a question, the query is sent to both the <strong>text pipeline</strong> and the <strong>visual pipeline</strong> simultaneously.</p>
<p>The text pipeline performs hybrid retrieval over hierarchically chunked content and returns the most relevant text passages. In parallel, the visual pipeline generates a query embedding using <strong>ColQwen2</strong> and performs multivector search in Qdrant to retrieve the most relevant document pages.</p>
<p>These two sources of information are then combined and sent to <strong>GPT-4o</strong>.</p>
<p>This design turned out to be particularly effective because each pipeline specializes in what it understands best. The text pipeline excels at retrieving semantic information from paragraphs, while the visual pipeline retrieves pages whose meaning depends on charts, tables, diagrams, and layout.</p>
<p>An important design decision was that the visual pipeline does not return extracted text—it returns the original page image. This allows GPT-4o to reason directly over the visual content instead of relying on potentially incomplete text extraction.</p>
<p>For example, a query such as:</p>
<blockquote>
<p><em>"What is the ARR projection till 2030?"</em></p>
</blockquote>
<p>retrieves:</p>
<ul>
<li><p>Text chunks describing financial projections.</p>
</li>
<li><p>The original page containing the ARR chart.</p>
</li>
</ul>
<p>GPT-4o then combines both textual and visual context to generate the final answer.</p>
<h3>Final Answer</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a4812ff6fd9defcf3075e8a/6d2f3aa7-1b59-4512-b705-db15373efb74.png" alt="" style="display:block;margin:0 auto" />

<h2>Complete source Code</h2>
<p>If you're interested in building a similar Multimodal RAG system or experimenting with multivector retrieval, I've open-sourced the complete implementation used in this article.</p>
<h3>📂 GitHub Repository</h3>
<p>The complete source code, including both the text and visual pipelines, is available here:</p>
<p><strong>🔗 GitHub:</strong> <em>&lt;</em><a href="https://github.com/SarveshChandnani/Due_Diligence_Agent"><em>GitHub Repository Link</em></a><em>&gt;</em></p>
<h3>📓 Google Colab Notebook</h3>
<p>Since ColQwen2 requires a GPU, I also created a Google Colab notebook that hosts the model using FastAPI and exposes it through ngrok. You can use it directly or adapt it to your own setup.</p>
<p><strong>🔗 Google Colab:</strong> <em>&lt;</em><a href="https://colab.research.google.com/drive/1bp1M7cmjLG_kiCKRa1ZTns-bMlOlGEHe?usp=sharing"><em>Colab Link</em></a><em>&gt;</em></p>
<h2>Final Thoughts</h2>
<p>Building this project completely changed how I think about RAG systems.</p>
<p>Initially, I believed that retrieval quality was the bottleneck. In reality, the bigger challenge was <strong>how the document was represented</strong>. A retriever can only search over the information it has been given, and if visual context is lost during preprocessing, no amount of retrieval optimization can recover it.</p>
<p>This project also reinforced another lesson: <strong>there is rarely a single "correct" architecture</strong>. Exploring captioning models, CLIP, and multivector retrieval helped me understand the trade-offs of each approach before arriving at a solution that fit my use case.</p>
<p>The current system already combines hybrid text retrieval with visual page retrieval to answer questions grounded in both text and images. There is still plenty of room for improvement—better reranking, golden dataset evaluation metrics, and optimized indexing are all on my roadmap—but this implementation provides a solid foundation for building document-aware Multimodal RAG systems.</p>
<p>If you found this article useful, I'd love to hear your thoughts or discuss different approaches to Multimodal RAG. Feel free to connect with me or open an issue on GitHub.</p>
]]></content:encoded></item></channel></rss>