<?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[Behind the Build]]></title><description><![CDATA[Most projects look obvious once they're finished. This publication is about everything that happened before that - the wrong assumptions, debugging rabbit holes]]></description><link>https://behind-the-build.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Behind the Build</title><link>https://behind-the-build.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 16:34:52 GMT</lastBuildDate><atom:link href="https://behind-the-build.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Prompting Is Easy. Engineering Reliable AI Systems Isn't.]]></title><description><![CDATA[TL;DR
I thought building an AI application would mostly be about prompt engineering. It wasn't. By the end, prompting occupied less than 5% of the codebase. Everything else was software engineering.
W]]></description><link>https://behind-the-build.hashnode.dev/prompting-is-easy-engineering-reliable-ai-systems-isn-t</link><guid isPermaLink="true">https://behind-the-build.hashnode.dev/prompting-is-easy-engineering-reliable-ai-systems-isn-t</guid><category><![CDATA[AI]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[golang]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[backend]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Ishita Tyagi]]></dc:creator><pubDate>Sat, 11 Jul 2026 21:12:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a529d1a17928d310b3aa1c8/82c5663c-bd78-47c7-a8b4-43bc063a2c39.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR</strong></p>
<p>I thought building an AI application would mostly be about prompt engineering. It wasn't. By the end, prompting occupied less than 5% of the codebase. Everything else was software engineering.</p>
<p>When I started this project, I thought the hard part would be getting Google Gemini to say the right thing. By the end, prompting was maybe 5% of the codebase. Everything else - the part nobody warns you about - was ordinary, unglamorous software engineering.</p>
<p>Here's how that happened.</p>
<h2><strong>The idea</strong></h2>
<p>The project was simple on paper: build an AI-powered research pipeline for SaaS applications. Point it at a product, and it figures out:</p>
<ul>
<li><p>Authentication method</p>
</li>
<li><p>Category</p>
</li>
<li><p>API surface</p>
</li>
<li><p>Buildability (can you realistically build an integration against it?)</p>
</li>
<li><p>Documentation URLs</p>
</li>
</ul>
<p>Store the results. Generate a report, move on to the next one. The final goal was never just JSON - it was a report someone could actually open, inspect, and trust. That part matters later.</p>
<p>My mental model of the architecture was almost embarrassingly simple:</p>
<p><code>Gemini → JSON</code></p>
<p>Ask the model, parse the response, done. I figured the real work would be iterating on the prompt until the outputs were clean. That assumption lasted about a day.</p>
<h2><strong>Where it fell apart</strong></h2>
<p>The first thing I noticed was authentication values. I'd ask for "auth type" and get back, across different runs and different apps:</p>
<ul>
<li><p>OAuth</p>
</li>
<li><p>OAuth2</p>
</li>
<li><p>OAuth 2</p>
</li>
<li><p>OAuth 2.0</p>
</li>
<li><p>Bearer OAuth</p>
</li>
</ul>
<p>All the same concept. Five different strings. Any analytics I tried to run on top of this - "how many apps use OAuth?" - were immediately useless, because the answer depended on how I grouped five spellings of the same word.</p>
<p>My first instinct was to fix it the way you'd fix a bad prompt: I kept rewriting the instructions. Be more specific about the format. Give it an enum. Add examples. Every time OAuth came back differently, I tweaked the wording again. It never really solved the problem - the next batch would produce a sixth variant I hadn't anticipated. Eventually I realized what I was actually doing: trying to fix a deterministic problem with a probabilistic tool. That realization is what changed the rest of the architecture.</p>
<p>Categories had the same problem, just quieter. Documentation URLs were worse: sometimes I'd get a marketing page instead of the actual developer docs, and there was no way to tell the difference just by looking at the JSON.</p>
<p>Then there was the free tier. Gemini's quota limits turned into a constant background hum of <code>RESOURCE_EXHAUSTED</code>, <code>429s</code>, and the occasional <code>503</code>. At first these felt like an annoyance I'd engineer around with retries. It took a while to realize they were actually telling me something about how the whole system should be shaped.</p>
<h2><strong>Version 1: the naive pipeline</strong></h2>
<p><code>Gemini → JSON</code></p>
<p>Problems: inconsistent outputs, unusable analytics, no real way to verify anything. This is the version most people ship first, and it's the version that makes "AI engineering" look like it's just prompting. It isn't - you just haven't hit the wall yet.</p>
<h2><strong>Version 2: normalization</strong></h2>
<p><code>Gemini → Normalization → JSON</code></p>
<p>I stopped trying to solve the OAuth-spelling problem in the prompt. Prompts are probabilistic; a mapping table isn't. I could have kept tweaking the wording forever, chasing every new variant the model invented, and never actually closed the problem - or I could write ten lines of deterministic code that map OAuth2, OAuth 2, OAuth 2.0, and Bearer OAuth to one canonical value, permanently. That's the trade-off in miniature: probabilistic effort scales with how many edge cases you're willing to keep discovering; a normalization layer just ends the discovery.</p>
<p>So I added one, between the model's output and everything downstream. The effect was immediate. Categories became stable. Auth types became groupable. Analytics that had been meaningless a day earlier suddenly worked. None of this involved touching the prompt.</p>
<h2><strong>Version 3: verification</strong></h2>
<p><code>Gemini → Normalization → Verification → Confidence Score</code></p>
<p>The obvious next move - the one every tutorial suggests - is to ask another LLM whether the first LLM's answer looks good. I didn't do that.</p>
<p>Instead I wrote a verifier as plain code. It checks structure, completeness, whether there's actual evidence backing a claim, whether the documentation URL points somewhere official, whether required fields are populated. Out of that comes a confidence score - not a vibe from a second model, but a number you can trace back to specific, inspectable checks.</p>
<p>This mattered more than it sounds. "Does this look good?" asked to an LLM gives you an opinion. A deterministic verifier gives you a reason. When something scored low, I could point at exactly which check failed, instead of shrugging and re-running the prompt.</p>
<h2><strong>Version 4: the actual system</strong></h2>
<p><code>Gemini → Normalization → Verification → Analytics → HTML Dashboard</code></p>
<p>By the final version, there's exactly one network call to a model per application researched. Everything after that - normalization, verification, aggregation, report generation - runs offline, deterministically, for free, as fast as the CPU allows.</p>
<p>That one-call constraint wasn't an optimization I added later. It was forced on me by the quota limits, and it turned out to be the right design anyway - more on that below.</p>
<h2><strong>Why not MCP, and why no agent</strong></h2>
<p>At some point I considered wiring in MCP, or restructuring the whole thing as a more agentic loop - let the model decide what to fetch, when to retry, how to explore. I looked at it seriously and then didn't do it.</p>
<p>The problem didn't call for it. Research pipeline in, structured record out - that's a linear job, not one that benefits from a model making its own control-flow decisions. Adding MCP would have meant more moving parts, more failure modes, more surface area to debug, for a task that was already well served by a fixed pipeline. Simpler architecture was just better architecture here. That's not a universal law, but it was true for this problem, and I had to actually build the complicated version in my head to be sure.</p>
<h2><strong>Why deterministic verification - and what the quota taught me</strong></h2>
<p>It's common to see AI projects reach for a second LLM, or an eval framework like RAGAS, to judge the first model's output. I avoided that path deliberately, and the free-tier quota is a big part of why.</p>
<p>The Gemini free tier constantly reminded me that every request has a cost - not an abstract one, an immediate RESOURCE_EXHAUSTED one. If I'd gone the "second LLM judges the first" route, I'd have doubled my request volume for a resource that was already the tightest constraint in the system. That wasn't an option, so I was forced to ask what a judge actually needs to do - check structure, check evidence, check required fields - and realized none of that requires a model at all. It's just code.</p>
<p>So the verifier became normal software, not another AI system. That gave me fewer API calls, lower latency, lower cost, and - the part I cared about most - outputs I can explain. A verifier written as code gives you the same answer every time on the same input. An LLM judging an LLM gives you another probability distribution to reason about, stacked on top of the first one. Ironically, the constraint I resented most in week one is the reason the architecture ended up sound: treat tokens like any other expensive, rate-limited resource, and the right design tends to fall out on its own.</p>
<h2><strong>Partial failures and retries</strong></h2>
<p>Once the pipeline became a batch processor, another problem appeared: partial failures. A single API timeout shouldn't stop research for the remaining applications. Instead of treating failures as fatal, I stored partial results, marked them for manual review, and let the batch continue. Reliability isn't about preventing failures - it's about handling them predictably. That's backend engineering, not AI engineering, and it's most of what "productionizing" actually meant here.</p>
<p>I also kept verbose logs throughout development - not because they looked nice, but because watching every stage of the pipeline made debugging significantly easier than staring at final JSON outputs and guessing which stage had gone wrong.</p>
<h2><strong>What the project actually was</strong></h2>
<p>If you'd asked me at the start what percentage of this project would be prompt engineering, I'd have guessed most of it. By the end, prompting was a small, stable piece - a few carefully worded requests that don't change much once they're right. The rest was normalization logic, a verification layer, retry and fault-tolerance handling, observability into what the pipeline was actually doing, analytics code, and a reporting layer to make the results legible.</p>
<p>That's not a knock on the model. Gemini did its job. The job just turned out to be smaller than I expected, and everything around it turned out to be bigger.</p>
<h2><strong>Where I landed</strong></h2>
<p>The biggest lesson wasn't how to use Gemini. It was learning when not to ask Gemini.</p>
<p>Prompting gives you an answer. Engineering determines whether you can trust it.</p>
<p>What surprised you the most the last time you built an AI application - the model, or everything around it?</p>
<hr />
<p>If this article focused on the "why," the repository contains the "how."</p>
<h3><strong>Further Reading</strong></h3>
<p>If you're curious about the implementation, I've open-sourced the complete project.</p>
<p><strong>GitHub Repository:</strong> 🔗 <a href="https://github.com/ishita17tyagi/tooling-intelligence">https://github.com/ishita17tyagi/tooling-intelligence</a></p>
<p>It includes:</p>
<ul>
<li><p>Complete Go source code</p>
</li>
<li><p>Deterministic verification engine</p>
</li>
<li><p>Normalization pipeline</p>
</li>
<li><p>Batch processing architecture</p>
</li>
<li><p>Analytics generation</p>
</li>
<li><p>HTML report generator</p>
</li>
<li><p>Engineering retrospective</p>
</li>
</ul>
<p><em>If this article resonated with you, I'd love to hear about the engineering decision that changed the way you think about building software.</em></p>
]]></content:encoded></item></channel></rss>