<?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: Muhammad Ahmad Nazir Ahmad</title>
    <description>The latest articles on DEV Community by Muhammad Ahmad Nazir Ahmad (@emadaed).</description>
    <link>https://dev.to/emadaed</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%2F4190919%2F68fffdc5-c626-4710-8c63-c9130b1335f3.jpg</url>
      <title>DEV Community: Muhammad Ahmad Nazir Ahmad</title>
      <link>https://dev.to/emadaed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emadaed"/>
    <language>en</language>
    <item>
      <title>Building Reliable AI Agents: Why LLMs Can't Verify Their Own Output (And What to Do Instead)</title>
      <dc:creator>Muhammad Ahmad Nazir Ahmad</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:10:18 +0000</pubDate>
      <link>https://dev.to/emadaed/building-reliable-ai-agents-why-llms-cant-verify-their-own-output-and-what-to-do-instead-2537</link>
      <guid>https://dev.to/emadaed/building-reliable-ai-agents-why-llms-cant-verify-their-own-output-and-what-to-do-instead-2537</guid>
      <description>&lt;p&gt;If you are building an AI agent that connects to a database, queries a knowledge base, or cites documents, you are likely dealing with a fundamental misconception: You cannot prompt-engineer an LLM into being perfectly factual.&lt;/p&gt;

&lt;p&gt;In a recent industry survey of 800+ decision-makers, 40% cited security and compliance as the #1 barrier to scaling agentic AI. 46% specifically worry about indirect prompt injection and tool poisoning. Most teams are operating in what the report calls "leap-of-faith mode"—trusting the LLM to faithfully report what it found in a database.&lt;/p&gt;

&lt;p&gt;I know this trap intimately. I built a local AI agent using LangGraph, Ollama, and ChromaDB connected to a live PostgreSQL ERP database. During adversarial testing, it confidently invented two fake item IDs with fake stock levels, directly contradicting the empty database result it had just received.&lt;/p&gt;

&lt;p&gt;This is the story of how I stopped trusting the LLM to verify its own output, and built a three-layer architecture using deterministic guardrails, exact-match verification, and scope-routing to make the agent safe enough for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture:&lt;/strong&gt; Prompt Establishes Intent, Code Enforces It&lt;br&gt;
My first instinct was to write a massive system prompt: "Never fabricate data," "Always ground your answer in retrieved context."&lt;/p&gt;

&lt;p&gt;It worked 90% of the time. But 90% is a failing grade in enterprise software. I had to rip the safety out of the prompt and move it into the graph state. The architecture now looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.The Prompt:&lt;/strong&gt; Establishes the persona and the intent to be helpful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.The Tool:&lt;/strong&gt; Executes the database query or vector search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.The Backstop (Deterministic Code):&lt;/strong&gt; Reads the raw tool output from the graph state and overrides the LLM's prose if a known failure shape appears.&lt;/p&gt;
&lt;h1&gt;
  
  
  1.The Deterministic Backstop Node
&lt;/h1&gt;

&lt;p&gt;I did not use a separate validation LLM (which just adds latency and another point of failure). I used a deterministic Python wrapper that checks the state before the final response is generated.&lt;/p&gt;

&lt;p&gt;If the database tool returns an empty array or a hard error, the code intercepts the response and replaces the LLM's output entirely:&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;apply_tool_result_guards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Deterministic backstop: if the last DB query came back empty or errored,
    do not trust the model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s prose about what it found.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;last_tool_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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="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;content&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_is_no_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_tool_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_is_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_tool_message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No matching data was found for that query. The data may not &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exist in the database yet, or try different filters.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; The LLM is simply not allowed to write the final sentence about what it found if the data came back empty.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. The Whitelist over the Correctness Checker
&lt;/h1&gt;

&lt;p&gt;When asking the agent for Islamic citations, it once cited verse 5:51 (which is about interfaith alliances) for a question about usury (riba). My original regex only caught Surah X:Y formatting, so it missed (Quran 5:51).&lt;/p&gt;

