<?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: Varshith Reddy</title>
    <description>The latest articles on DEV Community by Varshith Reddy (@varshithreddyaileni).</description>
    <link>https://dev.to/varshithreddyaileni</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077935%2F452924c7-7d2c-4f85-8023-0e587ff8c366.png</url>
      <title>DEV Community: Varshith Reddy</title>
      <link>https://dev.to/varshithreddyaileni</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/varshithreddyaileni"/>
    <language>en</language>
    <item>
      <title>When Your AI Confidently Replies to Emails It Shouldn't Touch</title>
      <dc:creator>Varshith Reddy</dc:creator>
      <pubDate>Sat, 15 Aug 2026 04:01:33 +0000</pubDate>
      <link>https://dev.to/varshithreddyaileni/when-your-ai-confidently-replies-to-emails-it-shouldnt-touch-1p00</link>
      <guid>https://dev.to/varshithreddyaileni/when-your-ai-confidently-replies-to-emails-it-shouldnt-touch-1p00</guid>
      <description>&lt;p&gt;&lt;em&gt;A technical investigation into a RAG system that can't tell when it's out of its depth&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;InboxSync is a personal project I built: a multi-account email aggregation API that uses a RAG (Retrieval-Augmented Generation) pipeline to suggest replies. The system indexes emails via IMAP, categorizes them with GPT-4o-mini, and for actionable emails retrieves semantically similar training examples from a pgvector database to generate contextually grounded reply suggestions.&lt;/p&gt;

&lt;p&gt;The stack: Node.js / TypeScript backend, PostgreSQL with the pgvector extension for vector similarity search, OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt; for embeddings, and &lt;code&gt;gpt-4o-mini&lt;/code&gt; for generation. The training corpus contains three examples covering: job interview scheduling, product demos, and partnership proposals.&lt;/p&gt;

&lt;p&gt;The system is built for B2B outreach helping salespeople respond to inbound leads faster. That framing matters for what follows.&lt;/p&gt;

&lt;p&gt;I built the &lt;code&gt;confidence&lt;/code&gt; field myself, months ago, with a vague plan to use it later to decide which replies were safe to auto-send versus flag for review. I never got around to wiring that logic in. Going back to finally do it, I pulled up a handful of real responses to sanity-check the values first and every single one said &lt;code&gt;0.85&lt;/code&gt;. That was the moment this stopped being a feature I hadn't finished and became a question I actually wanted to answer: what was that number even measuring?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Failure
&lt;/h2&gt;

&lt;p&gt;I ran five adversarial test queries against the live system. Every single one returned &lt;code&gt;"confidence": 0.85&lt;/code&gt;. Every single one got a fluent, polished reply. Here they are, with what correct behavior actually looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1 — Spam&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query:  "URGENT! Get 50% OFF on all products! Limited time! Click here NOW!"
Output: "Thank you for the exciting offer! I appreciate the heads-up about
         the Black Friday sale. I'll definitely take a look."
Correct: No reply. Replying to spam is harmful.
Confidence returned: 0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 2 — Out of Office auto-reply&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query:  "I am out of office until November 20th. I will respond upon my return."
Output: "Thank you for the update! I will follow up after November 20th.
         Enjoy your time away!"
Correct: No reply. Auto-reply to auto-reply = infinite loop in production.
Confidence returned: 0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 3 — Not Interested&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query:  "Thanks for reaching out. We are currently using a competitor and happy with it."
Output: "Thank you for your response! If circumstances change or if you ever
         want to explore options in the future, please don't hesitate to reach out."
Correct: The system has no concept "Not Interested" is a terminal state.
         The generated reply is precisely the pushy follow-up that gets salespeople blocked.
Confidence returned: 0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 4 — Out-of-domain (GDPR legal request)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query:  "Under Article 17 of the GDPR, I formally request the deletion of all
         personal data you hold about me. Please confirm within 30 days."
Output: "Dear [Recipient's Name], Thank you for your request. We take GDPR
         compliance seriously and will process your request promptly. You can expect
         confirmation of the data deletion within the next 30 days. Best regards,
         [Your Name] [Your Position] [Your Company]"
