<?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: Yasantha Hettiarachchi</title>
    <description>The latest articles on DEV Community by Yasantha Hettiarachchi (@yasantha_hettiarachchi_72).</description>
    <link>https://dev.to/yasantha_hettiarachchi_72</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%2F4011680%2F0c4dab0a-8bc9-4503-b970-c370fc193e47.jpg</url>
      <title>DEV Community: Yasantha Hettiarachchi</title>
      <link>https://dev.to/yasantha_hettiarachchi_72</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yasantha_hettiarachchi_72"/>
    <language>en</language>
    <item>
      <title>No citation, no claim: building a RAG backend that refuses to hallucinate"</title>
      <dc:creator>Yasantha Hettiarachchi</dc:creator>
      <pubDate>Sat, 12 Sep 2026 13:13:25 +0000</pubDate>
      <link>https://dev.to/yasantha_hettiarachchi_72/no-citation-no-claim-building-a-rag-backend-that-refuses-to-hallucinate-1klp</link>
      <guid>https://dev.to/yasantha_hettiarachchi_72/no-citation-no-claim-building-a-rag-backend-that-refuses-to-hallucinate-1klp</guid>
      <description>&lt;p&gt;A few weeks ago, an AI assistant gave me a confident, well-written answer about one of my own PDFs. It was completely wrong. Nothing in the document said what the model claimed — it had simply filled the gap with something plausible.&lt;/p&gt;

&lt;p&gt;That's the quiet failure mode of retrieval-augmented generation (RAG) done casually. It &lt;em&gt;looks&lt;/em&gt; grounded because you've stuffed some document chunks into the prompt. But nothing actually forces the model to use them. For a demo, that's fine. For anything you'd put in front of a real user, it isn't.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;DocuQuery&lt;/strong&gt; — a RAG backend with a single rule baked into its core: &lt;strong&gt;no citation, no claim.&lt;/strong&gt; Every answer has to point back to the specific part of the source document it came from, or the system says it doesn't know. It's open source (&lt;a href="https://github.com/yasantha/DocuQuery" rel="noopener noreferrer"&gt;github.com/yasantha/DocuQuery&lt;/a&gt;), and here's how it works and the decisions that actually mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;## The core idea&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pipeline is the standard RAG shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ingest &amp;amp; chunk → embed → store in pgvector → retrieve top-k → generate WITH citations → verify grounding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part most tutorials skip is the last two steps. Retrieving relevant chunks is easy. Making the model speak &lt;em&gt;only&lt;/em&gt; from them — and proving that it did — is the actual engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grounding: no citation, no claim
&lt;/h2&gt;

&lt;p&gt;Two things enforce it.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;before generation&lt;/strong&gt;: if the top retrieved chunks fall below a similarity threshold, the system short-circuits and returns "not found" rather than sending weak context to the model. A surprising amount of hallucination is really a retrieval problem in disguise — the search quietly came back with nothing useful, and the model papered over the gap.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;at generation&lt;/strong&gt;: the model is instructed to answer strictly from the supplied chunks and to return the specific chunk IDs it used — not just prose. Every returned ID is then validated against the retrieved set, so the model can't cite something that wasn't actually there. If it can't support the answer from the chunks, it's instructed to say so, which maps to a "not found" response.&lt;/p&gt;

&lt;p&gt;The response isn't just text — it carries its receipts:&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;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The renewal notice period is 60 days before expiry."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"citations"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vendor-agreement.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.89&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;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"embedding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"generation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0021&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;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;If a user can't see &lt;em&gt;where&lt;/em&gt; an answer came from, they can't trust it. Making citations a first-class field — not an afterthought in the prose — changes how much you can rely on the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL + pgvector
&lt;/h2&gt;

&lt;p&gt;I deliberately didn't reach for a dedicated vector database. For a huge class of applications, &lt;strong&gt;pgvector inside PostgreSQL&lt;/strong&gt; is more than enough — and it's one less piece of infrastructure to run, secure, and back up. Embeddings live in the same database as everything else, with an IVFFlat index for fast nearest-neighbour search. If you already run Postgres, you already have a vector store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud or fully local — your choice
&lt;/h2&gt;

&lt;p&gt;Vendor lock-in at the model layer is a real risk, so provider choice is a config switch, not a rewrite. DocuQuery runs embeddings and generation through &lt;strong&gt;Claude, Gemini, or Ollama&lt;/strong&gt; (fully local). The same code path gives you a "best quality" cloud preset and a "nothing leaves this machine" local preset. For anyone working with sensitive documents, that local option matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost, as a first-class concern
&lt;/h2&gt;

&lt;p&gt;RAG gets expensive quietly — every query is an embedding call plus a generation call, and it adds up fast. Two things help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic caching:&lt;/strong&gt; if a new question is close enough to one already answered, serve the cached result instead of paying for it again. Each cache entry records which document it was built from, so when a document is re-ingested, the entries tied to it are evicted — no stale answers surviving an update (with a TTL as a backstop).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cost dashboard:&lt;/strong&gt; every response records what it cost and whether it was cache-served, so I can see the cloud-vs-local split and actual savings rather than guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You optimise what you measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing against a real vector store
&lt;/h2&gt;

&lt;p&gt;RAG has a lot of moving parts, so the tests spin up a &lt;strong&gt;real pgvector-enabled PostgreSQL via Testcontainers&lt;/strong&gt; — not a mock. Retrieval and grounding get validated against actual vector search, which is the only reliable way to catch the subtle bugs (wrong distance metric, off-by-one chunking) that fake-backed unit tests sail straight past.&lt;/p&gt;

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

&lt;p&gt;Java 25 · Spring Boot 3.5 · PostgreSQL + pgvector · Claude / Gemini / Ollama (pluggable) · React + Vite · JUnit 5 + Testcontainers · Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone starting a RAG project
&lt;/h2&gt;

&lt;p&gt;The interesting engineering isn't the LLM call — that's a few lines now. It's everything &lt;em&gt;around&lt;/em&gt; it: keeping answers honest, making retrieval good enough that the right passages actually surface, controlling cost, and making the whole thing testable. Get those right, and RAG stops being a demo and starts being something you'd ship.&lt;/p&gt;

&lt;p&gt;The code is open source — have a look, break it, tell me what you'd do differently:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/yasantha/DocuQuery" rel="noopener noreferrer"&gt;github.com/yasantha/DocuQuery&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely value hearing how others are handling two things in particular: &lt;strong&gt;citation quality&lt;/strong&gt; and &lt;strong&gt;cost control&lt;/strong&gt; in production RAG. How are you approaching grounding in your own builds? Let me know in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>rag</category>
      <category>springboot</category>
    </item>
  </channel>
</rss>
