<?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: Anderson</title>
    <description>The latest articles on DEV Community by Anderson (@andersonasprilla).</description>
    <link>https://dev.to/andersonasprilla</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%2F4047045%2Fca368381-a72a-428d-ba3e-c59dca0ca083.jpeg</url>
      <title>DEV Community: Anderson</title>
      <link>https://dev.to/andersonasprilla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andersonasprilla"/>
    <language>en</language>
    <item>
      <title>I built a production AI agent as a Honda service advisor. Then I read the textbook.</title>
      <dc:creator>Anderson</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:22:26 +0000</pubDate>
      <link>https://dev.to/andersonasprilla/i-built-a-production-ai-agent-as-a-honda-service-advisor-then-i-read-the-textbook-a72</link>
      <guid>https://dev.to/andersonasprilla/i-built-a-production-ai-agent-as-a-honda-service-advisor-then-i-read-the-textbook-a72</guid>
      <description>&lt;p&gt;I'm a Honda service advisor in South Florida. My day is a service drive: customers dropping off cars, phones ringing, and the same fifteen questions over and over. &lt;em&gt;How much is a brake job? Is this covered? Can I come in Saturday? What does the wrench light with the "A1" mean?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I automated the repetitive half of my own job and kept the human half. It's an SMS agent — customers text the dealership's number over iMessage, and an LLM system answers from the actual owner's manual, checks their service contract, books appointments conversationally, and escalates to my Telegram when it needs a human. It's been live with real customers for months. Six languages, because my customers speak six languages.&lt;/p&gt;

&lt;p&gt;Here's a real exchange, after hours, in Spanish:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Customer:&lt;/strong&gt; Hola, quiero hacer una cita&lt;br&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; Claro — ¿qué tipo de servicio necesitas para tu Honda Ridgeline? Recomiendo en la mañana para que tengas tu vehículo de vuelta el mismo día.&lt;br&gt;
&lt;strong&gt;Customer:&lt;/strong&gt; El sábado en la mañana&lt;br&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; Lo siento, no estamos abiertos los sábados. ¿Te vendría bien el viernes? Abrimos a las 7 — ¿te funciona a las 8:00 AM?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent knew my schedule, refused the closed day, offered a concrete alternative, and never left the customer's language.&lt;/p&gt;

&lt;p&gt;I built all of it by hitting problems and fixing them. Then I read Chip Huyen's &lt;em&gt;AI Engineering&lt;/em&gt;, and had the strange experience of finding names for things I'd invented in the dark — plus one glaring gap I hadn't noticed at all.&lt;/p&gt;

&lt;p&gt;This is the mapping, and the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I learned by breaking production: the model classifies, code guarantees
&lt;/h2&gt;

&lt;p&gt;If I could hand one sentence to someone shipping their first agent, it's that one. It cost me three separate production bugs to write it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: the model cannot compare two dates
&lt;/h3&gt;

&lt;p&gt;A customer with an &lt;em&gt;expired&lt;/em&gt; service contract asked whether a repair was covered. The agent told them yes. It was reading the coverage data correctly — the expiration date was right there in the context — and it still got the comparison wrong.&lt;/p&gt;

&lt;p&gt;I did what everyone does first: I fixed the prompt. Put today's date in the system message. Spelled out "compare the expiration date to today's date." Told it to be careful.&lt;/p&gt;

&lt;p&gt;It failed 3 out of 3.&lt;/p&gt;

&lt;p&gt;So I stopped asking. Now the date comparison happens in Python and the model only gets to relay the verdict:&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;_annotate_expirations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stamp a computed past/active verdict next to every expiration date in the
    portal data. LLMs are unreliable at date comparison, so the comparison is
    done here and the model only relays the verdict.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;today&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;line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  ⚠️ [COMPUTED: this date has already PASSED — no longer valid]&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;line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [COMPUTED: date is in the future — still active]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every dated line in the coverage data arrives at the model pre-judged. There is no date math left for it to get wrong. That function now has seven unit tests, and they cost zero API calls to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: the model cannot add up a repair order