Correct: Refuse and escalate to a human. The system has zero training data
         about legal compliance — yet produced authoritative-sounding legal
         commitments with unfilled template placeholders. If sent, this is a
         binding legal acknowledgment of a data deletion obligation.
Confidence returned: 0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 5 — Multi-hop (two topics, one query)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query:  "I have a technical interview scheduled but also wanted to ask about
         your product pricing for a team of 50 before we proceed."
Output: "Thank you for your email! I look forward to the technical interview.
         Regarding pricing for a team of 50, I would be happy to provide that
         information." [no pricing information provided]
Correct: The system retrieved the closest single training example and ignored
         the other topic. It promised information it did not deliver.
Confidence returned: 0.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Diagnosis
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;0.85&lt;/code&gt; is not a computed value. It is a literal constant hardcoded in &lt;a href="https://github.com/varshithreddy7/InboxSync/blob/main/src/services/rag.service.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/services/rag.service.ts&lt;/code&gt;&lt;/a&gt;:&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;// rag.service.ts line 136&lt;/span&gt;
&lt;span class="nc"&gt;VALUES &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailId&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;suggestedReply&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nc"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// line 143&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;suggestedReply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system &lt;em&gt;does&lt;/em&gt; compute real similarity scores pgvector's cosine distance operator (&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;) runs correctly and returns accurate distances. I ran a diagnostic script to surface what those scores actually were for each test case (cosine similarity, 0–1 scale):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Best match&lt;/th&gt;
&lt;th&gt;Real similarity&lt;/th&gt;
&lt;th&gt;Reported confidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spam ("50% OFF")&lt;/td&gt;
&lt;td&gt;Product Demo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of Office&lt;/td&gt;
&lt;td&gt;Job Interview&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.27&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not Interested&lt;/td&gt;
&lt;td&gt;Partnership Proposal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.42&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR legal request&lt;/td&gt;
&lt;td&gt;Product Demo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop (interview+pricing)&lt;/td&gt;
&lt;td&gt;Job Interview&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.54&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those real scores are computed, then &lt;strong&gt;silently discarded&lt;/strong&gt; before the response is returned. The caller receives &lt;code&gt;0.85&lt;/code&gt; regardless of whether the retrieved training data is relevant, partially relevant, or entirely unrelated. The GDPR query where the system had essentially zero contextual grounding got the same confidence value as the multi-hop query, which had its best retrieval of the set.&lt;/p&gt;

&lt;p&gt;The second structural problem: &lt;strong&gt;there is no retrieval gate.&lt;/strong&gt; The system's branching logic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;retrieved_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fallback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;                            &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="nf"&gt;reply &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pgvector always returns rows it returns the &lt;em&gt;nearest&lt;/em&gt; neighbors regardless of actual distance. The low-confidence fallback path is effectively unreachable. Every query with a non-empty training corpus produces &lt;code&gt;confidence: 0.85&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause: Two Gaps, One Symptom
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gap 1 — No relevance threshold.&lt;/strong&gt; The retrieval step correctly computes distances but never checks them against a minimum before proceeding. "Has neighbors" and "has &lt;em&gt;relevant&lt;/em&gt; neighbors" are treated as equivalent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 2 — Confidence as a constant.&lt;/strong&gt; The &lt;code&gt;confidence&lt;/code&gt; field exists to let downstream callers decide whether to auto-send or flag for human review. Instead it's a decoration with a fixed value. Any business deploying this with an auto-send rule above &lt;code&gt;0.80&lt;/code&gt; will auto-send replies to spam, OOF messages, legal demands, and explicit rejections.&lt;/p&gt;

&lt;p&gt;Both gaps have engineering fixes: add a threshold check, replace the constant with the actual max similarity score. That's a few hours of work, and I've spec'd it out. But those fixes raise the question that's actually hard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Isn't Just a Bug
&lt;/h2&gt;

&lt;p&gt;From the outside, all five test cases look identical: &lt;code&gt;{ "success": true, "confidence": 0.85 }&lt;/code&gt;. A developer building a UI or automation on top of this API has no way to distinguish the product demo reply from the GDPR legal commitment.&lt;/p&gt;