&lt;p&gt;Instead of asking the LLM to check if the citation is correct (a probabilistic, hard problem), I built a whitelist check (a deterministic, easy problem).&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;_ALLOWED_CITATIONS&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;2:275&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;2:282&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# Pre-verified list
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enforce_citation_guard&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;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_CITATION_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&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;for&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_ALLOWED_CITATIONS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# Block even if the citation is real, if it's not on the whitelist.
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SAFE_FALLBACK_MESSAGE&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model guesses a real verse that isn't on the whitelist, it gets blocked. Boring and reliable beats smart and hallucinatory.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Never Let the Model Do Entity-Matching
&lt;/h1&gt;

&lt;p&gt;During testing, the agent correctly identified the lowest-scoring supplier from a sorted SQL query, but attached that value to the wrong row's identity. Two suppliers happened to share a display name, and the model mixed the fields.&lt;/p&gt;

&lt;p&gt;The fix? Don't hand the LLM raw JSON and hope it reads it correctly. Modify the tool to state the answer explicitly:&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;# Bad: Return raw JSON
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Good: Force the tool to state the entity match explicitly
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Row 1, id=4 (Supplier: Acme Corp) has the lowest value: $4,500. Do not combine this with fields from a different row.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Layer 2: The Abstration Problem (Why 4 Local LLMs Failed)
&lt;/h1&gt;