&lt;/h3&gt;

&lt;p&gt;Customer asks for three services and then, "so what's the total out the door?" The agent quoted three correct prices and then produced a total that was simply wrong — shop supplies and Florida sales tax applied to a subtotal it had added up by itself.&lt;/p&gt;

&lt;p&gt;Same fix. The fee formula lives in code exactly once:&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;compute_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sum quoted service prices into an out-the-door breakdown, rounded to cents.

    Single source of truth for the fee formula so it never drifts between the
    prompt and the code:
      shop supplies = min(subtotal × 10%, $59.50)
      tax           = (subtotal + shop supplies) × 7%
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model's job is to notice that the customer asked for a total and to say the resulting numbers like a human. Not to be a calculator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: the model saying "done" is not evidence that it's done
&lt;/h3&gt;

&lt;p&gt;The booking agent returns structured output — a Pydantic schema, enforced by the API — with a &lt;code&gt;complete&lt;/code&gt; flag. Early on, a booking saved with &lt;strong&gt;no service type in it&lt;/strong&gt;. The model had marked itself complete, and I believed it.&lt;/p&gt;

&lt;p&gt;Now the server independently re-checks that every required field is really filled before anything is written or emailed. The model's self-assessment is a suggestion. The check is the authority.&lt;/p&gt;

&lt;p&gt;Three different bugs, one shape: &lt;strong&gt;any claim the model makes that code can verify, code should verify.&lt;/strong&gt; Everything downstream of that rule got more reliable — and cheaper to test, because assertions on pure functions don't need an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stuff I'd built without knowing it had a name
&lt;/h2&gt;

