<?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: Amine</title>
    <description>The latest articles on DEV Community by Amine (@bynevolabs).</description>
    <link>https://dev.to/bynevolabs</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%2F4071344%2Ff93c642e-c62a-46c6-8821-1679a770635b.png</url>
      <title>DEV Community: Amine</title>
      <link>https://dev.to/bynevolabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bynevolabs"/>
    <language>en</language>
    <item>
      <title>Building a support agent that refuses to make things up</title>
      <dc:creator>Amine</dc:creator>
      <pubDate>Mon, 10 Aug 2026 15:03:59 +0000</pubDate>
      <link>https://dev.to/bynevolabs/building-a-support-agent-that-refuses-to-make-things-up-4g4l</link>
      <guid>https://dev.to/bynevolabs/building-a-support-agent-that-refuses-to-make-things-up-4g4l</guid>
      <description>&lt;p&gt;Most "AI customer service" demos fail the same way. You ask something the model&lt;br&gt;
can't answer from data, and instead of stopping, it produces a plausible answer.&lt;br&gt;
In a chat toy, that is a curiosity. In after-sales support it becomes a promise&lt;br&gt;
the company has to honour: a refund nobody approved, or a delivery date that&lt;br&gt;
never existed.&lt;/p&gt;

&lt;p&gt;I build these agents for e-commerce shops, for the pre-sale questions that decide&lt;br&gt;
whether someone buys and the after-sale ones that decide whether they come back.&lt;br&gt;
Almost all of the engineering goes into one problem: making the agent's honesty a&lt;br&gt;
property of the architecture rather than the prompt. The examples here lean on&lt;br&gt;
after-sales, where a wrong answer costs the most, but the same design carries&lt;br&gt;
pre-purchase questions just as well. This post walks through how.&lt;/p&gt;
&lt;h3&gt;
  
  
  The mistake: treating the model as the source of truth
&lt;/h3&gt;

&lt;p&gt;The naive design is one model, one big prompt, and a pile of documents in a&lt;br&gt;
vector store. Ask "where is my order 41822?" and the retrieval layer returns the&lt;br&gt;
three chunks that look most like the question. None of them contain order 41822,&lt;br&gt;
because it is a live database row rather than a document, so the model gets a&lt;br&gt;
context window full of order-shaped text and answers anyway. The answer is wrong.&lt;/p&gt;

&lt;p&gt;A better prompt does not fix this. What fixes it is removing the model's ability&lt;br&gt;
to answer that class of question at all.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bounded actions instead of free-form generation
&lt;/h3&gt;

&lt;p&gt;Every request the agent handles is routed to exactly one of a fixed set of&lt;br&gt;
intents: order status, delivery delay, return, refund status, exchange, invoice,&lt;br&gt;
product question, cancellation. That set is closed. There is no fallback intent&lt;br&gt;
that means "answer anyway".&lt;/p&gt;

&lt;p&gt;Each intent maps to a typed action with an explicit contract:&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="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Reads the order system of record. Never generates a status.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;order_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Shopify / WooCommerce / API
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t find that order number on this account.&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_facts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;# only whitelisted fields
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details do the real work here. The first is that &lt;code&gt;facts&lt;/code&gt; is a whitelist.&lt;br&gt;
&lt;code&gt;public_facts()&lt;/code&gt; returns the carrier, the tracking number, the shipped-at&lt;br&gt;
timestamp and the current state. It does not return the margin, the internal&lt;br&gt;
notes, the customer's other orders or the fraud score. The model can't leak a&lt;br&gt;
field it was never handed.&lt;/p&gt;

&lt;p&gt;The second is that the natural-language layer only phrases. The model receives&lt;br&gt;
the resolved facts plus a template intent, and writes one or two sentences in&lt;br&gt;
the shop's tone of voice. It never decides what the status is; it is handed the&lt;br&gt;
status and asked to say it well. There is no open question left at generation&lt;br&gt;
time, so hallucination has nothing to attach to.&lt;/p&gt;
&lt;h3&gt;
  
  
  Making refusal a first-class outcome
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Resolution.escalate&lt;/code&gt; is not a failure path. It is an expected outcome with its&lt;br&gt;
own quality bar, and the one that matters most.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;NOT_FOUND&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# no matching record
&lt;/span&gt;    &lt;span class="n"&gt;OUT_OF_SCOPE&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# intent not in the closed set
&lt;/span&gt;    &lt;span class="n"&gt;POLICY_UNCLEAR&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# rule exists but doesn't cover this case
&lt;/span&gt;    &lt;span class="n"&gt;LOW_CONFIDENCE&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# intent classification below threshold
&lt;/span&gt;    &lt;span class="n"&gt;HUMAN_REQUESTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# customer asked for a person
&lt;/span&gt;    &lt;span class="n"&gt;EMOTIONAL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# anger / distress detected
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each reason produces a different hand-off: a specific message to the customer, a&lt;br&gt;
priority in the human queue, and a summary attached to the ticket so whoever&lt;br&gt;
picks it up does not start from zero.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;EMOTIONAL&lt;/code&gt; branch matters more than it looks. A furious customer is not a&lt;br&gt;
retrieval failure; the system may hold every fact it needs. It still goes to a&lt;br&gt;
human, because "technically resolvable" and "should be handled by a machine" are&lt;br&gt;
different questions. Getting that wrong is how automation loses the trust it was&lt;br&gt;
supposed to earn.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LOW_CONFIDENCE&lt;/code&gt; needs a real threshold, calibrated per shop rather than a&lt;br&gt;
hard-coded &lt;code&gt;0.7&lt;/code&gt;, and it should be asymmetric: a wrong refund costs far more than&lt;br&gt;
an unnecessary escalation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Writes are gated separately from reads
&lt;/h3&gt;

