<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Building with Ludenyo]]></title><description><![CDATA[Building with Ludenyo]]></description><link>https://ludenyowrites.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69fa3239a386d7f121c1a8cd/f065456b-813f-41f7-aabf-f79802e66931.svg</url><title>Building with Ludenyo</title><link>https://ludenyowrites.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 20:51:33 GMT</lastBuildDate><atom:link href="https://ludenyowrites.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Model Is the Easy Part: My First Lessons in AI Engineering]]></title><description><![CDATA[The Model Is the Easy Part: Lessons in AI Engineering]]></description><link>https://ludenyowrites.hashnode.dev/ai-that-proves-every-number</link><guid isPermaLink="true">https://ludenyowrites.hashnode.dev/ai-that-proves-every-number</guid><category><![CDATA[Beginner Developers]]></category><category><![CDATA[AI]]></category><category><![CDATA[AI Engineering]]></category><category><![CDATA[chatbot]]></category><category><![CDATA[womenwhocode]]></category><category><![CDATA[kenya]]></category><dc:creator><![CDATA[ludenyoruth]]></dc:creator><pubDate>Sun, 01 Nov 2026 23:23:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69fa3239a386d7f121c1a8cd/995f15ac-9077-47ac-8251-642cbb40e3a7.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to build an AI project that shows real AI engineering, not just a chatbot with a nice prompt. Connecting to an AI model is easy. You can do it in an afternoon. The hard part is everything around it: giving it the right data, proving its answers are right, testing it, watching it once it's live, and stopping it from answering things it shouldn't.</p>
<p>So I picked a problem where this really matters: numbers. My rule was simple: every number the AI gives must come from an official record, and a separate program (not the AI)</p>
<p>That became <strong>Cited Answers</strong>, an assistant that answers questions about Kenya, Uganda and Tanzania using World Bank data.</p>
<h2>Why not just ask ChatGPT, Google or Claude?</h2>
<p>Good question. For everyday questions, those tools are great. The difference is what this one can prove.</p>
<p>ChatGPT and similar tools answer from what they learned in training or from websites. They might add a link, but nothing checks that the number is really there. If the data is missing, they might guess. You just have to trust them.</p>
<p>Cited Answers only uses one official dataset. Every number is linked to a record and checked by code. If the data isn't there, it says, "I don't have that." And it's tested with 30 questions every time I change something.</p>
<h2>The AI engineering behind it</h2>
<p>The language model (Claude Haiku) is one piece. These are the skills that turn it into something you can trust:</p>
<ol>
<li>Retrieval-augmented generation (RAG): 1,241 World Bank figures in PostgreSQL with pgvector, so answers come from a database, not memory. The model can search by meaning ("power at home" finds "electricity") or do an exact lookup and knowing when not to use embeddings made it faster and cheaper.</li>
<li>Tool calling and structured outputs: The model can't just write a paragraph. It must give the answer plus the source of every number.</li>
<li>Verification in code: A small Python program compares every number with its record. A wrong one gets one retry, then it's removed and the reader is told.</li>
<li>Testing on every change: 30 practice questions, including trick questions, run automatically on GitHub. If an answer breaks, the change is blocked.</li>
<li>Monitoring: every answer traced in Langfuse, with cost, speed and a citation score, tagged by prompt version.</li>
<li>Guardrails: off-topic and prompt-injection attempts are refused, and usage limits cap the cost.</li>
<li>Model Context Protocol (MCP): the same data tools work in Claude Desktop or any MCP client.</li>
<li>Packaging and deployment: Docker runs the database locally and packages the app into one container, with the embedding model built in so it starts without downloading anything. For the public version, the data lives in Neon, a hosted PostgreSQL database that supports pgvector.</li>
</ol>
<h2>The moment the engineering paid off</h2>
<p>My checks said almost every number was correct. Then I read the actual answers in Langfuse, and spotted this:</p>
<p><em>"47.1% of Ugandans had access to electricity in 2022, the latest year available."</em></p>
<p>The number was right for 2022, but the database had 2024 (55.3%). In 4 out of 5 runs, the model called an older year "the latest". My checker passed it, because it checked numbers, not the words around them.</p>
<p>A general assistant would show you that confidently, and you'd probably never notice. Here, monitoring surfaced it and testing measured it. I gave the model the latest year in every lookup, and stale answers dropped from <strong>4 in 5 to 0</strong>, and it used about 11% fewer tokens, which made it cheaper to run, most likely because it no longer needed extra lookups to find the newest year.</p>
<p><em>The lesson: a good score can still hide mistakes, because a test only catches what it was built to check.</em></p>
<h2>What it's really for</h2>
<p>World Bank data is just my example. Cited Answers isn't trying to compete with ChatGPT for the public. It's a blueprint for something general assistants can't offer: an AI assistant on your own data that proves every number.</p>
<p>Different organisations could put one on their websites or internal tools:</p>
<ul>
<li><em>A bank or credit-rating agency:</em> clients ask about ratings and financial figures, and each one links to the published report</li>
<li><em>A government statistics office:</em> citizens ask about population, prices or school enrolment, answered from official tables</li>
<li><em>A hospital or NGO:</em> staff ask questions over their own reports instead of digging through spreadsheets</li>
<li><em>Any business with a dashboard</em>**:** an "ask our data" box where every answer shows its source record</li>
</ul>
<p>If your colleagues keep asking ‘where did that number come from?’, I can build you an assistant that answers before they do.</p>
<h2>Final thoughts</h2>
<p>Building this taught me that the model is the easy part. Making it trustworthy is the real work: good data, ensuring it shows proof, testing, monitoring, and letting it say "Not sure." That's the work I want to do, and this project is just the beginning.</p>
<p>That's a wrap! I'm Favour Ludenyo, and I’ll keep building things and noting down the little lessons along the way. Follow along at <a href="https://ludenyowrites.hashnode.dev">ludenyowrites.hashnode.dev</a>. Your next favourite bug story is coming.</p>
]]></content:encoded></item><item><title><![CDATA[What I learned building a route planning service with FastAPI and OSRM]]></title><description><![CDATA[I built a delivery routing service and learned a lot. Here's what surprised me about real-world constraints.]]></description><link>https://ludenyowrites.hashnode.dev/what-i-learned-building-a-route-planning-service-with-fastapi-and-osrm</link><guid isPermaLink="true">https://ludenyowrites.hashnode.dev/what-i-learned-building-a-route-planning-service-with-fastapi-and-osrm</guid><category><![CDATA[Beginner Developers]]></category><category><![CDATA[backend]]></category><category><![CDATA[routing]]></category><category><![CDATA[optimization]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[ludenyoruth]]></dc:creator><pubDate>Tue, 05 May 2026 18:56:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69fa3239a386d7f121c1a8cd/0f9c5518-aeb4-40ca-bc85-2b1a6f8ae26f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to build something that actually solves a real problem, not just another to-do app. Delivery routing felt perfect. On the surface it sounds simple: pick up items, drop them off, done. But the moment you add multiple orders, multiple vehicles, weight limits, and shared drop-off points, things get interesting very fast. So I built a route planning service. And here's what I learned along the way.</p>
<h3><em>What the Service Does At its core;</em></h3>
<p>The service takes a list of delivery orders and figures out:</p>
<ul>
<li><p>Which vehicle should handle which orders</p>
</li>
<li><p>The most efficient route for each vehicle</p>
</li>
<li><p>How to handle edge cases (like two orders going to the same outlet)</p>
</li>
</ul>
<h3><em>The stack I chose:</em></h3>
<ol>
<li><p>Fast API for building the API quickly and cleanly</p>
</li>
<li><p>OSRM (Open-Source Routing Machine) for real road-based distance</p>
</li>
<li><p>Kafka for handling order events asynchronously</p>
</li>
<li><p>OpenTelemetry for observability (traces and metrics)</p>
</li>
<li><p>k6 for load and smoke testing</p>
</li>
</ol>
<p><em><strong>The 3 Most Interesting Engineering Decisions</strong></em></p>
<ol>
<li><p><strong>Consolidating orders going to the same outlet;</strong> Early on, I realized that if two orders were headed to the same delivery point, treating them as completely separate trips was wasteful. So I added logic to group orders by outlet before assigning them to vehicles. Simple idea, but it made the routing much more realistic.</p>
</li>
<li><p><strong>Using Haversine for very short distances</strong>; Calling OSRM (a full routing engine) for every single distance calculation even tiny ones felt like overkill. For very short distances, I used the Haversine formula instead. It calculates straight-line distance between two GPS coordinates. It's not perfect (it ignores roads), but for nearby points it's fast and good enough.</p>
</li>
<li><p><strong>Limiting vehicles by weight and max drop count</strong>; This one surprised me. A vehicle might have enough weight capacity for 10 orders but if those orders are spread across 20 different locations, the route becomes impractical. So I added two constraints: maximum weight and maximum number of stops. A vehicle is full when either limit is hit. This small addition made the output feel much more real-world ready.</p>
</li>
</ol>
<p><strong>Final Thoughts</strong></p>
<p>Building this taught me that real-world problems always have more edge cases than you expect. "Plan a route" turned into "group orders, respect weight, limit stops, handle fallbacks, and don't hammer the routing engine unnecessarily." That's what makes backend engineering fun. If you're thinking about building something similar just start. The interesting decisions reveal themselves as you go.</p>
<p><em>Thanks for reading! I'm Favour Ludenyo, building and writing at ludenyowrites.hashnode.dev. Follow along for more posts like this.</em></p>
]]></content:encoded></item></channel></rss>