&lt;p&gt;Reading the book was less "here's something new" and more "oh, that's what I've been doing":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval-augmented generation.&lt;/strong&gt; I'd indexed Honda owner's manuals into Pinecone because customers ask questions whose answers are buried in a PDF nobody opens. Turns out that's the canonical use case, not a hack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails, input side.&lt;/strong&gt; Incoming messages get scored and sanitized before they reach any agent — I wrote it after reading about prompt injection and getting nervous about a public phone number. It's a named layer with a literature behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured outputs.&lt;/strong&gt; I moved booking extraction from regex over prose to an enforced schema after data markers started leaking into customer texts. ("&lt;code&gt;[BOOKING_COMPLETE]&lt;/code&gt;" is not a thing you want a customer to receive.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing / orchestration.&lt;/strong&gt; One classifier call per message decides intent, language, vehicle, and whether to escalate, then dispatches through a registry. Adding a capability is one registry entry plus one handler. I built it because a single mega-prompt got unmaintainable around 400 lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context economics.&lt;/strong&gt; Pricing and policy blocks get injected only when the customer's message actually needs them; vehicle context loads once per session and caches. I did this because my token bill got my attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that made the book a waste of time. The opposite: I paid for each of those lessons in production incidents, with real customers on the other end. The book costs thirty dollars. If you're about to build this, buy it &lt;em&gt;first&lt;/em&gt; — you'll still hit your own bugs, but you'll hit better ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap: I had no evals
&lt;/h2&gt;

&lt;p&gt;Here's what reading it actually changed.&lt;/p&gt;

&lt;p&gt;My quality signal, for months, was &lt;strong&gt;"a customer complained, or a scripted terminal conversation happened to catch it."&lt;/strong&gt; That's not a signal. That's luck with a feedback delay.&lt;/p&gt;

&lt;p&gt;So I built the deterministic layer first — no LLM judges, no fancy framework, one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python evals.py                    &lt;span class="c"&gt;# full suite&lt;/span&gt;
python evals.py &lt;span class="nt"&gt;--runs&lt;/span&gt; 3           &lt;span class="c"&gt;# more runs → better flakiness signal&lt;/span&gt;
python evals.py &lt;span class="nt"&gt;--only&lt;/span&gt; orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over 130 cases now. Every single one is either a real production bug or a behavior I can assert exactly. The sections: date annotation, orchestrator classification, tech/pricing answers, the outreach flow, appointment confirmation, price gating, total math, routing, and maintenance-code decoding. Most of them are pure functions — they run in seconds and cost nothing.&lt;/p&gt;

&lt;p&gt;Three things about it worth stealing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The hallucinated-price detector.&lt;/strong&gt; My favorite eval in the suite, and it's nine lines. Regex every dollar amount out of the agent's reply, and assert each one exists in the pricing file:&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;_known_amounts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&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="n"&gt;PRICING_PATH&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_PRICE_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&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;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_hallucinated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_known_amounts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_PRICE_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deterministic hallucination detection for the one category of hallucination that would actually cost my dealership money. No judge model, no rubric, no ambiguity. If you have a domain where the ground truth is a file, you can write this today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It found a dormant bug in its first hour.&lt;/strong&gt; The orchestrator can classify a short follow-up ("Why?", "How come?") as &lt;code&gt;follow_up&lt;/code&gt;, which sticky-routes back to whichever agent answered last. Nice feature. Except &lt;code&gt;follow_up&lt;/code&gt; was missing from the intent registry, so the validator silently coerced every one of those verdicts to &lt;code&gt;tech&lt;/code&gt; — the entire sticky-routing path was dead code, and had been for weeks. Nothing crashed. No customer wrote in to say "your routing table has a missing enum member." It took writing down what I &lt;em&gt;expected&lt;/em&gt; to happen for the gap to become visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Flakiness is a result, not a nuisance.&lt;/strong&gt; Each LLM case runs N times and reports a pass &lt;em&gt;rate&lt;/em&gt;. A case that passes 4/5 is telling me something a binary green check would hide. Cases where the model is unreliable but code catches it downstream are marked &lt;code&gt;known_flaky&lt;/code&gt; — they report their rate and don't fail the suite. That's honest: the model &lt;em&gt;is&lt;/em&gt; flaky there, and the guard is the thing I'm actually relying on.&lt;/p&gt;

&lt;p&gt;The next layer is an LLM judge over full transcripts with binary rubric questions — &lt;em&gt;is the reply native-sounding in the customer's language? is it grounded in the provided manual context? was escalating the right call?&lt;/em&gt; — calibrated against transcripts I label myself, since I'm both bilingual and the domain expert. That's the nice thing about building for a job you actually work: I am the ground truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're shipping your first agent
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write down what the model must never be trusted with.&lt;/strong&gt; Dates, arithmetic, and its own claims of completion are on my list. Yours will be different but it will not be empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the model's job "notice and phrase," not "compute."&lt;/strong&gt; Reliability went up and prompts got shorter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your first evals should cost zero API calls.&lt;/strong&gt; Pure functions and regex over outputs caught more real bugs than anything clever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed the suite with every bug you've already shipped.&lt;/strong&gt; It's the cheapest dataset you'll ever have and it's already labeled by production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track pass rates, not pass/fail.&lt;/strong&gt; Non-determinism is data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain knowledge is the moat.&lt;/strong&gt; The reason this agent knows not to book Saturday, that an expired pass gets quoted at regular price, and that "A1" means oil change plus tire rotation isn't the model. It's a decade on a service drive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is on GitHub: &lt;a href="https://github.com/andersonasprilla/Service-Advisor-AI-Agent" rel="noopener noreferrer"&gt;Service-Advisor-AI-Agent&lt;/a&gt;. Architecture diagram, the eval suite, and a demo video of a real conversation are all in the README.&lt;/p&gt;

&lt;p&gt;I'm a service advisor teaching myself to be an engineer in public, and I'm looking for applied-AI / conversational-AI work. If you build in this space — dealer tech especially — I'd like to hear from you.****&lt;/p&gt;

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