&lt;p&gt;Reading an order is safe; issuing a refund is not. The two sit behind different&lt;br&gt;
gates, and the gate is configuration the merchant owns rather than a prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;order_status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;auto&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;return_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;auto&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;max_value_eur&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;80&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;refund&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;propose&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;        &lt;span class="c1"&gt;# drafts it, a human clicks send&lt;/span&gt;
  &lt;span class="na"&gt;cancel_order&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;propose&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;address_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;auto&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;before_dispatch_only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;propose&lt;/code&gt; mode is what makes the first month of a deployment survivable. The&lt;br&gt;
agent does the work and drafts the action; a human approves it, and you watch&lt;br&gt;
the approval rate. Once an action clears without edits often enough, that history&lt;br&gt;
is what justifies flipping it to &lt;code&gt;auto&lt;/code&gt;: a decision earned from your own data&lt;br&gt;
instead of a vendor's promise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the hosting boundary is an architecture decision
&lt;/h3&gt;

&lt;p&gt;I run each shop on its own instance, in France, on a French model&lt;br&gt;
(&lt;a href="https://mistral.ai/" rel="noopener noreferrer"&gt;Mistral&lt;/a&gt;). That sounds like a marketing line, so here is&lt;br&gt;
the engineering behind it.&lt;/p&gt;

&lt;p&gt;Support conversations are among the most sensitive data a shop holds, less for&lt;br&gt;
the order numbers than for what customers write around them: addresses, health&lt;br&gt;
reasons for a return, money troubles, complaints about a named employee. Once&lt;br&gt;
that runs through a shared multi-tenant pipeline in another jurisdiction, "where&lt;br&gt;
is my data" stops being a question you can answer and becomes one you forward to&lt;br&gt;
a vendor.&lt;/p&gt;

&lt;p&gt;A dedicated instance also makes reversibility real instead of contractual. When&lt;br&gt;
a merchant leaves, the export is a database dump and a config file, handed over&lt;br&gt;
without a support ticket in sight.&lt;/p&gt;

&lt;p&gt;The EU AI Act's transparency obligation (Article 50) points the same way: the&lt;br&gt;
customer has to know they are talking to a machine. That is easier to guarantee&lt;br&gt;
when the disclosure lives in your own message-composition layer than when it is&lt;br&gt;
a toggle in someone else's dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this costs you
&lt;/h3&gt;

&lt;p&gt;The trade-off is real: this design resolves fewer conversations than a model&lt;br&gt;
with a free hand. A closed intent set will not cover the long tail, bounded&lt;br&gt;
actions cannot improvise, and &lt;code&gt;propose&lt;/code&gt; mode keeps a human in the loop for weeks.&lt;/p&gt;

&lt;p&gt;I would make that trade every time. The permissive design has a worse failure&lt;br&gt;
mode. It does not produce a slightly worse answer; it produces a confident wrong&lt;br&gt;
statement that a real person acts on, in a channel where the shop is legally the&lt;br&gt;
one who said it.&lt;/p&gt;

&lt;p&gt;An agent that says "let me get a human on this" is a minor disappointment. An&lt;br&gt;
agent that invents a refund policy is an incident the shop then has to clean up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Amine, founder of &lt;a href="https://bynevolabs.com/" rel="noopener noreferrer"&gt;Bynevo Labs&lt;/a&gt;. We build&lt;br&gt;
&lt;a href="https://bynevolabs.com/ia-souveraine-service-client/" rel="noopener noreferrer"&gt;sovereign AI support agents&lt;/a&gt;&lt;br&gt;
for French e-commerce, answering customer questions before the sale and after it,&lt;br&gt;
hosted in France on an open-source stack. Happy to talk architecture in the&lt;br&gt;
comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>ecommerce</category>
      <category>python</category>
    </item>
  </channel>
</rss>
