<?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: Adi CCH</title>
    <description>The latest articles on DEV Community by Adi CCH (@adi_cch).</description>
    <link>https://dev.to/adi_cch</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%2F4020235%2F69f884ff-7807-49b6-bdf3-cdd57f4ccf9d.png</url>
      <title>DEV Community: Adi CCH</title>
      <link>https://dev.to/adi_cch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adi_cch"/>
    <language>en</language>
    <item>
      <title>What I learned building a compliance answer engine that refuses to make things up</title>
      <dc:creator>Adi CCH</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:53:18 +0000</pubDate>
      <link>https://dev.to/adi_cch/what-i-learned-building-a-compliance-answer-engine-that-refuses-to-make-things-up-42mp</link>
      <guid>https://dev.to/adi_cch/what-i-learned-building-a-compliance-answer-engine-that-refuses-to-make-things-up-42mp</guid>
      <description>&lt;p&gt;Swiss business compliance is a genuinely hard information problem. There is no national minimum wage, VAT rules live at the federal level, social-insurance (AHV) admin runs through 26 cantonal offices, and most of the official guidance is only in German or French. If you are a foreign founder setting up a GmbH, you are stitching answers together from a dozen government PDFs.&lt;/p&gt;

&lt;p&gt;I have been building &lt;a href="https://cantoncompliancehub.ch" rel="noopener noreferrer"&gt;Canton Compliance Hub&lt;/a&gt;, a free multilingual resource that tries to make that legible. The interesting engineering problem was not the content, it was trust: in a domain where a wrong number can cost someone real money, the system has to be citable and it must not hallucinate. Here is what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-layer retrieval beats one
&lt;/h2&gt;

&lt;p&gt;The naive RAG setup is: chunk documents, embed them, retrieve top-k, stuff into the prompt. It works until a legal term like AHV or MWST needs an exact match that a dense vector glosses over.&lt;/p&gt;

&lt;p&gt;I run two layers on Postgres with pgvector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw chunks&lt;/strong&gt; are the ground truth. They keep the original wording and the source URL, so every answer can point back to a specific government page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact cards&lt;/strong&gt; are distilled atoms extracted from those chunks, embedded separately, and queried first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieval is hybrid: a BM25 pass catches the exact legal acronyms, a vector pass catches the paraphrases, and a metadata filter (canton, topic, language) runs before ranking so a Geneva question never pulls a Zurich-only rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make hallucination structurally hard
&lt;/h2&gt;

&lt;p&gt;"Please don't hallucinate" in a system prompt is not a control. What worked was making every factual claim carry a citation marker back to a chunk, then treating any claim without one as a defect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generation emits inline citation markers. A claim with no marker gets flagged.&lt;/li&gt;
&lt;li&gt;A cite whose id is not in the retrieved set is a hallucinated citation, and the draft is rejected and retried at temperature 0.&lt;/li&gt;
&lt;li&gt;For the numbers (rates, thresholds, CHF amounts), a separate verification pass marks each numeric claim supported, unsupported, or contradicting against the retrieved evidence only. Unsupported numbers block publication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last pass was the highest-leverage thing we added. Cheap draft models are confident and wrong exactly where it matters most in compliance: a plausible-looking rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change detection is code, not AI
&lt;/h2&gt;

&lt;p&gt;The corpus has to stay current, but re-summarising an unchanged page with an LLM is waste. Change detection is four boring signals: Last-Modified, ETag, an on-page timestamp, and a SHA-256 of the extracted text. The LLM only runs when the content genuinely changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the primitives as tools
&lt;/h2&gt;

&lt;p&gt;Two byproducts became reusable public artifacts, grounded in official sources and published as open datasets (CC BY 4.0): a minimum-wage-by-canton comparison, and an SME &lt;a href="https://cantoncompliancehub.ch/resources/compliance-calendar" rel="noopener noreferrer"&gt;compliance-deadline calendar&lt;/a&gt;. The whole retrieval stack sits behind a free &lt;a href="https://cantoncompliancehub.ch/audit" rel="noopener noreferrer"&gt;compliance self-check&lt;/a&gt;: describe your situation in plain language and get a personalised, source-cited overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Two-layer retrieval (atoms first, raw chunks as citation fallback) is worth the extra table.&lt;/li&gt;
&lt;li&gt;Treat a missing or unknown citation as a build error, not a style preference.&lt;/li&gt;
&lt;li&gt;Verify numbers as a separate pass. The draft model will keep inventing them; a checker against your own corpus is the fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work with Swiss compliance content and want to compare notes, the site is &lt;a href="https://cantoncompliancehub.ch" rel="noopener noreferrer"&gt;cantoncompliancehub.ch&lt;/a&gt;. Happy to answer questions in the comments.&lt;/p&gt;

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