&lt;p&gt;When scaling to a 196-question benchmark, I needed the agent to refuse to answer questions that were out of scope (e.g., asking for a specific ruling that isn't in the text, or asking about modern cryptocurrencies).&lt;/p&gt;

&lt;p&gt;I tried to use a local 7B LLM as a relevance judge. The prompt was simple: "Given this question and these 5 retrieved texts, decide if the text answers the question. If not, abstain."&lt;/p&gt;

&lt;p&gt;I tested command-r7b-arabic, llama3.1, llama3.2:3b, and phi3. I ran 24 draws.&lt;/p&gt;

&lt;p&gt;The result: 0 correct abstentions on out-of-corpus questions.&lt;/p&gt;

&lt;p&gt;When asked "What is the ruling on horse meat?" (a topic that has nearby food prohibitions in the text, but no direct ruling), the models didn't say "I don't know." They grabbed the most topically-adjacent verse and confidently presented it as the answer.&lt;/p&gt;

&lt;p&gt;This is the capability floor. LLMs are great at "Topic Matching." They are terrible at "Relevance Judgment."&lt;/p&gt;

&lt;h1&gt;
  
  
  The Fix: Two-Layer Scope Routing
&lt;/h1&gt;

&lt;p&gt;Instead of asking the LLM to judge relevance, I split the task:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: LLM Intent Classifier.&lt;/strong&gt; I asked the model to classify the user's question into a category (e.g., "Is this asking for a ruling?"). LLMs are excellent at classification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Regex Backstop.&lt;/strong&gt; A deterministic keyword regex catches rule-start phrases (like "What is the ruling on..." or "How many...") to backstop the LLM.&lt;/p&gt;

&lt;p&gt;When the gate fires, the user gets a scope message explaining that the text doesn't provide a ruling, and offers to show adjacent verses instead. It catches 8/10 out-of-scope questions with 0/4 false positives on legitimate questions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Layer 3: The Evaluation Metric was Lying (The Reranker Bug)
&lt;/h1&gt;

&lt;p&gt;For months, my lexical search bucket was stuck at ~19% recall. I blamed the reranker. I built an evaluation harness to test pre-rerank vs. post-rerank scores.&lt;/p&gt;

&lt;p&gt;The measurement showed the opposite: &lt;strong&gt;The reranker was helping the Arabic path, but destroying the Roman-Urdu path by 11.7 points.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because I was asking the reranker to score (Roman-Urdu query, Arabic verse) pairs—a pairing it was never trained on. The model was scoring in a near-uniform noise band (1e-5).&lt;/p&gt;

&lt;p&gt;The fix was to remove the reranker from the Roman-Urdu path entirely. Roman-Urdu R@5 jumped from 7.1% to 17.5% overnight.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Final Lesson: Checkpoint Your Eval Pipeline
&lt;/h1&gt;

&lt;p&gt;During this, I ran a 100-minute evaluation that crashed in the final report-generation stage because of a two-character format string error ({"n":&amp;gt;4d} instead of s). Because nothing was written to disk, I lost the entire 100 minutes of compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always checkpoint your eval results to disk (pickle, JSON, whatever) immediately after the compute finishes, before you generate reports.&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;# Always checkpoint expensive compute
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rerank_checkpoint.pkl&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;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank_dicts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Conclusion: How to build a reliable agent in 2026
&lt;/h1&gt;

&lt;p&gt;A System Prompt is a Leash, not a Lobotomy. It establishes intent, but it cannot enforce truth.&lt;/p&gt;

&lt;p&gt;Deterministic Backstops are Mandatory. If a tool returns an empty array or an error, override the LLM's prose entirely.&lt;/p&gt;

&lt;p&gt;Use a Whitelist, not a Correctness Checker. Verifying if a citation is accurate is hard. Checking if it is in a pre-approved list is easy.&lt;/p&gt;

&lt;p&gt;Don't use LLMs for Entity-Matching. If you can do the math or the sorting in Python, do it. Hand the model the answer explicitly.&lt;/p&gt;

&lt;p&gt;Test the Test. I once had a weak assertion that passed a hallucinated quote as green because my regex only checked for the word "Hadith" nearby. A weak test is as dangerous as no test.&lt;/p&gt;

&lt;p&gt;Building agentic AI isn't about finding the smartest model. It's about building the most resilient architecture around an inherently probabilistic system.&lt;/p&gt;

&lt;p&gt;If you want to read the full 7-part engineering logs detailing these bugs, you can find them on my portfolio  &lt;a href="https://emadaed.github.io/digisuite/" rel="noopener noreferrer"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By Muhammad Ahmad | AI Agent Engineer | PMP®, RMP®&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>reliableaiagents</category>
      <category>entitymatching</category>
    </item>
    <item>
      <title>Finding the Bugs I Built an AI Agent to Prevent: Building Deterministic Guardrails with LangGraph &amp; Python</title>
      <dc:creator>Muhammad Ahmad Nazir Ahmad</dc:creator>
      <pubDate>Sat, 17 Oct 2026 18:28:21 +0000</pubDate>
      <link>https://dev.to/emadaed/finding-the-bugs-i-built-an-ai-agent-to-prevent-building-deterministic-guardrails-with-langgraph--agf</link>
      <guid>https://dev.to/emadaed/finding-the-bugs-i-built-an-ai-agent-to-prevent-building-deterministic-guardrails-with-langgraph--agf</guid>
      <description>&lt;p&gt;``&lt;br&gt;
A real debugging log: fabrication bugs, deployment gaps, and false test passes found and closed across 6 rounds of adversarial testing.&lt;/p&gt;

&lt;p&gt;System prompts are a leash, not a lobotomy. A system prompt can be ignored, misread, or drift under pressure. Deterministic code cannot.&lt;/p&gt;

&lt;p&gt;When I built Ahmad-AI — a local AI agent using LangGraph, Python, and Ollama with access to live SQL databases and Islamic knowledge bases — I established one non-negotiable design rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It must never state a specific citation or data claim unless that claim is independently verified. Verified, or it says nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because general-purpose LLMs fabricate citations confidently and often, "confidently wrong" is worse than "honestly uncertain." I spent weeks trying to prove the system would break its own rule anyway.&lt;/p&gt;

&lt;p&gt;It did. Repeatedly. Here is what I learned across six rounds of adversarial testing against a live agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 1: The First Adversarial Pass
&lt;/h3&gt;

&lt;p&gt;I ran a set of test questions against the live agent — empty database queries, Quran &amp;amp; Hadith citation requests, and ambiguous sorting queries.&lt;/p&gt;

&lt;p&gt;Two critical bugs immediately surfaced:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ghost Row Bug:
&lt;/h3&gt;

&lt;p&gt;When querying a table with no matching rows, the agent correctly output: "The query did not return any results." In the exact same sentence, it invented two fake item IDs with fake stock levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Regex Mismatch:
&lt;/h3&gt;

&lt;p&gt;When asked about usury (riba), the agent cited verse 5:51 (a real Quran verse, but entirely about interfaith alliances). My citation guard failed to fire because its regex expected Surah [name] X:Y, while the model wrote (Quran X:Y). A safety mechanism that doesn't match the shape of the output is functionally non-existent.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix Philosophy: Code, Not More Instructions
&lt;/h3&gt;

&lt;p&gt;My first instinct was to strengthen the system prompt. I resisted. Instead of writing tighter rules for the LLM, I built a deterministic backstop in Python that overrides the model’s prose when known failure patterns appear in state:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;python&lt;br&gt;
python:&lt;br&gt;
def apply_tool_result_guards(state, response):&lt;br&gt;
    """&lt;br&gt;
    Deterministic backstop: if the last DB query came back empty or hard-errored, &lt;br&gt;
    override entirely with a fixed, honest message regardless of what the model wrote.&lt;br&gt;
    """&lt;br&gt;
    if is_no_data(last_tool_message):&lt;br&gt;
        return "No matching data was found for that query. The data may not exist yet."&lt;/p&gt;

&lt;p&gt;ALLOWED_CITATIONS = {"20:114"}  # Pre-verified whitelist&lt;/p&gt;

&lt;p&gt;def enforce_citation_guard(text):&lt;br&gt;
    for match in CITATION_PATTERN.finditer(text):&lt;br&gt;
        if not any(allowed in match.group(0) for allowed in ALLOWED_CITATIONS):&lt;br&gt;
            return SAFE_ISLAMIC_FALLBACK  # Blocked even if the citation is real&lt;br&gt;
    return text&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h3&gt;
  
  
  Building an Evaluation Harness That Doesn't Lie
&lt;/h3&gt;

&lt;p&gt;Manually retyping test prompts in a terminal doesn't scale. To keep the agent honest, I split automated testing into two distinct layers:&lt;/p&gt;

&lt;p&gt;Unit Tests (22 tests): Direct execution of Python guard functions against synthetic inputs (fast, no live LLM required).&lt;/p&gt;

&lt;p&gt;End-to-End Evaluation Harness: Drives the live LangGraph execution against Ollama and Postgres.&lt;/p&gt;

&lt;p&gt;Treating fuzzy model outputs as binary pass/fail either hides real regressions behind noisy false failures or downgrades hard security checks to "close enough."&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways for AI Engineers
&lt;/h3&gt;

&lt;p&gt;A whitelist beats a correctness checker: Verifying "is this citation accurate?" is hard. Checking "is this citation on the pre-approved whitelist?" is simple and reliable.&lt;/p&gt;

&lt;p&gt;Print a Build Stamp at Startup: A version stamp printed at agent startup costs nothing and prevents testing against stale code.&lt;/p&gt;

&lt;p&gt;Distinguish Transient from Permanent Errors: Retrying a connection drop makes sense. Retrying a bad SQL column name is just a slower way to fail.&lt;/p&gt;

&lt;p&gt;Don't ask LLMs to do arithmetic or row matching: Stating query answers explicitly in tool outputs prevents models from misreading JSON rows.&lt;/p&gt;

&lt;p&gt;Read the Full Engineering Log&lt;br&gt;
This architecture is live, tested against a 22-unit-test suite and a 5/5 E2E hard check harness.&lt;/p&gt;

&lt;p&gt;For the complete technical breakdown — including deployment gap fixes, bounded execution limits (MAX_TOOL_ROUNDS), and verbatim adversarial outputs:&lt;/p&gt;

&lt;p&gt;📖 Read the full interactive log on my portfolio:&lt;/p&gt;

&lt;p&gt;👉  &lt;a href="https://emadaed.github.io/digisuite/engineering-log.html" rel="noopener noreferrer"&gt;Engineering Log 01: Guardrails &amp;amp; Hallucination Prevention&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Muhammad Ahmad is an AI Agent Engineer and Digital Supply Chain Architect (PMP®, RMP®) with 15+ years of operational leadership experience.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langgraph</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