&lt;p&gt;This is the shape of a deeper problem that keeps appearing in deployed AI systems: the output looks trustworthy regardless of whether the underlying computation actually was. A number that exists specifically to tell you when to trust the system turns out not to track that at all.&lt;/p&gt;

&lt;p&gt;The question I don't know how to answer and think is genuinely open is: &lt;strong&gt;what would a reliable uncertainty signal actually look like here?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retrieval similarity is a proxy, but even a perfect similarity score doesn't capture all the ways a reply can be wrong: training data could be outdated; the model could hallucinate specifics not in retrieved context; the semantically closest example could be contraindicated by the email's intent (a "Not Interested" reply may embed close to a "Product Demo" example because both use the word "product"). Accuracy on the training distribution doesn't generalize to knowing what the system doesn't know at test time.&lt;/p&gt;

&lt;p&gt;RAG improves on a pure LLM by grounding generation in retrieved context. It doesn't solve the meta-problem of knowing when retrieval was good enough. The confidence field was added under the assumption someone would solve that later. Nobody did.&lt;/p&gt;

&lt;p&gt;That gap between "we have a quality signal" and "the quality signal measures quality" is what I'd want to study.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix And What It Still Doesn't Solve
&lt;/h2&gt;

&lt;p&gt;I implemented both architectural corrections in &lt;a href="https://github.com/varshithreddy7/InboxSync/blob/main/src/services/rag.service.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/services/rag.service.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Real confidence&lt;/strong&gt; — replaced the hardcoded &lt;code&gt;${0.85}&lt;/code&gt; with &lt;code&gt;${maxSimilarity}&lt;/code&gt;, where &lt;code&gt;maxSimilarity&lt;/code&gt; is the actual top cosine similarity score from the pgvector retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relevance gate&lt;/strong&gt; — added &lt;code&gt;RELEVANCE_THRESHOLD = 0.35&lt;/code&gt;: if the best retrieved example scores below this, the system returns &lt;code&gt;{ suggestedReply: null, confidence: &amp;lt;real_score&amp;gt;, refused: true, reason: "..." }&lt;/code&gt; instead of generating a reply.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same five queries, before and after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Before (broken)&lt;/th&gt;
&lt;th&gt;After (fixed)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spam&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "Thank you for the offer..." confidence: 0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: null, confidence: 0.244, refused: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of Office&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "Enjoy your time away!" confidence: 0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: null, confidence: 0.265, refused: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not Interested&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "...don't hesitate to reach out" confidence: 0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "...I completely understand..." confidence: 0.423&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR legal request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "We take GDPR compliance seriously..." confidence: 0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: null, confidence: 0.224, refused: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "I look forward to the interview..." confidence: 0.85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reply: "...I can send a detailed proposal..." confidence: 0.533&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three cases that should never have generated replies now correctly refuse. But the Not Interested case (similarity 0.42) still generates which surfaces the boundary the engineering fix actually hits: the threshold cannot distinguish "semantically similar but contextually wrong" from "semantically similar and appropriate." The "Not Interested" email embeds close to "Partnership Proposal" because both involve business relationships but the intent is opposite. A cosine score can't see that.&lt;/p&gt;

&lt;p&gt;The cases where embeddings mislead, where the correct response depends on intent rather than surface form those are exactly the cases where the confidence signal most needs to be reliable, and where it's hardest to compute correctly.&lt;/p&gt;

&lt;p&gt;That's where I've had to stop and just sit with the problem rather than patch it. A tighter threshold doesn't fix it it just trades false approvals for false refusals, since intent and topic overlap in the same embedding space. What would actually distinguish them? Maybe a second pass where an LLM explicitly judges intent-match rather than relying on distance alone. Maybe better negative examples in training data, so "similar topic, opposite intent" has something to be measured against. Maybe the honest answer is that no single scalar confidence value can carry this much information, and the field itself is the wrong abstraction. I don't have a settled view yet I'm still turning it over, and I'd rather say that plainly than pretend the threshold I shipped actually closes the gap.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All outputs reproduced live against a running InboxSync instance (PostgreSQL + pgvector + GPT-4o-mini). Fix implemented and verified. Source: &lt;a href="https://github.com/varshithreddy7/InboxSync" rel="noopener noreferrer"&gt;github.com/varshithreddy7/InboxSync&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
