<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Writing | Law Zava</title><link>https://lawzava.com/blog/</link><description>Field notes on AI operating models, org design, decision latency, and the economics of serious execution.</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Wed, 01 Jul 2026 16:49:55 +0000</lastBuildDate><atom:link href="https://lawzava.com/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>The AI Strategy Stack: What Boards Mistake for Moats</title><link>https://lawzava.com/blog/2026-06-30-ai-strategy-stack-boards-mistake-moats/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-30-ai-strategy-stack-boards-mistake-moats/</guid><description>Most AI moat claims are distribution theater; durable moats come from routing economics, proprietary workflow data, and operational reliability.</description><content:encoded><![CDATA[<p>The strongest argument against this whole essay is short: foundation models keep getting better, so whatever gap your proprietary data closes this quarter, the next base model closes for free. If that were fully true, no data moat in AI would be worth funding. It is half true. The half it gets wrong is the half boards keep paying for.</p>
<p>Start with what the strategy deck stacks up as defensible. Three layers, usually. The model itself, rented, and your competitor can rent the same one. The scaffolding on top of it: prompt library, routing logic, eval harness, all shaped around one vendor&rsquo;s behavior and all of it breaks the morning you change providers. <strong>If the moat disappears when the vendor changes, it was never a moat. It was a dependency.</strong> That clears two of the three layers off the slide. Swap the provider in your head; whatever still works the next morning is the only candidate worth the word.</p>
<p>What survives is the third layer, and it is the one boards cannot tell apart from its imitation: data your own operation produces by running. Here is the mechanism, and the exact place it breaks.</p>
<p>Take support automation. The model drafts a resolution; a human approves before it ships. Every rejection or rewrite captures a labeled triple: the input, the output the model produced, the output the human accepted. Not a log line. A graded example of where your model was wrong and what right looked like, on your tickets, in your domain.</p>
<p>Now the load-bearing step, the one most decks wave through. How does that triple make a cheaper model tier handle a class it used to escalate? Two mechanisms, two different bills.</p>
<p>Retrieval, the few-shot route: index the accepted exemplars and inject the nearest ones into the prompt at inference. Cheap to stand up, live the moment you index a correction, but it taxes every call in tokens and latency, and the lift is capped because you are renting the base model&rsquo;s in-context learning.</p>
<p>Distillation, the fine-tune route: train the small model on the correction set. Latency stays flat, the behavior is baked in, per-call cost drops, but you pay a training-and-eval cycle up front and re-pay it on every base-model upgrade.  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >Which tier absorbs which class</a>
 is a cost decision, not a model-quality one: retrieval for the long tail, distillation for the high-volume classes once they stop drifting.</p>
<p>Either way, one number tells you which you have: escalation rate on a single request class, quarter over quarter. Falling and sustained while quality holds is the loop compounding. Flat is a warehouse with a dashboard bolted to it. A logging pipeline and a compounding loop look identical in the architecture diagram and behave nothing alike in the P&amp;L.</p>
<p>I cannot hand you a rival&rsquo;s P&amp;L to prove the good case, and any deck that shows you a clean before-and-after percentage is selling the illustration as the evidence. The honest test is one you run on your own numbers: name the class, name the two quarters it improved, name why a competitor on the same vendor cannot reproduce it. The answer to the last one is never the model. It is the  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >system around it</a>
 that turns each rejection into an exemplar only you hold.</p>
<p>Then the part the optimistic version omits: this asset depreciates. When the next base model ships, it absorbs your easy classes for free, everyone&rsquo;s, not only yours, and that compresses the set of failures where your corrections still move the number. Your edge is only ever the residual: corrections illegible outside your context, your product&rsquo;s quirks, your contractual edge cases, your policy language. The vendor will productize the capture loop; they already sell feedback buttons and fine-tuning APIs. What they cannot aggregate is a residual that means nothing without your business wrapped around it. The loop compounds only while you generate domain-specific corrections faster than a better base model erases the generic ones.</p>
]]></content:encoded></item><item><title>From Model Demos to Profit Engines: The CTO Playbook for AI Unit Economics</title><link>https://lawzava.com/blog/2026-06-25-ai-profit-engines-unit-economics/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-25-ai-profit-engines-unit-economics/</guid><description>AI value is won in routing and failure-cost control, not in picking a single “best” model.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>A beautiful demo is not a business model. It only proves the model can look useful before the business pays for edge cases. The bill arrives when the system hits real users, real load, and real failure conditions. At that point AI stops being a model-selection problem and becomes a  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >routing problem</a>
, a fallback problem, and a repair problem. Good CTOs do not buy &ldquo;smart.&rdquo; They buy systems that stay cheap enough, predictable enough, and reliable enough to survive the week.</p>
<h2 id="unit-economics-start-with-routing">Unit economics start with routing</h2>
<p>The wrong AI architecture sends every request to the most expensive path. That feels elegant until the invoice arrives. Mature systems route by value and by risk.</p>
<p>A practical routing model usually splits work into classes:</p>
<ul>
<li>trivial tasks that should  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >stay cheap and local</a>
</li>
<li>medium-value tasks that deserve a balanced model tier</li>
<li>high-stakes tasks that justify expensive reasoning and stronger checks</li>
</ul>
<p>This is not model worship. It is cost discipline.</p>
<h2 id="the-hidden-cost-is-rarely-the-model-line-item">The hidden cost is rarely the model line item</h2>
<p>Teams fixate on tokens because tokens are visible. The real bill sits around the model: retries, context assembly, human correction, support escalation, and the work of proving the output is acceptable.</p>
<p>If a system saves one minute for a customer and creates two minutes of cleanup, it is destroying margin.</p>
<p>A finance-aware CTO should be able to answer these questions without hand-waving:</p>
<ul>
<li> <a href="/blog/2024-10-14-ai-cost-benchmarking/"
   
   >what each class of request costs to serve</a>
</li>
<li>where the rework happens</li>
<li>what failure costs when the model is wrong</li>
<li>which parts of the workflow justify premium inference</li>
</ul>
<h2 id="the-real-decision-is-not-model-choice-it-is-failure-cost">The real decision is not model choice, it is failure cost</h2>
<p>&ldquo;Best model&rdquo; is usually the wrong conversation. The useful conversation is about failure cost.</p>
<p>A cheaper model that fails gracefully can beat a more expensive model that fails silently. A  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >local fallback</a>
 that keeps the system alive during a rate-limit event can matter more than a small quality lift in the happy path.</p>
<p>The CTO playbook is simple: optimize the whole system, not the benchmark screenshot.</p>
<h2 id="measure-margin-at-the-workflow-level">Measure margin at the workflow level</h2>
<p>The right unit of measure is the workflow, not the model call.</p>
<p>Ask:</p>
<ul>
<li>how much does this workflow cost end to end?</li>
<li>how often does it need human repair?</li>
<li>how long does it take to reach a trustworthy answer?</li>
<li>what is the revenue or labor value of the result?</li>
</ul>
<p>That is where the business truth lives. A model that looks slightly less accurate in isolation may create better margin if it is cheaper, faster, and easier to trust.</p>
<h2 id="a-practical-threshold">A practical threshold</h2>
<p>If the system does not improve margin, then it needs to improve risk or speed. If it improves neither, it is a demo that escaped the lab.</p>
<p>AI work that  <a href="/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/"
   
   >survives budget review</a>
 answers one of four questions:</p>
<ul>
<li>does it lower cost per task?</li>
<li>does it reduce human labor?</li>
<li>does it increase throughput?</li>
<li>does it unlock new revenue with acceptable risk?</li>
</ul>
<p>If not, the demo should stay in the demo lane.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Route cheap work cheaply.</li>
<li>Model cost is only part of the bill.</li>
<li>Measure workflow margin, not call cost.</li>
<li>If it does not improve  <a href="/blog/2026-04-28-margin-risk-speed-ai-strategy-metrics/"
   
   >margin, risk, or speed</a>
, it does not belong in production.</li>
</ul>
]]></content:encoded></item><item><title>The New Talent Stack: Product, Platform, and Applied AI Must Work as One System</title><link>https://lawzava.com/blog/2026-06-18-new-talent-stack-for-ai-organizations/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-18-new-talent-stack-for-ai-organizations/</guid><description>AI organizations create leverage when product, platform, and applied AI are designed as one operating system instead of three kingdoms.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most  <a href="/blog/2024-12-02-building-ai-teams/"
   
   >AI hiring plans</a>
 are trying to fix an interface problem with resumes.</p>
<p>If product, platform, and applied AI are not built as one operating system, new headcount adds motion but not leverage. The constraint is usually not talent scarcity. It is system design.</p>
<h2 id="recruiting-alone-cannot-fix-a-broken-stack">Recruiting Alone Cannot Fix a Broken Stack</h2>
<p>AI organizations often describe their issue as “we need stronger talent.” In many cases, they already have capable people. What they lack is a clear operating contract across teams.</p>
<p>The pattern is familiar:</p>
<ul>
<li>product optimizes for release velocity</li>
<li>platform optimizes for reliability and control</li>
<li>applied AI optimizes for model behavior and evaluation quality</li>
</ul>
<p>Each goal is rational. The breakdown happens at the handoffs.</p>
<p>When interfaces are unclear, every launch becomes a negotiation. When interfaces are explicit, the same teams produce compounding output.</p>
<h2 id="the-three-layer-talent-stack">The Three-Layer Talent Stack</h2>
<p>A healthy stack has three interlocking layers with distinct responsibilities:</p>
<ol>
<li><strong>Product</strong> — owns user outcomes and business success metrics.</li>
<li><strong>Platform</strong> — owns safe defaults, deployment paths, and observability.</li>
<li><strong>Applied AI</strong> — owns workflow behavior, retrieval/prompting/routing choices, and evaluation quality.</li>
</ol>
<p>These are not departments in competition. They are system components with different jobs.</p>
<p>If product outruns platform, quality debt accumulates.
If platform outruns product, infrastructure becomes generic overhead.
If applied AI outruns both, you get technically impressive demos that never operationalize.</p>
<h2 id="where-organizations-usually-break">Where Organizations Usually Break</h2>
<p>Most failures are boundary failures, not individual failures.</p>
<p>Common symptoms:</p>
<ul>
<li>no explicit owner for the model-to-product handoff</li>
<li>platform operating as a  <a href="/blog/2026-05-14-why-ai-platform-teams-become-bottlenecks/"
   
   >ticket queue instead of an enablement layer</a>
</li>
<li>applied AI measured by demo novelty instead of  <a href="/blog/2026-05-19-stop-building-internal-ai-tools-no-one-uses/"
   
   >adoption in live workflows</a>
</li>
<li>product committing features that infra cannot support safely</li>
</ul>
<p>A concise diagnosis: <strong>org debt is usually interface debt with better branding.</strong></p>
<h2 id="design-the-stack-intentionally">Design the Stack Intentionally</h2>
<p>The fix is not “more syncs.” The fix is explicit decision rights.</p>
<ul>
<li>product owns problem selection and business tradeoffs</li>
<li>platform owns reliability guardrails and release safety</li>
<li>applied AI owns workflow performance and  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation integrity</a>
</li>
<li>leadership owns  <a href="/blog/2026-06-10-ai-leadership-bench-roles-interfaces/"
   
   >escalation rules</a>
 when tradeoffs conflict</li>
</ul>
<p>Once this is explicit, hiring quality improves. You stop searching for mythical generalists and start  <a href="/blog/2026-05-26-hiring-operators-for-ai-teams/"
   
   >hiring operators</a>
 who can perform inside a coherent system.</p>
<h2 id="what-to-evaluate-before-adding-headcount">What to Evaluate Before Adding Headcount</h2>
<p>Before opening new roles, run this short check:</p>
<ol>
<li>Are cross-team handoffs documented and current?</li>
<li>Does each layer have clear success metrics it actually controls?</li>
<li>Are escalation paths clear when speed, reliability, and quality disagree?</li>
<li>Are teams rewarded for system outcomes rather than local optimization?</li>
</ol>
<p>If those answers are weak, fix interfaces first. New hires will scale the current operating model, good or bad.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Strong AI organizations are designed as a system, not staffed as silos.</li>
<li>Product, platform, and applied AI need explicit interfaces and decision rights.</li>
<li>Boundary clarity is a bigger lever than raw headcount.</li>
<li>Hiring works best after the operating contract is clear.</li>
</ul>
]]></content:encoded></item><item><title>The Executive Case for Local-First AI Infrastructure</title><link>https://lawzava.com/blog/2026-06-16-local-first-ai-infrastructure-executive-case/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-16-local-first-ai-infrastructure-executive-case/</guid><description>Local-first AI is not ideology. It is control over placement, margin, latency, and failure modes.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p> <a href="/blog/2025-08-18-local-ai-development/"
   
   >Local-first AI</a>
 is not anti-cloud. It is an operating decision about where work runs, where risk sits, and where margin leaks.</p>
<p>If placement is left to default settings, you inherit default latency, default privacy exposure, and default unit economics. Executives should treat compute placement the way they treat pricing and  <a href="/blog/2026-06-09-ai-vendor-negotiation-playbook/"
   
   >vendor strategy</a>
: explicit, reviewed, and tied to outcomes.</p>
<h2 id="stop-treating-placement-as-an-implementation-detail">Stop Treating Placement as an Implementation Detail</h2>
<p>Most teams still frame local-first as a tooling preference. That misses the real issue.</p>
<p>Placement determines four things that show up directly in business performance:</p>
<ul>
<li><strong>Latency control</strong> — fewer network hops and less variance in response time.</li>
<li><strong>Privacy control</strong> —  <a href="/blog/2026-04-06-sovereign-systems-privacy-non-optional/"
   
   >sensitive data can stay inside your perimeter</a>
.</li>
<li><strong>Cost control</strong> — high-frequency calls stop accumulating  <a href="/blog/2026-02-09-ai-cost-trends/"
   
   >per-request tax</a>
.</li>
<li><strong>Failure control</strong> — fallback paths are closer to the workload and easier to reason about.</li>
</ul>
<p>That is not ideology. That is operational control.</p>
<h2 id="where-local-first-should-be-the-default">Where Local-First Should Be the Default</h2>
<p>Local-first wins when work is frequent, bounded, and expensive to keep outsourcing call-by-call.</p>
<p>Typical candidates:</p>
<ul>
<li> <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >routing and classification</a>
</li>
<li>extraction and normalization</li>
<li>internal workflows handling regulated data</li>
<li>high-volume background tasks</li>
<li> <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval-heavy systems</a>
 where context assembly dominates spend</li>
</ul>
<p>The pattern is practical:  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >keep frontier work in the cloud, run repeatable workload locally</a>
, and route between them intentionally.</p>
<p>A useful line for leadership teams: <strong>placement discipline is margin discipline.</strong></p>
<h2 id="where-cloud-still-wins">Where Cloud Still Wins</h2>
<p>The right architecture is usually hybrid, not absolutist.</p>
<p>Cloud remains the better default when you need:</p>
<ul>
<li>frontier reasoning or specialized capabilities you do not host</li>
<li>burst capacity you cannot justify building for</li>
<li>minimal operational overhead for low-volume workloads</li>
<li>fast access to capabilities you do not need to own long term</li>
</ul>
<p>The mistake is not using cloud APIs. The mistake is using them by reflex after workload shape has changed.</p>
<h2 id="an-incremental-adoption-path">An Incremental Adoption Path</h2>
<p>Do not start with a full migration plan. Start with workload triage.</p>
<ol>
<li>Identify repeated tasks where per-request cost is accumulating.</li>
<li>Move low-risk routing and transformation paths first.</li>
<li>Keep cloud as escalation while you validate reliability and observability.</li>
<li>Expand local placement only after economics and failure behavior are proven.</li>
</ol>
<p>If you do this well, the architecture gets less dramatic over time. That is a success condition.</p>
<h2 id="executive-decision-rubric">Executive Decision Rubric</h2>
<p>Before moving a workload local, ask:</p>
<ol>
<li><strong>Is frequency high enough that per-request cost now matters?</strong></li>
<li><strong>Does the workload touch data that should stay inside our boundary?</strong></li>
<li><strong>Can we operate fallback and observability well enough to trust it?</strong></li>
</ol>
<p>If two of three are yes, you likely have a local-first candidate.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Local-first is a control strategy, not a cloud rejection strategy.</li>
<li>Compute placement directly affects latency, privacy posture, and margins.</li>
<li>Hybrid architecture is the practical default: local for repeated bounded work, cloud for frontier escalation.</li>
<li>Move incrementally and prove economics before scaling hardware commitments.</li>
</ul>
]]></content:encoded></item><item><title>Decision Latency as a P&amp;L Variable: The Leadership Metric Nobody Owns</title><link>https://lawzava.com/blog/2026-06-10-decision-latency-p-and-l-variable/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-10-decision-latency-p-and-l-variable/</guid><description>Decision latency is measurable and should be treated as a direct cost driver.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Slow decisions look like caution. In practice, they are hidden expense.</p>
<p>Decision latency belongs on the P&amp;L. Every day a real decision sits unresolved, the business pays in delay, rework, and attention.</p>
<h2 id="why-decision-latency-matters">Why Decision Latency Matters</h2>
<p>A team can  <a href="/blog/2026-05-05-measure-ai-progress-without-theater/"
   
   >look productive</a>
 and still be dragging the business down if every meaningful decision takes too long.</p>
<p>Decision latency shows up as:</p>
<ul>
<li>stalled launches</li>
<li>expired opportunities</li>
<li>duplicated work</li>
<li>growing frustration in the teams closest to the customer</li>
</ul>
<p>When leaders do not measure this, they blame execution when the real problem is delay. The work may be moving. The organization is not.</p>
<h2 id="what-decision-latency-looks-like-in-practice">What Decision Latency Looks Like in Practice</h2>
<p>You can usually find it by asking a few questions:</p>
<ul>
<li>How long does a high-signal issue sit before someone decides?</li>
<li>How many people need to weigh in before the first answer exists?</li>
<li>How often do decisions get reopened because no one owned the original call?</li>
<li>How much work is blocked waiting for alignment that never arrives?</li>
</ul>
<p>Those are not soft questions. They are economic questions.</p>
<p>If a release,  <a href="/blog/2026-05-26-hiring-operators-for-ai-teams/"
   
   >hiring decision</a>
,  <a href="/blog/2026-06-09-ai-vendor-negotiation-playbook/"
   
   >vendor decision</a>
, or  <a href="/blog/2026-06-02-ai-incident-review-changes-architecture/"
   
   >architecture decision</a>
 sits for weeks, the business is paying rent on uncertainty.</p>
<p>A useful line: <strong>ambiguous ownership is the most expensive architecture in your company.</strong></p>
<h2 id="make-it-visible">Make It Visible</h2>
<p>If you want leaders to care, make the metric visible.</p>
<p>Track:</p>
<ul>
<li>time from issue raised to decision made</li>
<li>time from decision made to action taken</li>
<li>number of escalations per decision class</li>
<li>number of decisions reopened after approval</li>
</ul>
<p>Once those numbers are in the open, patterns become hard to deny. You can see which teams move fast, which questions keep getting rerouted, and where the organization is burning time on decisions that should have been routine.</p>
<h2 id="how-to-reduce-it">How to Reduce It</h2>
<p>Decision latency drops when teams do four things well:</p>
<ol>
<li>Define  <a href="/blog/2026-06-10-ai-leadership-bench-roles-interfaces/"
   
   >who owns each decision class</a>
.</li>
<li>Set  <a href="/blog/2026-05-07-ai-governance-without-bureaucracy/"
   
   >decision boundaries</a>
 before the crisis.</li>
<li>Reduce the number of people required for routine calls.</li>
<li>Make escalation fast when the decision is truly material.</li>
</ol>
<p>This is not about making every decision unilateral. It is about making routine decisions quick and risky decisions explicit.</p>
<p>If the call is small, the system should move. If the call is material, the system should know exactly who has to weigh in.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Decision latency is a real cost driver.</li>
<li>Measure the time from issue to decision and from decision to action.</li>
<li>Ownership clarity reduces hidden opex.</li>
<li>The best organizations make routine decisions quickly and unusual decisions deliberately.</li>
</ul>
]]></content:encoded></item><item><title>Designing the AI Leadership Bench: Roles, Interfaces, and Failure Boundaries</title><link>https://lawzava.com/blog/2026-06-10-ai-leadership-bench-roles-interfaces/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-10-ai-leadership-bench-roles-interfaces/</guid><description>AI scaling needs explicit leadership interfaces between product, platform, reliability, and governance.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p> <a href="/blog/2026-05-21-ai-technical-leadership/"
   
   >AI leadership</a>
 does not fail because titles are missing. It fails because interfaces are missing.</p>
<p>A real leadership bench is the decision system connecting product, platform, reliability, and governance. If those seams are unclear, incidents turn into organizational confusion before they become technical recovery.</p>
<h2 id="a-bench-is-an-interface-map">A Bench Is an Interface Map</h2>
<p>Many companies think “strong bench” means “we hired senior people.” That is necessary, but not sufficient.</p>
<p>A working bench answers four questions without debate:</p>
<ul>
<li>who owns product tradeoffs</li>
<li>who owns platform reliability</li>
<li>who owns  <a href="/blog/2026-05-07-ai-governance-without-bureaucracy/"
   
   >model governance</a>
 and risk boundaries</li>
<li>who owns escalation when those priorities collide</li>
</ul>
<p>If the answers depend on who is online that day, the bench is not operational.</p>
<h2 id="core-roles-and-decision-rights">Core Roles and Decision Rights</h2>
<p>The exact titles vary. The interfaces should not.</p>
<p><strong>Product owner</strong> — accountable for business outcome and adoption targets.</p>
<p><strong>Platform owner</strong> — accountable for safe defaults,  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >observability</a>
, and deployment reliability.</p>
<p><strong>Applied AI owner</strong> — accountable for workflow behavior, routing, and  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation quality</a>
.</p>
<p><strong>Governance owner</strong> — accountable for explicit, reviewable risk boundaries.</p>
<p>The goal is not bureaucracy. The goal is unambiguous ownership when tradeoffs are real.</p>
<h2 id="failure-boundaries-beat-hero-culture">Failure Boundaries Beat Hero Culture</h2>
<p>Healthy leadership systems plan for predictable stress cases instead of hoping for heroic response.</p>
<p>Define boundary behavior for events like:</p>
<ul>
<li>model quality degradation</li>
<li>vendor policy or terms changes</li>
<li>quiet workflow failure that evades basic monitoring</li>
<li>loss of a  <a href="/blog/2026-05-26-hiring-operators-for-ai-teams/"
   
   >key operator</a>
</li>
</ul>
<p>If those handoffs are documented and rehearsed, incidents stay technical. If not, incidents become political.</p>
<p>One reliable warning sign: one person is expected to explain the full system from memory. That is not a bench. That is a single point of organizational failure.</p>
<h2 id="how-to-build-the-bench-in-practice">How to Build the Bench in Practice</h2>
<p>Make interfaces concrete and testable:</p>
<ul>
<li>document what each owner can decide without escalation</li>
<li>define escalation thresholds for speed vs reliability vs governance conflicts</li>
<li>map core metrics to the leader who can actually move them</li>
<li>rehearse  <a href="/blog/2026-06-02-ai-incident-review-changes-architecture/"
   
   >incident handoffs</a>
 before live incidents force improvisation</li>
</ul>
<p>This is operational hygiene, not ceremony.</p>
<p>A line worth keeping: <strong>great leaders design boundaries before they design org charts.</strong></p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>AI leadership strength comes from interfaces, not senior titles alone.</li>
<li>Product, platform, applied AI, and governance need explicit owners and decision rights.</li>
<li>Failure boundaries should be defined before incidents, not during them.</li>
<li>If one person holds the whole system context, the bench is underbuilt.</li>
</ul>
]]></content:encoded></item><item><title>The Operating Cadence: Turning AI Leadership Interfaces Into Predictable Output</title><link>https://lawzava.com/blog/2026-06-10-operating-cadence-ai-leadership-interfaces/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-10-operating-cadence-ai-leadership-interfaces/</guid><description>Interfaces describe who owns what. Cadence is what turns those interfaces into compounding output.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>A  <a href="/blog/2026-06-10-ai-leadership-bench-roles-interfaces/"
   
   >bench with clear interfaces</a>
 is a necessary foundation. It is not a compounding system. Without rhythm, documented ownership drifts back into informal updates, and informal updates beat formal ones right up until they don&rsquo;t.</p>
<p>Cadence is the mechanism that keeps interfaces load-bearing.</p>
<h2 id="interfaces-without-cadence-degrade">Interfaces Without Cadence Degrade</h2>
<p>When a team documents who owns what, the clarity is real — for a few weeks. Then the pace picks up, the weekly sync gets skipped once, and the product owner starts resolving platform questions directly because it is faster. The interface is still on paper. It is no longer operational.</p>
<p>This is the failure mode that connects a well-designed bench to a  <a href="/blog/2026-06-10-post-prototype-ai-org/"
   
   >year-two org</a>
 that is back to improvising. Nobody dismantled the system. They just stopped running it.</p>
<p>Formal coordination loses to informal coordination every time informal coordination has lower friction. The only fix is making the formal cadence the path of least resistance — by keeping it short, metric-anchored, and non-negotiable.</p>
<h2 id="the-three-cadences-that-compound">The Three Cadences That Compound</h2>
<p>Three rhythms cover the full operating surface of a scaling AI program.</p>
<p><strong>Weekly operating cadence</strong> — 30 minutes, same metrics every cycle. Latency, error rate,  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >eval scores</a>
, blocked work. The point is not status; it is signal. Any metric outside its threshold triggers an owner, not a discussion. If nothing is outside threshold, the meeting ends early.</p>
<p><strong>Monthly outcome review</strong> — 90 minutes, owners present against targets set the previous month. What moved, what did not, what is at risk next month. This is where product and platform tradeoffs surface before they become incidents. Governance owner attends. Decisions are recorded with the owner and the date.</p>
<p><strong>Quarterly architecture audit</strong> — half day, forward-looking. Where is the system accumulating hidden cost? What capability investment is being deferred? What would break first if the load doubled? The audit produces a short list of bets for the next quarter, not a roadmap deck.</p>
<p>Each cadence locks in a different time horizon. Weekly locks in operational latency. Monthly locks in outcome reliability. Quarterly locks in capability investment. Together they cover the full range from &ldquo;is anything on fire today&rdquo; to &ldquo;are we building toward where the load is going.&rdquo;</p>
<h2 id="what-each-cadence-prevents">What Each Cadence Prevents</h2>
<p>The weekly cadence prevents  <a href="/blog/2025-11-10-ai-incident-management/"
   
   >alert fatigue</a>
 from becoming normalized degradation. Teams that skip it tend to discover the same problems later, at higher cost, under more pressure.</p>
<p>The monthly review prevents the gap between product ambition and platform reality from widening silently. That gap is where most  <a href="/blog/2026-05-28-ai-roadmaps-survive-reality/"
   
   >AI roadmap slippage</a>
 hides. By the time it is visible to leadership, it is already a quarter behind.</p>
<p><em>Cadence does not eliminate incidents. It shortens  <a href="/blog/2026-06-10-decision-latency-p-and-l-variable/"
   
   >the distance between a signal and a decision</a>
.</em></p>
<p>The quarterly audit prevents  <a href="/blog/2026-06-02-ai-incident-review-changes-architecture/"
   
   >incident-driven re-architecture</a>
. The single most expensive pattern in scaling AI programs is emergency redesign under production pressure. Orgs that run a quarterly audit tend to make the same architectural changes earlier, cheaper, and with less organizational disruption. The audit is not a guarantee — it is a forcing function for the conversation that should happen before the crisis.</p>
<h2 id="the-predictability-test">The Predictability Test</h2>
<p>A cadence is working when the team can answer one question before the quarter ends: what is the most likely bottleneck next quarter, and who owns the intervention?</p>
<p>This is not a forecasting exercise. It is a structural test. If nobody can answer it, the cadence is collecting status but not producing foresight. The monthly reviews are not surfacing risk early enough, or the quarterly audit is not connected to the weekly signal.</p>
<p>If the team can answer it — even roughly — the cadence is compounding. The interfaces are being exercised on a predictable rhythm, and that rhythm is generating the kind of organizational memory that makes year-two scale possible without heroics.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Documented interfaces degrade without a cadence to run them; informal coordination fills the gap and eventually breaks.</li>
<li>Three rhythms cover the full operating surface: weekly operating, monthly outcome review, quarterly architecture audit.</li>
<li>Each cadence locks in a different time horizon — latency, reliability, and capability investment respectively.</li>
<li>A cadence is working when the team can predict next quarter&rsquo;s bottleneck before it arrives.</li>
</ul>
]]></content:encoded></item><item><title>The Post-Prototype AI Org: Operating Models That Survive Year Two</title><link>https://lawzava.com/blog/2026-06-10-post-prototype-ai-org/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-10-post-prototype-ai-org/</guid><description>Year-two AI failure usually comes from org-design mismatch, not model-quality mismatch. The handoffs are where the system slows down.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>A lot of AI orgs look healthy in month three and brittle by year two. The model usually did not fail. The operating model did. Prototype energy is easy to create; durable coordination is not.</p>
<p>The question is not whether the team can ship something exciting. The question is whether the company can keep shipping after the novelty fades.</p>
<h2 id="why-the-prototype-phase-hides-the-real-problem">Why the prototype phase hides the real problem</h2>
<p>In the early phase, AI teams often succeed because everyone is close to the work. Decisions are informal, context is shared, and the whole system fits in a few people’s heads. That stops scaling almost immediately.</p>
<p>As soon as the team grows, the same strengths turn into liabilities:</p>
<ul>
<li>knowledge becomes hidden</li>
<li>approvals multiply</li>
<li>handoffs slow down</li>
<li>nobody owns the  <a href="/blog/2026-06-10-ai-leadership-bench-roles-interfaces/"
   
   >interface boundaries</a>
</li>
</ul>
<p>What worked when the team was small no longer works when the company needs predictability.</p>
<h2 id="the-operating-model-should-be-explicit">The operating model should be explicit</h2>
<p>A post-prototype AI org needs to define how work moves.</p>
<p>The model should answer:</p>
<ul>
<li>who owns the user problem?</li>
<li>who owns the runtime?</li>
<li>who owns the quality signal?</li>
<li>who owns the  <a href="/blog/2026-05-07-ai-governance-without-bureaucracy/"
   
   >risk boundary</a>
?</li>
<li>who can stop the release?</li>
</ul>
<p>Without those answers, the team is improvising around gaps that will eventually become incidents or delays.</p>
<h2 id="handoffs-are-the-hidden-bottleneck">Handoffs are the hidden bottleneck</h2>
<p>Most  <a href="/blog/2026-05-28-ai-roadmaps-survive-reality/"
   
   >AI roadmaps</a>
 do not fail because the team lacks ideas. They fail because each handoff adds ambiguity.</p>
<p>The problem shows up in predictable places:</p>
<ul>
<li>product asks for speed, platform asks for safety</li>
<li>applied AI wants more freedom, compliance wants more proof</li>
<li>leadership wants output, the system wants more control</li>
</ul>
<p>That tension is normal. What is not normal is leaving it unresolved.</p>
<p>A good operating model turns tension into a documented interface, not a recurring crisis.</p>
<h2 id="scale-requires-less-heroics-not-more">Scale requires less heroics, not more</h2>
<p>The post-prototype org has to depend less on heroic behavior and more on repeatable behavior.</p>
<p>That usually means:</p>
<ul>
<li>clearer ownership</li>
<li> <a href="/blog/2026-06-10-decision-latency-p-and-l-variable/"
   
   >smaller decision surfaces</a>
</li>
<li>stronger  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >eval gates</a>
</li>
<li>visible  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >rollback paths</a>
</li>
<li>fewer ambiguous exceptions</li>
</ul>
<p>This can feel slower at first, but it is the only way the org gets faster at scale.</p>
<h2 id="a-simple-test">A simple test</h2>
<p>Ask whether the AI system can survive a senior person going on vacation for two weeks.</p>
<p>If the answer is “not really,” the organization is still running on hidden tribal knowledge.</p>
<p>If the answer is “yes, with documented ownership and a stable operating model,” the company is moving from prototype to production.</p>
<p>That is the real year-two test.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Prototype energy does not scale on its own.</li>
<li>The year-two problem is usually organizational, not model-related.</li>
<li>Ownership, interfaces, and escalation paths matter more than the demo itself.</li>
<li>A durable AI org is designed for scale before the prototype succeeds.</li>
</ul>
]]></content:encoded></item><item><title>The AI Vendor Negotiation Playbook for CTOs</title><link>https://lawzava.com/blog/2026-06-09-ai-vendor-negotiation-playbook/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-09-ai-vendor-negotiation-playbook/</guid><description>Vendor leverage in AI comes from architecture readiness, eval data, and exit credibility — not procurement theater.</description><content:encoded><![CDATA[<p>Use this before any AI vendor contract renewal, initial procurement, or pricing negotiation. Most CTOs walk in under-prepared — the vendor knows your dependency footprint better than you do. This worksheet closes that gap. Work through it the day before the meeting.</p>
<hr>
<h2 id="1-workload-facts-you-must-have">1. Workload Facts You Must Have</h2>
<p>The vendor’s first move is to define your usage for you. Don’t let them.</p>
<ul>
<li><input disabled="" type="checkbox"> Total request volume per month, broken out by use case
<em>A single aggregate number is not enough. Know which workflows drive cost.</em></li>
<li><input disabled="" type="checkbox"> Cost per task class (e.g., generation vs. classification vs. retrieval)
<em>If you cannot name your top three cost drivers, you cannot challenge the invoice.</em></li>
<li><input disabled="" type="checkbox"> Latency p50/p95 by workflow, measured from  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >your own instrumentation</a>

<em>Vendor SLAs are measured at their edge, not yours.</em></li>
<li><input disabled="" type="checkbox"> Percentage of spend attributable to this vendor vs.  <a href="/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/"
   
   >total AI budget</a>

<em>Concentration creates leverage — for them. Know the number.</em></li>
<li><input disabled="" type="checkbox"> Named owner of the vendor relationship on your side
<em>If no one owns it, no one negotiates it.</em></li>
</ul>
<h2 id="2-architecture-leverage-check">2. Architecture Leverage Check</h2>
<p>Leverage is an architecture property. Answer these before you sit down.</p>
<ul>
<li><input disabled="" type="checkbox"> Is the vendor’s API called directly from product code, or through an  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >abstraction layer</a>
?
<em>Direct calls = switching costs measured in months. Abstraction = measured in days.</em></li>
<li><input disabled="" type="checkbox"> How many distinct integration points does this vendor touch?
<em>Write the number. Fewer than five is manageable. More than ten is a dependency.</em></li>
<li><input disabled="" type="checkbox"> What is the estimated engineering cost to swap this vendor?
<em>Get a real estimate, even a rough one. &ldquo;Unknown&rdquo; is not an answer.</em></li>
<li><input disabled="" type="checkbox"> Do you have a secondary provider you have already integrated, even partially?
<em>Yes/No. If no, you have no credible threat.</em></li>
<li><input disabled="" type="checkbox"> Does your data pipeline depend on vendor-specific formats or  <a href="/blog/2023-07-10-embedding-models-deep-dive/"
   
   >embeddings</a>
?
<em> <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >Format lock-in</a>
 is often more expensive than API lock-in.</em></li>
</ul>
<h2 id="3-evaluation-evidence">3. Evaluation Evidence</h2>
<p>Vendors sell on benchmark claims. Counter with your data.</p>
<ul>
<li><input disabled="" type="checkbox"> Do you have  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evals that measure model performance on your actual workload</a>
?
<em>Yes/No. If no, you are buying on their terms by default.</em></li>
<li><input disabled="" type="checkbox"> Which models have you tested against your task suite in the last 90 days?
<em>List them. If the answer is only theirs, you have no comparison point.</em></li>
<li><input disabled="" type="checkbox"> What is your acceptable quality threshold, defined numerically?
<em>&ldquo;Good enough&rdquo; is not a threshold. A number is.</em></li>
<li><input disabled="" type="checkbox"> Have you run a  <a href="/blog/2024-10-14-ai-cost-benchmarking/"
   
   >cost-per-correct-output comparison</a>
 across providers?
<em>Price per token is a distraction. Price per correct result is the metric.</em></li>
<li><input disabled="" type="checkbox"> Who owns your eval framework and can demo it in the meeting if needed?
<em>Named person, not a team.</em></li>
</ul>
<h2 id="4-exit-credibility">4. Exit Credibility</h2>
<p>A vendor that believes you cannot leave does not negotiate. Make them uncertain.</p>
<ul>
<li><input disabled="" type="checkbox"> Do you have a documented migration plan, even a sketch?
<em>It does not need to be final. It needs to exist.</em></li>
<li><input disabled="" type="checkbox"> What is your contractual notice period to exit?
<em>Know this before they remind you of it.</em></li>
<li><input disabled="" type="checkbox"> Have you identified which vendor you would move to first if pricing increased 40%?
<em>Name them. Vague alternatives are not alternatives.</em></li>
<li><input disabled="" type="checkbox"> Is there a sunset timeline for any features that are vendor-exclusive today?
<em>If yes, the vendor knows your dependency has an expiration date.</em></li>
<li><input disabled="" type="checkbox"> Can your team absorb a two-week migration sprint without derailing the roadmap?
<em>Yes/No. Honest answer only.</em></li>
</ul>
<hr>
<p>If you cannot fill in the workload numbers, you are not done preparing — you are about to negotiate against someone who has already modeled your spend. If you have no eval data, you will accept their performance claims by default. If there is no exit plan, any number they name is essentially a take-it-or-leave-it offer. The meeting itself is the wrong place to discover these gaps. Thirty minutes with this worksheet before you walk in is worth more than any negotiation tactic once you are in the room.</p>
]]></content:encoded></item><item><title>How to Run an AI Incident Review That Changes Architecture, Not Slides</title><link>https://lawzava.com/blog/2026-06-02-ai-incident-review-changes-architecture/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-06-02-ai-incident-review-changes-architecture/</guid><description>Incident reviews should produce architecture deltas and control updates, not narrative theater.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>An AI incident review is only useful if it changes the system. Anything else is a postmortem-shaped meeting.</p>
<p>If the review does not change architecture, evaluation, or  <a href="/blog/2026-05-07-ai-governance-without-bureaucracy/"
   
   >control boundaries</a>
, the organization has paid for ceremony and learned too little.</p>
<h2 id="the-point-of-an-incident-review">The Point of an Incident Review</h2>
<p>The point of an incident review is not to assign theater-friendly blame.</p>
<p>The point is to answer:</p>
<ul>
<li>what failed</li>
<li>why it failed</li>
<li>how we knew</li>
<li>what should change so it fails differently next time</li>
</ul>
<p>If that last step is missing, the review is incomplete.</p>
<h2 id="what-good-reviews-produce">What Good Reviews Produce</h2>
<p>A strong incident review should produce concrete outputs:</p>
<ul>
<li>a change to architecture</li>
<li>a change to  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation coverage</a>
</li>
<li>a change to  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >alerting or observability</a>
</li>
<li>a change to access or  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >fallback policy</a>
</li>
<li>a change to ownership or escalation rules</li>
</ul>
<p>If the only output is a slide deck, the organization is optimizing for closure, not improvement.</p>
<p>The cleanest signal is whether the same class of incident can happen again. If it can, the review was not done.</p>
<h2 id="how-ai-incidents-are-different">How AI Incidents Are Different</h2>
<p> <a href="/blog/2025-11-10-ai-incident-management/"
   
   >AI incidents</a>
 often degrade quietly long before they trigger a loud outage.</p>
<p>The symptoms may be:</p>
<ul>
<li>degraded answer quality</li>
<li>increased retries</li>
<li>hallucinated outputs that look plausible</li>
<li>cost spikes hiding inside normal traffic</li>
<li>users losing trust before the team notices</li>
</ul>
<p>That means incident reviews need to look at both user impact and system behavior. You cannot fix what you did not measure.</p>
<p>Incidents tell you where the system was  <a href="/blog/2026-04-21-enterprise-ai-architecture-fails/"
   
   >more fragile than the architecture review admitted</a>
.</p>
<h2 id="a-useful-review-template">A Useful Review Template</h2>
<p>A practical review should cover:</p>
<ol>
<li>the triggering event</li>
<li>the timeline</li>
<li>the technical failure mode</li>
<li>the business impact</li>
<li>the monitoring gap</li>
<li>the architectural fix</li>
<li>the owner of the fix</li>
<li>the follow-up verification date</li>
</ol>
<p>That is enough to keep the review grounded and actionable.</p>
<p>A  <a href="/blog/2021-11-29-incident-management-practices/"
   
   >postmortem</a>
 without system change is paperwork.</p>
<p>The template is simple on purpose. If the review cannot name the control that changes, the meeting was too abstract.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Incident reviews should change architecture, not just record narrative.</li>
<li>AI failures often show up as silent degradation before loud incidents.</li>
<li>Good reviews end with specific fixes, owners, and verification dates.</li>
<li>If the same class of incident can recur, the review was not complete.</li>
</ul>
]]></content:encoded></item><item><title>How Great CTOs Design AI Roadmaps That Survive Contact With Reality</title><link>https://lawzava.com/blog/2026-05-28-ai-roadmaps-survive-reality/</link><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-28-ai-roadmaps-survive-reality/</guid><description>AI roadmaps fail when they are sequenced around ambition instead of dependency, verification, and rollback cost.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI roadmaps fail when ambition is treated as sequencing. Dependencies slip, rollback gets expensive, and the team discovers the missing work only after the launch date is already spoken for.</p>
<p>A survivable roadmap is not a prettier Gantt chart. It is a dependency-aware budget for uncertainty.</p>
<h2 id="roadmaps-fail-at-the-edges">Roadmaps Fail at the Edges</h2>
<p>The core mistake is treating the roadmap like a statement of intent instead of a statement of sequencing.</p>
<p>AI work fails at the edges:</p>
<ul>
<li>data access is slower than expected</li>
<li>model behavior is less stable than expected</li>
<li>review cycles take longer than expected</li>
<li>vendor changes arrive earlier than expected</li>
</ul>
<p>If your roadmap does not account for those edges, it is not a plan. It is a confidence exercise.</p>
<p>Most teams only find out those edges are missing after the launch date is already public.</p>
<p>The fix is to move the hidden work into the plan before the promise is made.</p>
<h2 id="budget-the-dependency-chain">Budget the Dependency Chain</h2>
<p>Every AI feature has a dependency chain:</p>
<ul>
<li>data availability</li>
<li> <a href="/blog/2024-07-22-context-window-strategies/"
   
   >context assembly</a>
</li>
<li> <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >model routing</a>
</li>
<li>evaluation</li>
<li>deployment</li>
<li>fallback</li>
</ul>
<p>If any one of those links is not ready, the feature will not survive real use.</p>
<p>If the chain is incomplete, the roadmap is lying by omission.</p>
<p>The most honest roadmap is the one that writes the chain down first. That slows the conversation, but it also keeps the team from selling a feature that depends on work nobody has budgeted.</p>
<p>Slower conversations are cheaper than broken launches.</p>
<h2 id="make-rollback-a-first-class-requirement">Make Rollback a First-Class Requirement</h2>
<p>Good roadmaps assume the first version will be wrong.</p>
<p>That means every AI initiative should answer four questions:</p>
<ul>
<li>How do we turn this off?</li>
<li> <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >How do we know it is hurting us?</a>
</li>
<li>How fast can we revert?</li>
<li>What manual path exists if the model degrades?</li>
</ul>
<p>If those answers are fuzzy, the roadmap is overconfident.</p>
<p>If you cannot turn it off quickly, you have  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >shipped a liability with a product label</a>
.</p>
<p>Roadmaps should not only describe the happy path. They should budget for the probability that the first version is wrong, the vendor changes terms, or the model regresses under load.</p>
<p>That is not pessimism. It is operational seriousness.</p>
<h2 id="wip-limits-matter-more-than-hope">WIP Limits Matter More Than Hope</h2>
<p>A roadmap that promises too many parallel AI experiments is usually a roadmap that does not respect WIP.</p>
<p>The more novel the work, the lower the WIP should be.</p>
<p>Concurrency feels productive until it multiplies rework.</p>
<p>Strong teams set rules like:</p>
<ul>
<li>no more than one high-risk AI launch per squad at a time</li>
<li>no feature ships without  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation coverage</a>
</li>
<li>no vendor migration without a fallback path</li>
<li>no roadmap item enters “done” until the operational notes exist</li>
</ul>
<p>That may sound strict. It is. Novel work punishes loose concurrency.</p>
<h2 id="what-a-survivable-roadmap-looks-like">What a Survivable Roadmap Looks Like</h2>
<p>Survivable roadmaps are dependency-explicit, rollback-aware, and honest about capacity.</p>
<p>A roadmap is not a promise. It is a bet with visible failure modes.</p>
<p>If the failure modes are invisible, the roadmap is pretending.</p>
<p>You do not need a roadmap that impresses the room. You need one the organization can execute without pretending the hard parts are somebody else&rsquo;s problem.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>AI roadmaps fail at dependency and rollback boundaries.</li>
<li>Treat the roadmap as a budget for uncertainty, not a wish list.</li>
<li>Limit WIP, make rollback explicit, and require evaluation coverage before launch.</li>
<li>The best roadmap is the one the organization can survive.</li>
</ul>
]]></content:encoded></item><item><title>Hiring for AI Teams: The Operator Profile That Actually Scales</title><link>https://lawzava.com/blog/2026-05-26-hiring-operators-for-ai-teams/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-26-hiring-operators-for-ai-teams/</guid><description>The highest-leverage AI hires are operators who can handle ambiguity, systems tradeoffs, and verification pressure.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>The best AI hires are not the people who can narrate the model stack. They are the operators who can turn ambiguity into a system, make the failure mode legible, and keep shipping when the first answer is wrong.</p>
<p>That is why judgment matters more than hype. Teams that hire for excitement get enthusiastic meetings. Teams that hire for operator discipline get leverage.</p>
<h2 id="the-operator-profile">The Operator Profile</h2>
<p>Strong AI operators usually have four traits:</p>
<ul>
<li>they can turn a vague brief into a tractable plan without waiting for perfect inputs</li>
<li>they know enough about systems tradeoffs to challenge weak assumptions early</li>
<li>they care about verification as much as output</li>
<li>they can move between engineering, product, and executive language without flattening the nuance</li>
</ul>
<p>Model trivia is cheap. Operator judgment is what survives contact with production.</p>
<p>The market is full of people who can name the newest framework. The shortage is people who can keep a system healthy when the workload changes, the vendor shifts, or the first release misbehaves.</p>
<h2 id="what-most-teams-hire-wrong">What Most Teams Hire Wrong</h2>
<p> <a href="/blog/2024-12-02-building-ai-teams/"
   
   >AI hiring</a>
 goes off the rails when teams reward signals that are easy to notice but hard to run with.</p>
<p>Teams over-index on:</p>
<ul>
<li>prompt fluency without operational discipline</li>
<li>research taste without delivery habits</li>
<li>architecture opinions without  <a href="/blog/2025-11-10-ai-incident-management/"
   
   >incident literacy</a>
</li>
<li>product instinct without measurement rigor</li>
</ul>
<p>None of those traits is bad. The problem is imbalance.</p>
<p>A strong AI team needs people who will own the boring parts:  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evals</a>
, fallback logic, access boundaries, cost control, and documentation precise enough that someone else can operate the system later.</p>
<p>If a candidate can talk fluently about models but cannot explain how they would debug a bad release, they are not ready to own production AI.</p>
<h2 id="the-interview-questions-that-matter">The Interview Questions That Matter</h2>
<p>You do not need a clever  <a href="/blog/2018-04-16-technical-interviewing-what-actually-works/"
   
   >hiring process</a>
. You need questions that force real evidence.</p>
<p>Ask candidates to walk through:</p>
<ol>
<li>
<p><strong>A system they had to stabilize.</strong>
What was broken, how did they know, and what changed after they touched it?</p>
</li>
<li>
<p><strong>A decision they reversed.</strong>
Strong operators do not defend bad ideas forever. They update when the evidence changes.</p>
</li>
<li>
<p><strong>A workflow they measured.</strong>
If they cannot show how they connected work to metrics, they probably did not own the outcome.</p>
</li>
<li>
<p><strong>A failure they made safer.</strong>
In AI, good operators do not eliminate failure. They bound it.</p>
</li>
</ol>
<p>A useful answer is concrete, a little messy, and grounded in actual work. The worst answer sounds polished and empty.</p>
<h2 id="hire-for-the-shape-of-the-system">Hire for the Shape of the System</h2>
<p> <a href="/blog/2026-02-16-ai-team-structures/"
   
   >AI teams</a>
 do not need the same operator profile in every context. Research-heavy, production-heavy, and regulated enterprise teams all demand different instincts.</p>
<p>If you want a research-heavy team, hire for exploration and rigor. If you want a production-heavy team, hire for stability and operational discipline. If you want a regulated enterprise team, the bar is not “exciting.” The bar is whether this person can help you ship safely, repeatedly, and without heroics.</p>
<p>That is the real operator profile:</p>
<ul>
<li>can handle uncertainty without freezing</li>
<li>can make tradeoffs explicit</li>
<li>can  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >leave behind a system other people can run</a>
</li>
<li>can keep pace without turning every launch into a performance</li>
</ul>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Hire AI operators for judgment, not model vocabulary.</li>
<li>Ask about stabilization, reversal, measurement, and safer failure.</li>
<li>The strongest people leave behind systems, not just stories.</li>
<li>If a candidate cannot explain how they debug and bound failure, keep looking.</li>
</ul>
]]></content:encoded></item><item><title>Technical Leadership in the AI Era (It’s About Throughput, Not Trends)</title><link>https://lawzava.com/blog/2026-05-21-ai-technical-leadership/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-21-ai-technical-leadership/</guid><description>Technical leadership in mid-2026: anchor decisions in throughput, verification, and operability instead of chasing the latest agent framework.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI does not change the core job of technical leadership. It changes the cost of being vague. In 2026, the best leaders still do the same three things: set direction, remove friction, and keep production systems measurable. The difference is that AI makes every weak assumption show up faster.</p>
<p>The real mandate is throughput. Not more noise. Not more  <a href="/blog/2026-05-05-measure-ai-progress-without-theater/"
   
   >experimentation theater</a>
. Throughput.</p>
<h2 id="the-leadership-pivot-focus-on-throughput">The Leadership Pivot: Focus on Throughput</h2>
<p>Organizations do not pay technical leaders to keep up with model releases. They pay them to improve  <a href="/blog/2026-03-30-throughput-engineer-headcount-lagging-metric/"
   
   >organizational throughput</a>
.</p>
<p>That means reducing cognitive overhead, tightening verification, and making deployment paths boring enough that teams can move without drama. If you cannot measure what an AI workflow produced, or what it cost to produce it, you do not have an operating system yet. You have a prototype with invoices.</p>
<p>The leadership question is simple: are we removing blockers faster than we are adding complexity?</p>
<h2 id="decision-making-in-practice">Decision-Making in Practice</h2>
<p>AI work gets messy when teams debate tools before they define the outcome.</p>
<p>Good leaders force the conversation back to first principles:</p>
<ul>
<li>What business metric should change if we ship this?</li>
<li>What latency budget do we actually have?</li>
<li> <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >What happens when the model is wrong?</a>
</li>
</ul>
<p>Those questions cut through a lot of noise. They keep the team from turning architecture meetings into opinion contests about  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector databases</a>
, prompt styles, or the latest agent framework.</p>
<p>If the answer to any of those questions is fuzzy, the work is not ready for serious implementation.</p>
<h2 id="define-good-enough-and-measure-it">Define “Good Enough” and Measure It</h2>
<p>Reliability is not just accuracy. It is consistency, cost, and the ability to  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >catch degradation before customers do</a>
.</p>
<p>Sometimes  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >a smaller, cheaper model</a>
 is the right answer. Sometimes the frontier model is worth the price. The point is not to be religious about either option. The point is to define the bar, test against it, and choose the system that meets it with the least operational pain.</p>
<p>Your job is not to build a perfect AI system. It is to build one where failure is bounded, expected, and visible.</p>
<h2 id="the-cultural-shift">The Cultural Shift</h2>
<p>Technical leadership still has a change-management problem. Engineers will worry about ownership, safety, and the volatility of the ecosystem. Those concerns are real.</p>
<p>The right response is not debate for its own sake. It is instrumentation.</p>
<p>Stop arguing in design docs about whether a model will work. Build the telemetry that shows whether it works. Stop treating every new framework like a strategy reset. Run small, contained experiments that either produce evidence or die cheaply.</p>
<p>The strongest teams are not the ones that sprint toward the newest beta API. They are the ones that can absorb change without losing control.</p>
<h2 id="final-take">Final Take</h2>
<p>AI rewards leaders who are disciplined about outcomes and ruthless about verification. If the team can move quickly, measure clearly, and recover cleanly, AI becomes leverage. If not, it becomes another source of drag.</p>
]]></content:encoded></item><item><title>Stop Building Internal AI Tools No One Uses</title><link>https://lawzava.com/blog/2026-05-19-stop-building-internal-ai-tools-no-one-uses/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-19-stop-building-internal-ai-tools-no-one-uses/</guid><description>Internal AI tools fail when teams optimize for launch instead of habit formation, trust, and workflow fit.</description><content:encoded><![CDATA[<p>The demo went well. A mid-size logistics company — roughly 800 people, enough procurement complexity to  <a href="/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/"
   
   >justify the investment</a>
 — had spent three months building an internal AI tool to surface contract terms during vendor negotiations. The launch Slack channel hit 40 reactions in the first hour. A VP called it the kind of thing that changes how the team operates.</p>
<p>Six weeks later, the channel had five messages in it, four of them automated. The procurement leads were still pulling PDFs manually and copying terms into a shared spreadsheet. One support engineer, who had quietly championed the project from the beginning, had reverted to her old database query because &ldquo;the tool doesn&rsquo;t know about the amendments.&rdquo; The tool was still running. Nobody had officially abandoned it. It had simply become invisible.</p>
<p>This pattern is not unusual. It is almost the default.</p>
<h2 id="what-actually-failed">What Actually Failed</h2>
<p>The postmortem conversation usually centers on the wrong things — model choice, interface design, rollout timing. Those are symptoms. The root causes are structural.</p>
<p>The contract tool was built around a narrow slice of the negotiation workflow: surfacing base terms. But procurement work is not base terms. It is base terms plus amendments plus prior history plus the relationship context the lead carries in her head. The tool knew one layer of a five-layer problem. It looked complete in a demo because demos are controlled. Real work is not controlled.</p>
<p>The output trust problem arrived fast. In week two, the tool surfaced an incorrect payment term — technically correct in the original contract, superseded by a signed amendment it had not been given access to. The lead caught it before it caused damage, but she stopped relying on it after that. <em>One unexplained wrong answer is enough to demote a tool from co-worker to footnote.</em> The team had not  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >built evaluation into the system</a>
, so there was no way to know how often this happened, which made the uncertainty worse, not better.</p>
<p>Nobody owned adoption after the launch. The engineer who built it moved to a different priority. The VP who celebrated it never checked  <a href="/blog/2025-07-07-ai-product-metrics/"
   
   >sustained usage</a>
. When procurement leads developed workarounds, there was no one watching the signal and no one with a mandate to respond. The tool drifted.</p>
<h2 id="when-it-works">When It Works</h2>
<p>A different team at a professional services firm built something structurally simpler: a tool that drafted the engagement summary section of a client report, pulling from structured notes the consultant had already entered into their project management system. Narrow scope. No novel context required. One predictable output format, reviewed every time before it went anywhere.</p>
<p>The tool stuck. Not because it was more technically impressive — it was considerably less so. It stuck because it removed a specific, recurring task that consultants genuinely disliked, it used context they were already maintaining anyway, and the output was always human-reviewed before it mattered. The failure mode was visible and safe. The value was obvious the first time you used it and every time after.</p>
<p>The team lead reviewed usage weekly for the first two months and made three small adjustments based on what she saw. That ownership — unglamorous, persistent, post-launch — is what made the difference.</p>
<h2 id="the-structural-difference">The Structural Difference</h2>
<p>Both companies built  <a href="/blog/2025-08-04-ai-workflow-automation/"
   
   >AI tools for internal workflows</a>
. One failed quietly, one became a habit. The gap was not the model. It was not the interface. It was whether the tool was designed around how work actually moves or around  <a href="/blog/2026-05-05-measure-ai-progress-without-theater/"
   
   >what would look good in a demo</a>
.</p>
<p>Tools that survive are ones that fit a narrow, complete slice of a workflow, produce output that is either verifiable or bounded enough to trust, require no context the user does not already have, and have someone whose job it is to watch whether people are actually using them.</p>
<p>That last part is the one most teams skip. Usage is not a launch outcome. It is an operating responsibility.</p>
]]></content:encoded></item><item><title>Build the System the Model Cannot Break</title><link>https://lawzava.com/blog/2026-05-14-build-the-system-the-model-cannot-break/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-14-build-the-system-the-model-cannot-break/</guid><description>A manifesto for building AI-native organizations. Twelve tenets across strategy, architecture, economics, and people — and the only test that matters in year two.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>An AI-native company is not a company that uses AI. It is a company whose operating model — decisions, ownership, interfaces, capital, and failure boundaries — has been built so AI compounds inside it instead of evaporating around it.</p>
<p>The model will change. The system around it should not.</p>
<p>This is a manifesto. It is opinionated, deliberately. Twelve tenets, four movements, one test. Borrow what works. Argue with the rest.</p>
<hr>
<h1 id="movement-i--strategy">Movement I — Strategy</h1>
<h2 id="1-the-operating-model-is-the-strategy">1. The operating model is the strategy</h2>
<p>The model is the most expensive dependency in your stack. It is not the brain. The brain is everything you build around it: context assembly, retrieval, validation, retries, telemetry, fallback, escalation.</p>
<p>Two companies buy the same frontier model on the same Tuesday. One ships in six weeks with a deterministic fallback, a typed validator, and an eval gate on every PR. The other ships in six months with a notebook of &ldquo;good prompts&rdquo; and a Slack channel for incidents. Same model. Different company.</p>
<p>If your AI plan begins with &ldquo;which model should we buy,&rdquo; you are solving the easiest problem in the room. <strong>The moat is everything around the model.</strong></p>
<h2 id="2-capital-allocation-is-the-first-product-decision">2. Capital allocation is the first product decision</h2>
<p>Great AI teams do not start with a roadmap. They start with  <a href="/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/"
   
   >a kill list</a>
. Capital is finite. Attention is finite. Support burden is finite.</p>
<p>Three questions before any AI initiative gets funded:</p>
<ol>
<li>Does this increase <strong>margin</strong>, reduce <strong>risk</strong>, or improve <strong>speed</strong>?</li>
<li>Can we measure that effect within one to three quarters?</li>
<li>Do we own the <strong>fallback</strong> if the model or vendor changes?</li>
</ol>
<p>If the answer to all three is not yes, the default is no.</p>
<p>The most common pattern across Series B–D companies that quietly stalled in 2024–2025: somewhere between $1M and $3M of engineering and infra burned on internal copilots that never crossed adoption threshold, plus a duplicate prompt orchestration layer because two teams built one in parallel. Neither project had a measurable failure mode. Both had a sponsor.</p>
<p>A four-dimension scorecard makes the next budget meeting honest:</p>
<ul>
<li><strong>Adoption</strong> — are real users using it in a real workflow?</li>
<li><strong>Reliability</strong> — does it fail in bounded, observable ways?</li>
<li><strong>Margin</strong> — does it reduce cost or improve unit economics?</li>
<li><strong>Speed</strong> — does it shorten a real business cycle time?</li>
</ul>
<p><strong>If you cannot defend it with numbers, the project is not innovative. It is unpriced.</strong></p>
<h2 id="3-decision-latency-is-a-pl-variable">3. Decision latency is a P&amp;L variable</h2>
<p>Slow decisions look like caution. In practice, they are hidden expense. Every day a real decision sits unresolved, the business pays in delay, rework, and attention.</p>
<p>Headcount is an input.  <a href="/blog/2026-03-30-throughput-engineer-headcount-lagging-metric/"
   
   >Throughput is an outcome</a>
. Adding the tenth engineer to a system that takes nine days to approve a deploy adds nine more days of waiting, not 10% more output.</p>
<p>Track four numbers with the same seriousness as revenue:</p>
<ul>
<li>time from issue raised to decision made</li>
<li>time from decision made to action taken</li>
<li>escalations per decision class</li>
<li>decisions reopened after approval</li>
</ul>
<p><strong>Ambiguous ownership is the most expensive architecture in your company.</strong></p>
<hr>
<h1 id="movement-ii--architecture">Movement II — Architecture</h1>
<h2 id="4-build-firewalls-not-masterpieces">4. Build firewalls, not masterpieces</h2>
<p>A statistical engine cannot be expected to behave like deterministic infrastructure. If your architecture only works when the model is correct 100% of the time, it is not architecture. It is wishful thinking with a demo budget.</p>
<p>Three failure modes, three firewalls. They are not the same thing and they are not solved by the same code:</p>
<ul>
<li><strong>Inbound sanitization.</strong> What data is permitted into the prompt context. PII strippers, schema enforcers, retrieved-document trust scoring. This is also where indirect prompt injection — instructions hidden in a vendor PDF, a customer message, or a tool output — gets caught before it reaches the model.</li>
<li><strong>Outbound validation.</strong> A typed schema checker stands between the model and the operational database. Malformed JSON, out-of-range values, and policy-violating outputs are rejected at the boundary, not absorbed by downstream services.</li>
<li><strong>Operational fallback.</strong> Circuit breakers for vendor outages and rate limits. If the model returns invalid output three times in a row, the system degrades to a deterministic path — not a stack trace in front of the user.</li>
</ul>
<p>Each of these is a separate piece of code with a separate owner, a separate test surface, and a separate failure mode. A &ldquo;kill switch&rdquo; that catches all three is a slide, not a system.</p>
<p><strong>You cannot prompt your way out of entropy. You have to architect your way out of it.</strong></p>
<h2 id="5-evaluation-is-the-spine">5. Evaluation is the spine</h2>
<p>If you cannot define an eval suite before shipping a feature, you do not understand the system well enough to ship it.</p>
<p>A  <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >five-level maturity ladder</a>
:</p>
<ol>
<li><strong>Vibes-based.</strong> Someone eyeballs prompts before release.</li>
<li><strong>Spreadsheet.</strong> Suite exists, runs occasionally, blocks nothing.</li>
<li><strong>CI/CD-integrated.</strong> Evals run on every PR. A failed gate stays failed.</li>
<li><strong>Continuous telemetry.</strong> Production samples scored asynchronously. Incidents become regression tests.</li>
<li><strong>Governance as moat.</strong> Evaluation shapes architecture before code. Margin, latency, and sovereignty tradeoffs are quantified, not asserted.</li>
</ol>
<p>Below Level 3 is not a production system. It is a demo with a pager.</p>
<p>Level 4 is where most organizations get stuck, and the reason is rarely effort. Judge models drift, ground truth ages, sampling bias creeps in, and your asynchronous scoring quietly stops tracking the failure mode you cared about. Mature teams hold a small, hand-labeled golden set as the anchor, treat the judge model as a versioned dependency, and re-calibrate when either changes.</p>
<p>Eval portability is a year-two survival trait. If your eval suite is hand-tuned to one model&rsquo;s tokenizer and one vendor&rsquo;s output quirks, you have not built an eval suite. You have built a benchmark for the model you are about to be unable to leave.</p>
<h2 id="6-agentic-systems-run-on-a-reliability-contract">6. Agentic systems run on a reliability contract</h2>
<p>Agents are not magical workers. They are autonomous systems with more ways to fail. The reliability discipline gets stricter, not looser.</p>
<p>Every production agent answers five questions in one meeting, without hand-waving:</p>
<ul>
<li>what is it allowed to do?</li>
<li>what is it explicitly not allowed to do?</li>
<li>what metrics prove it is healthy?</li>
<li>what happens when the model degrades?</li>
<li>who can stop it, and how fast?</li>
</ul>
<p>But the five questions are a meeting checklist. The contract is a published artifact with <strong>SLOs, blast-radius caps in dollars or rows or API calls, rollback latency targets, and a named owner per failure mode.</strong> Blast radius is the real design variable: data scope, action scope, time scope, permission scope, fallback scope.</p>
<p>Kill switches are not weakness. They are governance that can move faster than the failure. A useful test of any AI control: <strong>could an engineer follow this rule at 2 a.m. without calling a committee?</strong></p>
<p>A roadmap that ships an agent without answers to these questions is a roadmap that has shipped a liability with a product label. Every initiative names how it turns off, how it knows it is hurting, how fast it reverts, and what manual path exists when the model degrades.</p>
<p><em>Companion:  <a href="/docs/agent-reliability-contract"
   
   >Agent Reliability Contract template</a>
.  <a href="/docs/rollback-template"
   
   >Rollback document template</a>
.</em></p>
<p><strong>Autonomy without a reliability contract is just an incident waiting for a timeline.</strong></p>
<hr>
<h1 id="movement-iii--economics--externals">Movement III — Economics &amp; Externals</h1>
<h2 id="7-unit-economics-live-at-the-workflow-not-the-model-call">7. Unit economics live at the workflow, not the model call</h2>
<p>Teams fixate on tokens because tokens are visible. The real bill sits around the model: retries, context assembly, human correction, support escalation, and the work of proving the output is acceptable.</p>
<p>Route by value and by risk. Trivial work stays cheap and local. High-stakes work earns expensive inference and stronger checks. A finance-aware leader can answer, without hand-waving:</p>
<ul>
<li>what each class of request costs to serve, end to end</li>
<li>where the rework happens</li>
<li>what failure costs when the model is wrong</li>
<li>which parts of the workflow justify premium inference</li>
</ul>
<p>The cost question nobody owns until it explodes: <strong>when product ships a feature that 10x&rsquo;s tokens, who pays?</strong> If the answer is &ldquo;we&rsquo;ll figure it out,&rdquo; you have not designed an operating model. You have deferred a fight.</p>
<p>Compute placement is part of this calculation, not a separate one. For high-frequency agentic workloads, a chain of round-trips across regions and vendors compounds into real latency tax and real egress cost. Local-first, hardware-aware patterns earn their place where the workload mix justifies them — and create a worse outcome where it does not. Measure first, place compute second.</p>
<p><strong>A cheaper model that fails gracefully beats an expensive model that fails silently.</strong></p>
<h2 id="8-sovereignty-is-an-architecture-constraint">8. Sovereignty is an architecture constraint</h2>
<p> <a href="/blog/2026-04-06-sovereign-systems-privacy-non-optional/"
   
   >Privacy is not a feature you bolt on</a>
 before an enterprise contract closes. It is the shape of the system.</p>
<p>A sovereign system controls the full lifecycle of every piece of data — where it lives, who can access it, how long it persists, and what happens when someone asks you to delete it. In practice, four concrete patterns:</p>
<ul>
<li><strong>Customer-managed keys.</strong> BYOK or hold-your-own-key. If your cloud provider holds the only copy of the encryption key, &ldquo;we cannot access your data&rdquo; is a policy promise, not a verifiable claim.</li>
<li><strong>Regional routing with storage isolation.</strong> EU data does not leave EU infrastructure. The application layer handles the routing. The deployment pipeline ships multi-region.</li>
<li><strong>Scoped, short-lived access.</strong> No ambient credentials. Service-to-service tokens with explicit grants and automatic expiry.</li>
<li><strong>Immutable audit trails.</strong> Append-only, tamper-evident logging of every access, transformation, and movement.</li>
</ul>
<p>&ldquo;We use AWS&rdquo; is not an answer to &ldquo;where does my data live.&rdquo; <strong>Sovereignty is about specificity.</strong></p>
<p>The compounding bill arrives when you try to add this later. The discount arrives when you build it in early and close enterprise contracts without an architectural retrofit.</p>
<h2 id="9-the-threat-model-is-the-manifesto">9. The threat model is the manifesto</h2>
<p>An AI manifesto without a threat model is marketing copy. Four risks every operator names explicitly:</p>
<ul>
<li><strong>Indirect prompt injection.</strong> Instructions hidden in retrieved documents, tool outputs, and user uploads — not just in the user&rsquo;s direct prompt. Treat every retrieved string as potentially adversarial. Validate before it reaches the model. Strip before it reaches the agent.</li>
<li><strong>Silent quality drift.</strong> The model returns <em>slightly</em> worse reasoning. The tone shifts. The retrieval starts ignoring critical documents. There is no stack trace. Only asynchronous production scoring, anchored to a golden set, catches this before customers do.</li>
<li><strong>Vendor and model lock-in by accident.</strong> Fine-tunes, preference data calibrated to one model family, and prompts hand-tuned to a specific tokenizer compound. By year two, your &ldquo;swappable&rdquo; model is a six-month migration. Discipline preserves optionality: prompt abstraction, eval portability, vendor-neutral preference data, and a quarterly review of what would break if the vendor changed terms tomorrow.</li>
<li><strong>Agent blast radius creep.</strong> Permissions accumulate. The agent that summarizes documents quietly gains write access to your billing API because someone needed it once. Audit scope quarterly. Treat agent permissions like database credentials, not like configuration.</li>
</ul>
<p>Threat modeling is not a one-time exercise. It is the bill of materials your system runs on.</p>
<hr>
<h1 id="movement-iv--people--failure">Movement IV — People &amp; Failure</h1>
<h2 id="10-interfaces-beat-titles">10. Interfaces beat titles</h2>
<p>Most AI hiring plans try to fix an interface problem with resumes. They rarely work.</p>
<p>A working leadership system is not a roster of senior titles. It is a decision map. Four owners with explicit decision rights and explicit escalation paths:</p>
<ul>
<li><strong>Product</strong> — user outcomes, adoption, business tradeoffs.</li>
<li><strong>Platform</strong> — safe defaults, deployment paths, observability, paved roads.</li>
<li><strong>Applied AI</strong> — workflow behavior, routing, prompting, retrieval, evaluation quality.</li>
<li><strong>Governance</strong> — risk boundaries, sovereignty controls, escalation thresholds.</li>
</ul>
<p>The titles can be anything. The interfaces cannot be ambiguous. If the answers depend on who is online that day, the system is not operational.</p>
<p>The same logic governs platform teams. A platform exists to make repeated decisions disappear into the default path — identity, routing, eval harnesses, logging, safe deployment, fallback behavior. The moment platform becomes a queue that has to bless every use case,  <a href="/blog/2026-05-14-why-ai-platform-teams-become-bottlenecks/"
   
   >the queue is the product</a>
 and waiting is the cost. <strong>A platform should remove waiting, not become a waiting room.</strong></p>
<p>Hiring works after the operating contract is clear, not before. New hires scale the current operating model, good or bad. <strong>Org debt is interface debt with better branding.</strong></p>
<h2 id="11-anti-fragility-requires-portability-discipline">11. Anti-fragility requires portability discipline</h2>
<p>Resilience is surviving the shock. Anti-fragility is using the shock to remove the next one.</p>
<p>Fragility hides in the org chart and in the stack. One engineer who knows the routing. One vendor whose terms changed last week. One fine-tune that took six months to train and would take six months to migrate. That is not an organization or a system. That is a single point of failure wearing a department badge or a model card.</p>
<p>Four design choices build strength:</p>
<ul>
<li><strong>Modular ownership.</strong> No critical function depends on one person&rsquo;s memory. Deputies are named.</li>
<li><strong>Resettable interfaces.</strong> A model, vendor, or workflow can be swapped without a rewrite. This is not free. It requires prompt abstraction, eval portability, vendor-neutral preference data, and a regular drill where the team actually proves a swap is possible.</li>
<li><strong>Fast learning loops.</strong> Every failure produces a tighter eval, a better fallback, or a clearer operating boundary.</li>
<li><strong>Cross-training on the boring parts.</strong> Alerts, evals, fallback logic, access boundaries. The unglamorous work is what keeps the organization elastic.</li>
</ul>
<p>A short anti-fragility check:</p>
<ul>
<li>Can you swap a model without rewriting the product?</li>
<li>Can you lose a key engineer without losing the system?</li>
<li>Can you absorb a vendor price increase without panic?</li>
<li>Can you turn a production incident into an improved control?</li>
</ul>
<p>If any answer is no, the organization is more brittle than it thinks. The most expensive lie an AI organization tells itself is that the model is swappable when nobody has tried.</p>
<h2 id="12-the-year-two-test">12. The year-two test</h2>
<p>A lot of AI organizations look healthy in month three and brittle by year two. The model did not fail. The operating model did. Prototype energy is easy to create. Durable coordination is not.</p>
<p>The single question that separates the two:</p>
<blockquote>
<p>Can the AI system survive a senior person going on vacation for two weeks?</p>
</blockquote>
<p>If the answer is &ldquo;not really,&rdquo; the organization is still running on hidden tribal knowledge.</p>
<p>If the answer is &ldquo;yes, with documented ownership, a published reliability contract, an eval suite that blocks releases, and a fallback path the on-call engineer can execute at 2 a.m.,&rdquo; the company is moving from prototype to production.</p>
<p>That is the only year-two test that matters. Everything else in this manifesto is in service of passing it.</p>
<hr>
<h2 id="what-this-manifesto-is-not">What this manifesto is not</h2>
<p>It is not a prediction about which model wins. It is not a framework for replacing engineers with agents. It is not a defense of any vendor, any cloud, or any stack.</p>
<p>It is a statement about how serious companies organize for AI when the easy money, the demo budgets, and the hype cycles are done — and only the operating model is left to do the work.</p>
<p>The model will change.</p>
<p>The system around it should not.</p>
<hr>
<p><em>Law Zava writes about the operating model behind serious AI execution. Companion artifacts:  <a href="/docs/agent-reliability-contract"
   
   >Agent Reliability Contract template</a>
 ·  <a href="/docs/rollback-template"
   
   >Rollback document template</a>
 ·  <a href="/docs/eval-starter-kit"
   
   >Eval Suite starter kit</a>
. The canonical reading path is at  <a href="/blog"
   
   >/blog</a>
.</em></p>
]]></content:encoded></item><item><title>Why Most AI Platform Teams Become the New Bottleneck</title><link>https://lawzava.com/blog/2026-05-14-why-ai-platform-teams-become-bottlenecks/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-14-why-ai-platform-teams-become-bottlenecks/</guid><description>AI platform teams fail when they centralize decisions instead of capabilities. The queue is the bug.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI platform teams become bottlenecks when they start reviewing every use case instead of shipping safe defaults. Once the team needs a ticket to approve basic work, the queue is the product and the platform is just a delay with a nicer name.</p>
<p>The answer is not to shrink the team and hope demand goes away. It is to move decisions out of the queue and into the platform.</p>
<h2 id="a-platform-team-is-a-product-with-a-queue">A Platform Team Is a Product with a Queue</h2>
<p>A healthy  <a href="/blog/2017-12-28-building-platform-teams/"
   
   >platform team</a>
 exists to make repeated decisions disappear.</p>
<p>If every experiment needs a ticket, a Slack ping, and a weekly exception review, the platform is no longer a platform. It is a gate with a service catalog.</p>
<p>The warning signs show up fast:</p>
<ul>
<li>request backlogs that never get smaller</li>
<li>the same exception coming back under a new name</li>
<li>engineers building shadow infrastructure because the official path is too slow</li>
<li>work that should have been standardized long ago still handled by hand</li>
</ul>
<p>Once teams start routing around the platform, the default path has already lost.</p>
<h2 id="what-bottleneck-behavior-looks-like">What Bottleneck Behavior Looks Like</h2>
<p>Bottlenecks rarely announce themselves. They sound like process.</p>
<p>You hear it in the same lines over and over:</p>
<ul>
<li>“We are waiting on the platform team.”</li>
<li>“Can we make this an exception?”</li>
<li>“We built a small internal workaround.”</li>
<li>“The platform is a few weeks behind us.”</li>
</ul>
<p>None of those lines is fatal on its own. The pattern becomes a problem when they turn into the normal way work gets done.</p>
<p>A platform team becomes a bottleneck when it centralizes decisions that should have been made once, written down, and pushed into the default path.</p>
<h2 id="redesign-the-team-around-capabilities-not-control">Redesign the Team Around Capabilities, Not Control</h2>
<p>Good platform teams build  <a href="/blog/2019-03-11-building-internal-developer-platforms/"
   
   >paved roads</a>
.</p>
<p>They own the hard parts once:</p>
<ul>
<li>identity and access patterns</li>
<li> <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >model routing defaults</a>
</li>
<li> <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation harnesses</a>
</li>
<li>logging and traceability</li>
<li>safe deployment templates</li>
<li>fallback behavior</li>
</ul>
<p>Then they get out of the way.</p>
<p>The wrong shape is a team that has to bless every new use case. The right shape is a team that makes the safe path easier than the unsafe one.</p>
<p>A good test: <strong>a platform team should  <a href="/blog/2026-05-14-build-the-system-the-model-cannot-break/"
   
   >remove waiting, not become a waiting room</a>
.</strong></p>
<h2 id="the-metrics-that-reveal-the-truth">The Metrics That Reveal the Truth</h2>
<p>Most platform dashboards avoid the real question. You need blunt metrics.</p>
<p>Measure:</p>
<ul>
<li>time from request to usable platform support</li>
<li>exceptions granted per month</li>
<li>shadow systems discovered in production</li>
<li>hours spent waiting on platform review</li>
<li>AI workflows shipped without platform involvement</li>
</ul>
<p>Those metrics tell you whether the platform is compounding or constraining.</p>
<p>If exceptions keep rising and the team calls that “flexibility,” the default path is still too hard to use.</p>
<h2 id="what-good-looks-like">What Good Looks Like</h2>
<p>The best AI platform teams I have seen share three habits:</p>
<ol>
<li>They bias toward self-service.</li>
<li>They make safe defaults boring.</li>
<li>They track the cost of waiting as carefully as the cost of infrastructure.</li>
</ol>
<p>That last one matters. Waiting is not free. Every hour a product team spends blocked on the platform is an hour not spent learning from users.</p>
<p>A good platform team does more than improve developer experience. It improves business velocity.</p>
]]></content:encoded></item><item><title>The CTO Communication Protocol: Aligning Engineers, Executives, and Investors in AI Programs</title><link>https://lawzava.com/blog/2026-05-12-cto-communication-protocol-ai-programs/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-12-cto-communication-protocol-ai-programs/</guid><description>AI programs fail when each layer hears a different success definition.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI programs rarely fail because one team is incompetent. They fail because the organization tells itself three different stories about the same system. Engineers hear one version of reliability, executives hear one version of commercial impact, and investors hear one version of scale. By the time those stories collide in a board meeting, the disagreement has already been baked into the program.  <a href="/blog/2026-04-14-ai-cto-perspective/"
   
   >A CTO&rsquo;s job</a>
 is to keep the story true enough that people can act on it.</p>
<h2 id="the-alignment-problem">The Alignment Problem</h2>
<p>Every layer in a company listens for a different failure.</p>
<p>Engineers ask: can we make it reliable without turning the stack into a science project?</p>
<p>Executives ask: can it matter this quarter, not someday?</p>
<p>Investors ask: can it scale without becoming a support burden, a security problem, or a  <a href="/blog/2026-04-28-margin-risk-speed-ai-strategy-metrics/"
   
   >margin leak</a>
?</p>
<p>If those questions are not coordinated, the organization drifts into avoidable conflict. Product thinks it shipped success. Engineering thinks it shipped risk. Finance thinks it shipped cost. The AI program becomes a political object instead of an operating system.</p>
<h2 id="what-each-layer-needs-to-hear">What Each Layer Needs to Hear</h2>
<p>A good communication protocol gives each audience the right level of detail and nothing more.</p>
<p><strong>Engineers</strong> need constraints, failure modes, ownership, and the exact conditions under which they should stop or escalate.</p>
<p><strong>Executives</strong> need the business outcome, the tradeoffs, the cost of delay, and the risk of waiting for a perfect answer.</p>
<p><strong>Investors or board members</strong> need the thesis, the numbers, the confidence interval around those numbers, and the reason the company believes the numbers are real.</p>
<p>The common mistake is predictable: over-share implementation detail upward and under-share operational reality downward. Leaders either talk past each other or sand off the complexity to keep the room calm. Neither habit helps. Clarity is kinder than politeness when the system is expensive.</p>
<h2 id="build-a-communication-rhythm">Build a Communication Rhythm</h2>
<p>Strong CTOs do not improvise every update. They set a rhythm that forces the same narrative to appear at predictable intervals, so the organization can spot drift before it becomes a surprise.</p>
<p>A practical cadence looks like this:</p>
<ul>
<li>weekly: operational progress, blockers, decisions made, decisions deferred</li>
<li>monthly:  <a href="/blog/2026-05-05-measure-ai-progress-without-theater/"
   
   >outcome metrics</a>
, risk posture, and what changed in the operating assumptions</li>
<li>quarterly: strategy shifts, tradeoffs, roadmap changes, and what the board should expect next</li>
</ul>
<p>That structure gives the organization memory and gives the board a clean way to compare this quarter with the last one.</p>
<p>The point is not to produce more slides. The point is to keep the story consistent enough that people can challenge it honestly.</p>
<p>Misaligned narratives are delayed incidents.</p>
<h2 id="use-the-same-three-questions-everywhere">Use the Same Three Questions Everywhere</h2>
<p>Keep asking the same three questions in every forum: what changed, what did it affect, and what happens next? Those questions work at the team level, the executive level, and the board level because they force the same discipline: outcome, consequence, next move. If a layer cannot answer them, the communication is not yet useful.</p>
<p>Alignment is not consensus. It is a shared operating picture.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>AI programs fail when each audience hears a different success definition.</li>
<li>Engineers, executives, and investors need different levels of detail, but they need the same core truth.</li>
<li>Use a consistent communication rhythm so the story does not change every time the room changes.</li>
<li>Keep asking what changed, what it affected, and what happens next until the answer is sharp enough to survive board scrutiny.</li>
</ul>
]]></content:encoded></item><item><title>AI Governance Without Bureaucracy</title><link>https://lawzava.com/blog/2026-05-07-ai-governance-without-bureaucracy/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-07-ai-governance-without-bureaucracy/</guid><description>Effective AI governance is tighter defaults, clearer ownership, and faster escalation — not more committees.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Good AI governance does not look busy. It looks boring: tighter defaults, named owners, and fast escalation paths. If governance slows safe work and never stops unsafe work, it is bureaucracy with a policy memo attached.</p>
<h2 id="the-governance-mistake">The Governance Mistake</h2>
<p>Most organizations confuse governance with oversight theater.</p>
<p>They create committees, review boards, and approval layers, then act surprised when teams route around them. The result is predictable: slow delivery, hidden risk, and a false sense of control.</p>
<p> <a href="/blog/2025-03-03-ai-governance-practice/"
   
   >AI governance</a>
 should answer a simpler question: what is allowed by default, what requires review, and what is forbidden?</p>
<p>If those boundaries are clear, teams can move. If they are not, every decision becomes a negotiation.</p>
<h2 id="tight-defaults-beat-loose-rules">Tight Defaults Beat Loose Rules</h2>
<p>Good governance systems do not ask engineers to remember every policy. They make the safe path the easy path.</p>
<p>That means:</p>
<ul>
<li>default data access is scoped, not ambient</li>
<li>model use is tied to approved workflows</li>
<li>logs retain enough context to investigate failures</li>
<li>high-risk actions require explicit escalation</li>
<li>evals run before release, not after incident review</li>
</ul>
<p>Governance works when it compresses uncertainty. It fails when it only adds paperwork.</p>
<p>A useful test: <strong>could an engineer follow the rule at 2 a.m. without calling a committee?</strong> If not, the rule is too vague or too heavy.</p>
<h2 id="ownership-matters-more-than-policy">Ownership Matters More Than Policy</h2>
<p>The fastest way to break governance is to make it everyone’s job.</p>
<p>Real governance needs named owners for:</p>
<ul>
<li> <a href="/blog/2025-09-15-ai-data-privacy/"
   
   >data classification</a>
</li>
<li>model approval</li>
<li> <a href="/blog/2026-04-23-ai-evaluation-maturity/"
   
   >evaluation coverage</a>
</li>
<li>exception handling</li>
<li> <a href="/blog/2025-11-10-ai-incident-management/"
   
   >incident response</a>
</li>
</ul>
<p>Without ownership, governance becomes a shared belief system. Shared belief systems feel flexible until something breaks.</p>
<p>The people who matter most are not the ones writing the longest policy. They are the ones who can answer: who decides, who reviews, and how fast can we change course?</p>
<h2 id="build-the-smallest-control-stack-that-works">Build the Smallest Control Stack That Works</h2>
<p>You do not need 30 controls to govern AI well. You need the smallest control stack that actually changes behavior.</p>
<p>Start with:</p>
<ol>
<li>a short list of approved data classes</li>
<li>a clear model use policy by workflow</li>
<li>required evals for release</li>
<li>a lightweight exception path</li>
<li>an incident review process that changes architecture, not just slides</li>
</ol>
<p>If you can keep that stack small, understandable, and enforced, you will get more compliance and less resistance.</p>
<p>A line worth keeping: <strong>the best control is the one engineers can still use at 2 a.m.</strong></p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Governance should compress uncertainty, not create bureaucracy.</li>
<li>Use tighter defaults and named ownership.</li>
<li>Keep the control stack small enough to operate.</li>
<li>If the policy cannot survive real work, it is not governance; it is paperwork.</li>
</ul>
]]></content:encoded></item><item><title>The Board Deck Is Lying: How to Measure AI Progress Without Theater</title><link>https://lawzava.com/blog/2026-05-05-measure-ai-progress-without-theater/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-05-05-measure-ai-progress-without-theater/</guid><description>Most AI progress reporting confuses activity with value. Executive measurement should collapse around adoption, reliability, margin, and delivery speed.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most AI dashboards count motion, not progress. They record pilots, prompts, and meetings, then call that momentum. If the scorecard cannot show adoption, reliability, margin, or cycle-time improvement, it is a prop. A board should be able to read it and know whether the business is better off.</p>
<h2 id="the-theater-problem">The Theater Problem</h2>
<p>AI reporting drifts toward  <a href="/blog/2022-10-17-engineering-metrics-that-matter/"
   
   >vanity metrics</a>
 because vanity metrics are easy to collect and hard to argue with.</p>
<p>The usual suspects:</p>
<ul>
<li>number of pilots launched</li>
<li>number of prompts written</li>
<li>number of models tested</li>
<li>number of meetings held</li>
<li>number of slides in the board update</li>
</ul>
<p>None of those is useless on its own. The problem is that none of them answers the only question that matters: <strong>what improved because we shipped this?</strong></p>
<h2 id="a-better-executive-scorecard">A Better Executive Scorecard</h2>
<p>A serious AI scorecard should be small enough to remember and strong enough to force a decision.</p>
<p>Start with four dimensions:</p>
<ol>
<li><strong>Adoption</strong> — are real users using it in a real workflow?</li>
<li><strong>Reliability</strong> — does it fail in bounded, observable ways?</li>
<li><strong>Margin</strong> — does it reduce cost or improve unit economics?</li>
<li><strong>Speed</strong> — does it shorten a real business cycle time?</li>
</ol>
<p>If a project does not move at least one of those numbers, it is not strategic. It is a lab exercise with a budget.</p>
<p>The point is not to build a perfect dashboard. The point is to make it impossible to hide weak outcomes behind busy activity.</p>
<h2 id="what-to-report-weekly">What to Report Weekly</h2>
<p>A weekly AI review should be short, blunt, and decision-oriented.</p>
<p>Report:</p>
<ul>
<li>what shipped</li>
<li>what users actually did with it</li>
<li>what broke</li>
<li>what it cost</li>
<li>what decision changed because of the data</li>
</ul>
<p>That last bullet matters. Progress reporting without decisions is performance art.</p>
<p>A team can launch five experiments in a week and still have no strategy. Strategy shows up when the evidence sharpens the next choice.</p>
<h2 id="keep-the-dashboard-honest">Keep the Dashboard Honest</h2>
<p>There are two reliable ways AI dashboards lie.</p>
<p>First, they drift toward lagging metrics only. By the time the board sees the number, the product problem is already old.</p>
<p>Second, they reward volume instead of signal. A busy roadmap can still be a weak roadmap.</p>
<p>Keep the dashboard honest by requiring every metric on the top page to map to one of  <a href="/blog/2026-04-28-margin-risk-speed-ai-strategy-metrics/"
   
   >three board outcomes</a>
:</p>
<ul>
<li>margin expansion</li>
<li>risk compression</li>
<li>execution-speed advantage</li>
</ul>
<p>If a metric does not help the board understand at least one of those outcomes, it belongs lower in the stack or not at all.</p>
<p>A line worth keeping: <strong>if the scorecard cannot survive finance review, it is not strategy.</strong></p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Measure adoption, reliability, margin, and speed.</li>
<li>Weekly reviews should force decisions, not decorate slides.</li>
<li>Tie every visible metric to margin, risk, or execution speed.</li>
<li>If the dashboard cannot survive finance review, move it off the first page.</li>
</ul>
]]></content:encoded></item><item><title>The 2026 AI Build vs. Buy Calculus (It’s Just Operational Cost)</title><link>https://lawzava.com/blog/2026-04-30-ai-build-vs-buy/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-30-ai-build-vs-buy/</guid><description>By mid-2026, AI build vs buy has nothing to do with novelty. It is a ruthless mathematical calculation of telemetry, context freshness, and infrastructure lock-in.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>In 2026, build vs. buy is not a taste question. It is an operational cost question. Are you prepared to own the telemetry, the fallback paths, and the failure modes that come with the stack? Buying gives you speed and leaves the analytics with someone else. Building gives you control and hands you the overhead.</p>
<h2 id="the-myth-of-the-headline-price">The Myth of the Headline Price</h2>
<p>Most teams compare API pricing to GPU rental and stop there. That is the wrong first-order model.</p>
<p>Token price is the easiest number to quote and the least useful number to trust. The real bill shows up in the work around the model:</p>
<ul>
<li><strong>Telemetry &amp; Evals:</strong> If you self-host, you must build the pipeline that captures, scores, and reviews output. Vendor APIs may bundle some of this, but then they also own the metadata.</li>
<li><strong>Graceful Degradation:</strong> When the provider throttles you at peak, do you have local fallback? Hybrid systems buy resilience, but they also add systems-engineering work.</li>
<li><strong> <a href="/blog/2026-04-06-sovereign-systems-privacy-non-optional/"
   
   >Data Sovereignty</a>
:</strong> Sometimes the reason to build is simple: the data cannot legally leave your VPC. Once that is true, the token price stops mattering.</li>
</ul>
<h2 id="when-to-buy-the-commodity-highway">When to Buy (The Commodity Highway)</h2>
<p>Buy when the AI capability is a feature, not the product.</p>
<p>If you are building an internal documentation chatbot, a support-ticket summarizer, or a semantic search overlay, buy the API. Do not spend engineering throughput standing up vLLM instances and chasing KV-cache optimizations for a problem that is not your moat.</p>
<p>The catch is lock-in at the integration layer. If your code imports vendor-specific classes directly, you will feel the squeeze when prices change or a model line is deprecated.  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >Keep the provider behind an internal interface</a>
.</p>
<h2 id="when-to-build-the-crucible-of-control">When to Build (The Crucible of Control)</h2>
<p>Build when AI sits inside unit economics or inside a hard trust boundary.</p>
<p>You must build if:</p>
<ol>
<li>Your margins depend on it. Billions of tokens a day can make  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >the API tax</a>
 the difference between a healthy product and a broken one.</li>
<li>You operate under zero-trust or residency constraints. In healthcare, finance, or defense, the data cannot touch a multi-tenant cloud edge.</li>
<li>You need hardware-level optimization. Sub-150ms tail latency usually means quantization, attention fusion, and serious control over the runtime.</li>
</ol>
<p>That is the part teams underestimate. You are no longer building a prompt pipeline. You are operating a distributed, heavily constrained state machine. That takes engineers who understand memory bandwidth, not just prompting.</p>
<h2 id="the-hybrid-default">The Hybrid Default</h2>
<p>The mature pattern in 2026 is a barbell.</p>
<p>Buy frontier models for complex reasoning, planning, and high-context zero-shot tasks. Build or host  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >quantized, heavily tuned 8B models</a>
 for the large volume of routing, formatting, and classification work that sits underneath the product.</p>
<p>The CTO&rsquo;s job is not to choose a camp. It is to make the handoff between buy and build a config change, not a rewrite.</p>
]]></content:encoded></item><item><title>Margin, Risk, and Speed: The Three Numbers That Should Drive AI Strategy</title><link>https://lawzava.com/blog/2026-04-28-margin-risk-speed-ai-strategy-metrics/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-28-margin-risk-speed-ai-strategy-metrics/</guid><description>Most AI strategy becomes clearer when leadership stops tracking novelty and starts forcing every decision through three numbers.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most AI strategy decks are full of nouns and short on numbers. That is usually the tell. If a project cannot move margin, reduce risk, or shorten the path to an outcome, it is not strategy. It is activity with a steering committee.</p>
<h2 id="why-three-numbers-are-enough">Why Three Numbers Are Enough</h2>
<p>Leaders overcomplicate AI strategy because they do not want to choose.</p>
<p>But every AI decision eventually lands in one of three buckets:</p>
<ul>
<li><strong>Margin</strong> — does it improve unit economics?</li>
<li><strong>Risk</strong> — does it make the system safer or more controllable?</li>
<li><strong>Speed</strong> — does it shorten the path from decision to outcome?</li>
</ul>
<p>That is the executive frame. Everything else supports it.</p>
<p>If a project cannot clearly improve at least one of those numbers, it does not belong near the top of the roadmap.</p>
<h2 id="the-trap-of-novelty-metrics">The Trap of Novelty Metrics</h2>
<p>AI teams love the wrong metrics because the wrong metrics are easy to count.</p>
<p>Number of models tested. Number of pilots launched. Number of prompts written. Number of demos shown. Number of meetings held.</p>
<p>Those numbers can tell you whether work is happening. They do not tell you whether the company is getting more profitable, less exposed, or faster to act.</p>
<h2 id="build-a-scorecard-around-outcomes">Build a Scorecard Around Outcomes</h2>
<p>A serious AI scorecard is short.</p>
<ol>
<li>Did margin improve?</li>
<li>Did risk go down?</li>
<li>Did  <a href="/blog/2026-03-30-throughput-engineer-headcount-lagging-metric/"
   
   >cycle time</a>
 shorten?</li>
</ol>
<p>Everything else is instrumentation that helps answer those questions.</p>
<p>That does not mean you ignore adoption, reliability, or cost. It means you use them as inputs to the three executive numbers, not as substitutes for them.</p>
<p>The strongest boards and founders do not need twenty metrics. They need a few numbers that are hard to fake.</p>
<h2 id="make-the-three-numbers-operational">Make the Three Numbers Operational</h2>
<p>The framework only works if the numbers are real.</p>
<p>For each AI initiative, define:</p>
<ul>
<li>the baseline</li>
<li>the target</li>
<li>the measurement cadence</li>
<li>the owner</li>
<li>the rollback path if the numbers move the wrong way</li>
</ul>
<p>That keeps the conversation concrete and makes the project accountable.</p>
<p>A line worth keeping: <strong>if a strategy cannot change one of the three numbers, it is probably theater.</strong></p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li> <a href="/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/"
   
   >Margin, risk, and speed</a>
 are enough to evaluate AI strategy.</li>
<li>Stop reporting novelty metrics as if they were outcomes.</li>
<li>Give every project a baseline, target, owner, cadence, and rollback path.</li>
<li>If the work does not change the numbers, the work is not strategic.</li>
</ul>
]]></content:encoded></item><item><title>AI Production Governance: A Maturity Model</title><link>https://lawzava.com/blog/2026-04-23-ai-evaluation-maturity/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-23-ai-evaluation-maturity/</guid><description>The gap between stable AI features and shipping chaos isn&amp;amp;rsquo;t tools—it&amp;amp;rsquo;s production governance. How mature teams evaluate, deploy, and roll back.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most AI teams do not have a model problem. They have a control problem. The gap between stable production AI and production chaos is usually governance: small trusted evals, release gates that actually block, and rollback paths that fire before users feel the drift. If you cannot explain how a change is tested, approved, and reversed, you do not have a production system. You have a demo with a pager.</p>
<h2 id="the-governance-maturity-model">The Governance Maturity Model</h2>
<h3 id="level-1-vibes-based-deployment">Level 1: &ldquo;Vibes-Based&rdquo; Deployment</h3>
<p>Evaluation is manual, episodic, and easy to ignore. Someone checks the prompts when there is time, ships the change, and waits for users to find the regression.</p>
<p>You can tell you are at Level 1 when the answer to &ldquo;How do you know yesterday&rsquo;s model swap was safe?&rdquo; is a shrug, a few sample prompts, or &ldquo;it looked fine.&rdquo; There is no baseline. There is no history. There is only whatever the latest person happened to test.</p>
<p>The failure mode is silent degradation. The model changes, behavior drifts, and the team learns about it weeks later from an angry customer or a support escalation that should never have reached production.</p>
<h3 id="level-2-the-spreadsheet-era">Level 2: The &ldquo;Spreadsheet&rdquo; Era</h3>
<p>There is an  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >eval suite</a>
, but it lives beside the delivery process instead of inside it. Someone runs a small Python script over a fixed list of cases before a big release and calls that &ldquo;testing.&rdquo;</p>
<p>Level 2 teams understand that evaluation matters, but they still treat it like a chore. The suite covers happy-path prompts and misses the things that actually break systems: adversarial inputs, schema violations, prompt injection, PII leakage. And because the results are not wired into release decisions, a bad run usually gets waved through anyway.</p>
<p>The failure mode is false confidence. The team trusts a narrow test set because it exists, not because it is representative. Then a multi-turn attack, a bad schema shift, or a quiet regression makes the gap obvious in production.</p>
<h3 id="level-3-cicd-integration-the-minimum-operational-bar">Level 3: CI/CD Integration (The Minimum Operational Bar)</h3>
<p>Evaluation is part of the delivery pipeline. The suite is broad enough to cover core capabilities and common failure modes, and the results block release candidates when they miss the bar.</p>
<p>At Level 3, every PR or deployment candidate runs the eval suite automatically. The checks include latency, cost per token, output schema validity, and the core reasoning path your product depends on. Results show up in CI next to unit tests. A failed gate stays failed until someone writes the exception and owns the risk.</p>
<p>This is the minimum bar for an enterprise team. A vendor can release an &ldquo;improved&rdquo; model on Tuesday, and a Level 3 team can run the suite on Wednesday morning and decide, with evidence, whether the new model actually helps their workload.</p>
<h3 id="level-4-continuous-production-telemetry">Level 4: Continuous Production Telemetry</h3>
<p>Evaluation does not stop when code ships. The system  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >keeps watching in production</a>
 and turns incidents into future tests.</p>
<p>At Level 4, an asynchronous sampling job pulls 5% of production responses, scores them with a cheaper model or other fast evaluator, and flags anomalies. When something goes wrong, the exact input/output pair that caused it becomes a regression test. The system assumes drift is normal, because with LLMs, it is.</p>
<h3 id="level-5-governance-as-a-strategic-moat">Level 5: Governance as a Strategic Moat</h3>
<p>Evaluation shapes architecture before code is written. Quality and privacy are not afterthoughts; they are constraints that drive the design.</p>
<p>At Level 5, the team knows how much reasoning quality they give up if they move traffic from a large cloud API to a quantized local 8B model, because they have the metrics to prove it. That gives the CTO real room to choose between margin, latency, and data sovereignty. It also lets the company close larger enterprise deals because it can show, in operational terms, where customer data lives and where it does not.</p>
<h2 id="how-to-force-maturity">How to Force Maturity</h2>
<p>If you are leading a team stuck at Level 1 or 2, you will not buy your way out with a new tool. You have to change how releases work.</p>
<ol>
<li><strong>Stop accepting demos.</strong> Do not ship the next feature unless it includes a 20-case eval suite attached to the PR.</li>
<li><strong>Wire it to CI.</strong> If evaluation does not block the deploy, it is a suggestion, not a control.</li>
<li><strong>Build circuit breakers.</strong> Treat the model like a flaky dependency. If it fails to return valid JSON three times, fall back to a deterministic system or fail safely. Do not hand hallucinations to the user and call that progress.</li>
</ol>
<p>Mature teams do not treat AI as magic. They treat it like a volatile operational dependency that has to be contained, measured, and rolled back fast.</p>
]]></content:encoded></item><item><title>Why Most Enterprise AI Architecture Fails in Year One</title><link>https://lawzava.com/blog/2026-04-21-enterprise-ai-architecture-fails/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-21-enterprise-ai-architecture-fails/</guid><description>In 2026, enterprise AI isn&amp;amp;rsquo;t failing because models are bad. It is failing because organizations are building brittle demos instead of bounded, operable systems.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Enterprise AI projects fail in their first year for a simple reason: teams ask a statistical engine to behave like deterministic infrastructure. If your architecture only works when the model is correct 100% of the time, it is not architecture. It is wishful thinking with a demo budget.</p>
<p>By mid-2026, the honeymoon phase of GenAI is over. Executives want ROI, and engineering organizations are staring at cloud bills, silent degradations, and brittle integration layers. The root cause is almost always the same: teams built highly optimized demos instead of heavily constrained, operable systems.</p>
<h2 id="the-fiction-of-the-flawless-prompt">The Fiction of the Flawless Prompt</h2>
<p>The most destructive belief in enterprise AI architecture is that the LLM is a magical function: put string in, get business outcome out.</p>
<p>When a demo works 95% of the time in a Jupyter notebook, product owners assume the remaining 5% is a prompt engineering problem. It is not. It is entropy.</p>
<p>You cannot prompt your way out of entropy. You have to architect your way out of it.</p>
<h2 id="defining-failure-boundaries">Defining Failure Boundaries</h2>
<p>If a traditional distributed database like ScyllaDB or Cassandra fails to return a row, the application does not simply crash with a stack trace visible to the user. It degrades gracefully. It falls back to a cache, a static default, or an asynchronous queue.</p>
<p>Enterprise AI architecture routinely lacks those boundaries. The model hallucinates a malformed JSON object, and the downstream system ingests it directly, corrupting application state.</p>
<p><strong>Mature architecture enforces strict boundaries:</strong></p>
<ul>
<li><strong>Inbound:</strong> What data is strictly permitted to enter the prompt context? Do you have PII strippers actively defending the edge?</li>
<li><strong>Outbound:</strong> Does the LLM communicate directly with the operational database, or does it write to an intermediate queue that is validated by a deterministic, typed schema checker before the transaction commits?</li>
</ul>
<p>If your architecture allows the model to act unilaterally without a deterministic validator acting as a bouncer, production failure is not a surprise. It is the expected outcome.</p>
<h2 id="the-missing-telemetry-layer">The Missing Telemetry Layer</h2>
<p>When an older microservice begins leaking memory, Ops teams see the P99 latency spike in Datadog and roll back the deployment.</p>
<p>When an LLM begins to silently degrade—perhaps because the vendor aggressively quantized its backend to save on compute—there is no stack trace. The model simply returns <em>slightly</em> worse reasoning. The tone shifts. The RAG retrieval starts ignoring critical documents.</p>
<p>Most enterprise builds fail because they have zero  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >telemetry to detect this drift</a>
. They ship the feature and assume it will perform equally well forever.</p>
<p>Robust systems do not trust models. They probe them. They sample 5% of all production outputs and score them asynchronously. They run hundreds of  <a href="/blog/2024-08-19-llm-testing-strategies/"
   
   >unit tests against the prompt pipeline</a>
 with every deployment. They treat the LLM as a hostile dependency that must continually prove its competence.</p>
<h2 id="build-firewalls-not-masterpieces">Build Firewalls, Not Masterpieces</h2>
<p>The winning architectures in 2026 are not the most complex. They are the most defensive.</p>
<p>They use small, fast, highly specialized models for routing. They enforce rigid,  <a href="/blog/2024-04-29-structured-output-patterns/"
   
   >typed output schemas</a>
. They degrade to entirely non-AI, algorithmic fallbacks the moment latency spikes or a validation check fails.</p>
<p>Stop trying to build a perfect AI. Start building architecture that survives when the AI inevitably acts stupid.</p>
]]></content:encoded></item><item><title>AI Capital Allocation: What Great CTOs Stop Funding First</title><link>https://lawzava.com/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/</link><pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-16-ai-capital-allocation-what-to-stop-funding/</guid><description>Strong AI strategy starts with a kill list. If a project cannot defend margin, risk, or speed, it should not survive the next budget meeting.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Great AI teams do not start with a roadmap. They start with a kill list. If a project cannot defend margin, risk, or speed, it does not deserve the next budget cycle. Capital is finite. Attention is finite. Support burden is finite.</p>
<p>The real mistake most companies make is treating AI spend as a separate class of spend. It is not. It competes with product work, platform work, hiring, and operational debt. If you cannot explain why an AI initiative deserves scarce capital, you are not allocating capital. You are subsidizing hope.</p>
<h2 id="capital-allocation-is-the-first-product-decision">Capital Allocation Is the First Product Decision</h2>
<p>Capital allocation is not a finance problem that happens to engineering. It is a technical leadership problem with finance consequences.</p>
<p>Every AI project consumes three things:</p>
<ul>
<li>engineering time</li>
<li>infrastructure budget</li>
<li>organizational attention</li>
</ul>
<p>If the project does not improve one of three board-level outcomes — margin expansion, risk compression, or execution speed — it is likely a vanity project wearing a product costume.</p>
<p>That does not mean the project has to be immediately profitable. It does mean you should be able to state what gets better if the project works and what gets worse if it does not.</p>
<h2 id="what-should-die-first">What Should Die First</h2>
<p>The easiest place to make mistakes is the demo room. The second easiest is the budget meeting.</p>
<p>Stop funding these first:</p>
<ol>
<li>
<p><strong>Thin demos that do not survive workflow reality.</strong>
If the user needs three manual edits after every response, you have built a presentation layer, not a product.</p>
</li>
<li>
<p><strong>Duplicate platform work.</strong>
If two teams are building separate prompt orchestration, evaluation, or routing layers, one of them should stop. Duplication feels like speed until the maintenance bill lands.</p>
</li>
<li>
<p><strong>Ambiguous experiments with no owner.</strong>
“We should explore AI” is not a strategy. It is a permission slip for drift.</p>
</li>
<li>
<p><strong>Projects with no measurable failure mode.</strong>
If nobody can say what counts as bad output, bad latency, bad cost, or bad adoption, the project cannot be managed.</p>
</li>
</ol>
<p>There is a simple reason these projects linger: they are emotionally easy to defend. Nobody wants to kill a project that sounds innovative. But if you cannot defend it with numbers, the project is not innovative. It is unpriced.</p>
<h2 id="the-kill-list-rubric">The Kill-List Rubric</h2>
<p>A good kill list is not a spreadsheet of personal dislikes. It is a decision system.</p>
<p>Before funding a new AI initiative, ask three questions:</p>
<ul>
<li><strong>Does this increase margin, reduce risk, or improve speed?</strong></li>
<li><strong>Can we measure that effect within one quarter?</strong></li>
<li><strong>Do we own the fallback if the model or vendor changes?</strong></li>
</ul>
<p>If the answer to all three is not yes, the default should be no.</p>
<p>This is where a lot of teams get sentimental. They continue funding because the project has a sponsor, or because it already consumed sunk cost, or because it looks good in a board deck. Those are weak reasons to keep a system alive.</p>
<p>Strong reasons to keep funding an AI initiative usually look like this:</p>
<ul>
<li>it replaces high-volume manual work</li>
<li>it improves decision quality in a regulated workflow</li>
<li>it reduces customer wait time</li>
<li>it protects a revenue stream that depends on fast, accurate responses</li>
</ul>
<p>Notice that none of those reasons mention hype.</p>
<h2 id="what-to-keep-funding-instead">What to Keep Funding Instead</h2>
<p>The highest-return AI investments are boring in the best way.</p>
<p>Fund the parts that make the system measurable and durable:</p>
<ul>
<li>retrieval and context quality</li>
<li> <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation harnesses</a>
</li>
<li>fallback logic</li>
<li>routing by task class</li>
<li>observability around bad outputs and retries</li>
<li>workflow-specific data collection</li>
</ul>
<p>The point is not to chase the smartest model. The point is to build a system that can absorb model churn without forcing a rewrite every six months.</p>
<p>A useful line to keep in mind: <strong>if a system cannot be measured under load, it is still a pilot.</strong> Pilots are fine. Pilots just should not keep consuming production budget forever.</p>
<h2 id="the-hard-part-is-saying-no">The Hard Part Is Saying No</h2>
<p>The best operators are not famous for being aggressive spenders. They are famous for being disciplined about what they do not fund.</p>
<p>That discipline becomes a reputation asset. The founder who sees you delete a weak AI project starts trusting your judgment. The board member who sees you cut duplicate work starts trusting your signal. The engineering team that sees you protect their time starts trusting your priorities.</p>
<p>Capital allocation is how you tell the truth about what matters. If a project cannot defend margin, risk, or speed, it should not survive by momentum alone. Fund the systems that make AI measurable, recoverable, and cheap to operate. Cut the rest.</p>
]]></content:encoded></item><item><title>AI Strategy: The CTO Perspective (It's Just Data Infrastructure)</title><link>https://lawzava.com/blog/2026-04-14-ai-cto-perspective/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-14-ai-cto-perspective/</guid><description>A CTO&amp;amp;rsquo;s AI strategy is not about chasing models. It is about resilient data infrastructure, operational boundaries, and measured throughput.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>In 2026, a CTO&rsquo;s AI strategy is not a model shortlist. It is an operating model for data, latency, evaluation, and failure. The model will change. The system around it should not.</p>
<p>If your AI plan still starts with &ldquo;which model should we buy,&rdquo; you are solving the easiest problem in the room. The moat is the pipeline that feeds context, the eval loop that catches regressions, and the fallback path that keeps the product standing when the model misses.</p>
<h2 id="the-strategy-is-the-infrastructure">The Strategy Is the Infrastructure</h2>
<p>The single biggest mistake engineering organizations make is treating the model as the brain. It is not. It is the most expensive dependency in the stack.</p>
<p>The brain is everything you build around it: context assembly, retrieval, validation, retries, telemetry, and rollback.</p>
<p>A CTO must focus ruthlessly on three pillars:</p>
<h3 id="1-the-context-pipeline">1. The Context Pipeline</h3>
<p>The model is only as intelligent as the context you feed it. If Postgres, Cassandra, or Scylla takes five seconds to assemble structured context, encode it, and hand it to the orchestrator, your feature is already late before inference begins.</p>
<p>Strategy means architecting data replication, embedding generation, and caching so the latency budget stays intact for the inference layer. If your data infrastructure is not close to real time, your AI will not be either.</p>
<h3 id="2-the-evaluation-framework">2. The Evaluation Framework</h3>
<p>You cannot scale what you cannot measure. If your organization is still eyeballing model outputs before deployment, you are running a pilot, not a production system.</p>
<p>Leadership means demanding continuous evaluation. Every PR that touches an orchestration layer must be blocked by a CI pipeline that runs 500 deterministic evals against the new reasoning flow. Building that telemetry <em>is</em> the AI strategy.</p>
<h3 id="3-graceful-degradation-and-fallbacks">3. Graceful Degradation and Fallbacks</h3>
<p>LLMs fail. APIs throttle. Endpoints rotate. If a model hallucinates malformed JSON and your core application crashes, that is not an AI failure; that is an architectural failure.</p>
<p>A mature strategy wraps every AI interaction in circuit breakers. If the model fails three times, what is the deterministic fallback? If the cloud provider rate-limits you, where is the  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >local, quantized 8B-parameter fallback model</a>
 running in your own cluster?</p>
<h2 id="stop-chasing-the-frontier">Stop Chasing the Frontier</h2>
<p>The frontier-model conversation is a distraction. Unless you are OpenAI or Anthropic, you do not win by having the smartest model. You win by having the tightest feedback loop, the cleanest data access, and the lowest cost per transaction.</p>
<p>A strong CTO designs for  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >swapability</a>
: a single configuration commit, zero downtime, and telemetry that proves the new model performs 4% better on the exact workload that matters.</p>
<p>That is the strategy. Everything else is theater.</p>
]]></content:encoded></item><item><title>Sovereign Systems: Building for a World Where Data Privacy Is Non-Optional</title><link>https://lawzava.com/blog/2026-04-06-sovereign-systems-privacy-non-optional/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-06-sovereign-systems-privacy-non-optional/</guid><description>Privacy is an architecture constraint, not a feature toggle. Building sovereignty in early avoids painful retrofits and closes enterprise deals faster.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Privacy is no longer a feature you bolt on before an enterprise deal closes. It&rsquo;s an architecture constraint that shapes how you store data, route requests, grant access, and deploy infrastructure. Teams that treat sovereignty as a first-class design input ship faster, close contracts with fewer surprises, and avoid the painful retrofit that hits every product that grows past its original assumptions. Build it in early or pay compound interest later.</p>
<h2 id="what-sovereign-actually-means">What &ldquo;Sovereign&rdquo; Actually Means</h2>
<p>In practical engineering terms, a sovereign system is one where you control the full lifecycle of every piece of data: where it lives, who can access it, how long it persists, and what happens when someone asks you to delete it. That&rsquo;s it. No mysticism, no marketing language.</p>
<p>This doesn&rsquo;t require owning physical hardware. It means having enforceable guarantees about data residency, encryption boundaries, identity controls, and audit trails, regardless of whether you run on bare metal, a private cloud, or a scoped partition within a public provider.</p>
<p>The distinction matters because &ldquo;we use AWS&rdquo; is not an answer to &ldquo;where does my data live.&rdquo; Region selection, encryption key ownership, cross-account access policies, and backup replication targets are the answers. Sovereignty is about specificity.</p>
<h2 id="why-this-is-urgent-now">Why This Is Urgent Now</h2>
<p>Three forces are converging.</p>
<p>First, data residency rules are tightening globally. The EU&rsquo;s enforcement posture has hardened. Brazil, India, and multiple Southeast Asian jurisdictions now impose localization requirements that are recent and still evolving. Cross-border transfer mechanisms that worked in 2023 are under review or already invalidated.</p>
<p>Second, AI systems multiply the problem. Every model inference potentially creates a copy of the input data. Retrieval-augmented generation pipelines pull documents into contexts that may span regions. Fine-tuning creates derivative datasets. Logging captures prompts and completions that contain customer data. If you weren&rsquo;t tracking data lineage before, AI workflows make the gap impossible to ignore.</p>
<p>Third, retrofitting is brutally expensive. Teams that scale first and add privacy controls later face a familiar pattern: months of engineering time, frozen feature development, emergency compliance audits, and customer conversations that should have happened at contract signing. The cost of early privacy controls is a fraction of the remediation bill.</p>
<h2 id="minimum-viable-controls">Minimum Viable Controls</h2>
<p>You don&rsquo;t need to solve everything at once. Four controls cover the critical surface.</p>
<p><strong>Identity boundaries.</strong> Every access to customer data, whether by a human, a service, or a model, must pass through an identity system with explicit grants. No ambient access. No shared credentials. No &ldquo;the app has a database connection string&rdquo; as your entire access model. Service-to-service authentication with short-lived tokens and scoped permissions is baseline, not advanced.</p>
<p><strong>Encryption with key ownership.</strong> Encrypt at rest and in transit, but also control the keys. If your cloud provider holds the only copy of the encryption key, you&rsquo;ve delegated a critical trust boundary. Customer-managed keys or bring-your-own-key arrangements aren&rsquo;t paranoia. They&rsquo;re the mechanism that makes &ldquo;we can&rsquo;t access your data&rdquo; a verifiable claim instead of a policy promise.</p>
<p><strong>Retention and deletion.</strong> Define how long each data category lives, and enforce it automatically. When a customer asks for deletion, you need to know every location where their data exists, including backups, logs, caches, model training sets, and analytics pipelines. If you can&rsquo;t enumerate those locations, you can&rsquo;t comply. Automated retention policies with verified deletion are the only way this works at scale.</p>
<p><strong>Audit trails.</strong> Log every access, transformation, and movement of sensitive data. Not for compliance theater, but because when something goes wrong, you need to reconstruct what happened. Immutable, append-only audit logs with tamper detection give you forensic capability and regulatory evidence in the same system.</p>
<h2 id="zero-trust-patterns-for-data-access">Zero-Trust Patterns for Data Access</h2>
<p> <a href="/blog/2021-08-23-zero-trust-architecture/"
   
   >Zero-trust</a>
 is overused as a buzzword, but the core principle is sound: never grant access based on network position alone. Every request must be authenticated, authorized, and logged regardless of where it originates.</p>
<p>For sovereign systems, this means your internal services don&rsquo;t get a free pass. A microservice running in the same VPC as the database still authenticates with scoped credentials and gets only the permissions its function requires. Lateral movement, the classic post-breach escalation path, becomes much harder when every hop requires fresh authorization.</p>
<p>This adds friction. That&rsquo;s the point. Friction at the access layer is cheap insurance against breaches that cost orders of magnitude more.</p>
<h2 id="multi-region-architecture-tradeoffs">Multi-Region Architecture Tradeoffs</h2>
<p>Data residency requirements often mean running infrastructure in multiple regions. This introduces real engineering tradeoffs.</p>
<p>Latency increases when data can&rsquo;t leave a region. If your EU customers&rsquo; data must stay in Frankfurt, serving those customers from us-east-1 isn&rsquo;t an option. You need regional deployments with local data stores, which means your application must handle regional routing, and your deployment pipeline must support multi-region releases.</p>
<p>Consistency gets harder. If you previously relied on a single-region database with strong consistency, splitting across regions forces you to choose between synchronous replication with higher latency or eventual consistency with application-level conflict resolution. Most teams find that eventual consistency with well-designed conflict resolution is the pragmatic choice, but it requires upfront design work.</p>
<p>Operational complexity increases linearly with regions. Each region needs monitoring, alerting, backup verification, and incident response capability. Teams that underestimate this end up with &ldquo;dark&rdquo; regions where infrastructure runs but nobody watches it.</p>
<p>The honest tradeoff:  <a href="/blog/2019-06-17-multi-region-architecture/"
   
   >multi-region sovereign architecture</a>
 costs more to build and operate than a single-region deployment. But for products selling to regulated industries or international customers, it&rsquo;s not optional. Budget for it explicitly rather than discovering the cost mid-contract.</p>
<h2 id="staged-implementation">Staged Implementation</h2>
<p>For teams with existing platforms, a staged approach works.</p>
<p><strong>Stage 1: Visibility.</strong> Map where customer data lives. Every database, cache, log store, backup, and third-party integration. You can&rsquo;t control what you can&rsquo;t see. This is usually the most humbling step.</p>
<p><strong>Stage 2: Boundaries.</strong> Implement identity-based access controls and encryption key management. Replace ambient access patterns with explicit grants. This is the highest-leverage change.</p>
<p><strong>Stage 3: Automation.</strong> Build automated retention enforcement, deletion verification, and audit log aggregation. Manual processes don&rsquo;t scale and don&rsquo;t survive employee turnover.</p>
<p><strong>Stage 4: Regional controls.</strong> If your market requires it, add data residency enforcement with regional routing and storage isolation. This is the most expensive stage and should be driven by actual customer and regulatory requirements, not speculation.</p>
<h2 id="governance-checklist">Governance Checklist</h2>
<p>For alignment between engineering, legal, and executive leadership:</p>
<ol>
<li>Document every data category, its sensitivity level, and its residency requirements.</li>
<li>Map data flows across services, regions, and third parties. Update quarterly.</li>
<li>Establish key ownership policy: who holds encryption keys, and what&rsquo;s the rotation schedule.</li>
<li>Define retention periods per data category with automated enforcement.</li>
<li>Build deletion capability that covers all storage locations, including backups and derived datasets.</li>
<li>Implement access logging with immutable audit trails.</li>
<li>Run a tabletop exercise: a customer requests full data deletion. Can you do it within your SLA?</li>
<li>Review  <a href="/blog/2025-09-15-ai-data-privacy/"
   
   >AI-specific data flows</a>
: where do prompts, completions, and training data live?</li>
</ol>
<h2 id="key-takeaways">Key Takeaways</h2>
<p>Sovereignty is not a premium feature or an enterprise upsell. It&rsquo;s core infrastructure for products that handle other people&rsquo;s data. The cost of building it in early is a fraction of the cost of retrofitting it later, and the trust it builds with customers compounds over every contract cycle.</p>
<p>The teams that get this right treat privacy as a design constraint alongside latency, reliability, and cost. Not as a checkbox for the legal team. The architecture follows from that decision.</p>
]]></content:encoded></item><item><title>The Throughput Engineer: Why Headcount Is a Lagging Metric</title><link>https://lawzava.com/blog/2026-03-30-throughput-engineer-headcount-lagging-metric/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-30-throughput-engineer-headcount-lagging-metric/</guid><description>Headcount is a lagging metric. The best engineering organizations measure throughput: decision speed, defect containment, and constraint removal.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Headcount is an input. Throughput is an outcome. The best engineering organizations have stopped asking &ldquo;how many engineers do we need?&rdquo; and started asking &ldquo;what&rsquo;s blocking the engineers we have?&rdquo; Teams that optimize for decision speed, defect containment, and execution clarity outperform teams twice their size. Hiring more people into a broken system just makes the system break faster.</p>
<h2 id="the-metric-everyone-tracks-and-nobody-questions">The Metric Everyone Tracks and Nobody Questions</h2>
<p>Every quarterly planning cycle, the same conversation happens. The roadmap is too ambitious for the team. The proposed solution is more headcount. The exec team approves some fraction of the ask. Six months later, the team is bigger but the roadmap is still slipping.</p>
<p>This pattern persists because headcount is easy to measure and feels actionable. You can put a number on a slide. You can point to it in a board meeting and say &ldquo;we&rsquo;re investing in engineering.&rdquo;</p>
<p>But headcount measures capacity the way adding lanes measures highway throughput. It works up to a point, then coordination overhead offsets the capacity gain. The tenth engineer doesn&rsquo;t add 10% more output. They add 10% more communication paths, 10% more code review load, and another person who needs context on every architectural decision.</p>
<p>The organizations getting this right have shifted to outcome metrics. Not &ldquo;how many people do we have&rdquo; but &ldquo;how fast do decisions move from identification to resolution.&rdquo; Not &ldquo;how many PRs did we merge&rdquo; but &ldquo;what&rsquo;s our  <a href="/blog/2022-01-24-dora-metrics-implementation/"
   
   >change failure rate</a>
 and how quickly do we recover.&rdquo;</p>
<h2 id="staff-growth-versus-constraint-removal">Staff Growth Versus Constraint Removal</h2>
<p>Adding staff is an additive intervention. It puts more resources into the system. Constraint removal is a multiplicative intervention. It makes every existing resource more effective.</p>
<p>Consider a team of eight engineers where the average PR sits in review for 18 hours. Hiring two more engineers does nothing to fix the review bottleneck. It makes it worse because there are now more PRs competing for the same review bandwidth. But changing the review process, setting a 4-hour SLA, pairing reviewers with authors, and shrinking PR scope, can cut that 18 hours to 4 without adding a single person.</p>
<p>The same principle applies at every level. Slow deploys, unclear ownership, meetings that could be async documents, long approval chains. Each costs every engineer on the team hours per week. Multiply by team size and the waste is staggering.</p>
<p>If 20 engineers each lose 5 hours per week to process friction, that&rsquo;s 100 engineer-hours, equivalent to 2.5 full-time engineers doing nothing but waiting. Removing the friction is cheaper than hiring, faster to implement, and doesn&rsquo;t increase coordination costs.</p>
<p>AI tooling has made this dynamic sharper. A well-structured team with good tooling and clear ownership regularly outships teams twice its size. But a poorly structured team with AI tooling just generates more half-finished work faster. AI amplifies the system it operates in, good or bad.</p>
<h2 id="the-operating-system-of-a-high-throughput-team">The Operating System of a High-Throughput Team</h2>
<p>High-throughput teams share three operational patterns that have nothing to do with individual talent.</p>
<p><strong>Clear intent over detailed instructions.</strong> When an engineer picks up a task, they should know the outcome that matters, not the exact steps to get there. &ldquo;Reduce P95 latency on the search endpoint below 200ms&rdquo; is clear intent. &ldquo;Refactor the search query builder to use connection pooling&rdquo; is a solution masquerading as a task. The first lets the engineer use judgment. The second removes it.</p>
<p>Teams that operate on intent move faster because decisions happen at the point of most information, the engineer doing the work, rather than being routed through a manager who has less context. This requires trust, and trust requires that the intent is genuinely clear and that the engineer has the authority to make reasonable tradeoffs.</p>
<p><strong>Delegated authority with explicit boundaries.</strong> Every recurring decision type should have a documented owner and a decision boundary. &ldquo;The on-call engineer can roll back any deploy without approval&rdquo; is a delegation. &ldquo;Database schema changes require review from the data team&rdquo; is a boundary. When these are written down and understood, decisions happen in minutes instead of hours.</p>
<p>The failure mode is implicit authority. Nobody knows who can make the call, so everyone escalates. The escalation chain adds latency to every decision. In a team of 15, this can mean that a simple operational decision takes a day instead of an hour because it bounces between three people who each assume someone else owns it.</p>
<p><strong> <a href="/blog/2020-04-13-async-communication-practices/"
   
   >Async-first communication</a>
.</strong> Synchronous communication, meetings, Slack pings expecting immediate response, tap-on-the-shoulder interruptions, is the most expensive coordination mechanism. It requires everyone to be available simultaneously and context-switch away from focused work.</p>
<p>Async-first doesn&rsquo;t mean no meetings. It means meetings are for decisions that genuinely require real-time discussion. Everything else is a written document, a recorded decision in a ticket, or a code review comment.</p>
<h2 id="a-weekly-operating-cadence">A Weekly Operating Cadence</h2>
<p>Decision tempo separates high-throughput teams from slow ones. A lightweight weekly cadence keeps the system self-correcting without drowning in noise.</p>
<p><strong>Weekly: review leading metrics.</strong> Cycle time from commit to production, change failure rate, time to recover from incidents, review queue depth, and decision latency on open questions. Don&rsquo;t track vanity metrics like lines of code or number of PRs.</p>
<p><strong>Biweekly: connect signals to causes.</strong> Is cycle time creeping up? Is one team&rsquo;s change failure rate spiking? Are the same types of decisions getting stuck repeatedly? The goal is systemic diagnosis, not individual blame.</p>
<p><strong>Biweekly: pick one constraint to remove.</strong> &ldquo;This sprint, we&rsquo;re going to cut our deploy time from 45 minutes to under 10&rdquo; is a decision. &ldquo;We&rsquo;re going to improve developer experience&rdquo; is not. One thing, not five.</p>
<p><strong>Continuous: execute, measure, repeat.</strong> Act on the decision, measure the result, and feed it back into the next weekly review. If cutting deploy time didn&rsquo;t improve cycle time, the constraint was elsewhere. Move to the next one.</p>
<h2 id="incentives-that-reward-impact-over-activity">Incentives That Reward Impact Over Activity</h2>
<p>Most engineering organizations accidentally incentivize busyness. The engineer who closes the most tickets gets praised. The team that ships the most features gets the biggest headcount allocation. The manager who runs the most meetings looks the most engaged.</p>
<p>Throughput-oriented incentives look different.</p>
<p>Reward engineers who eliminate recurring work, not just complete it. The engineer who automates away a manual process that costs the team 10 hours per week has created more value than the engineer who ships a new feature used by 50 people.</p>
<p>Reward teams that improve their own throughput metrics, not just output volume. A team that cuts its change failure rate from 15% to 3% has freed up enormous capacity that was previously spent on rollbacks, hotfixes, and incident response. That&rsquo;s worth more than two new features.</p>
<p>Reward leaders who make themselves less necessary. The manager whose team operates smoothly when they&rsquo;re on vacation has built a better system than the manager who&rsquo;s cc&rsquo;d on every decision.</p>
<h2 id="a-12-week-operating-reset">A 12-Week Operating Reset</h2>
<p>For teams experiencing delivery drag, a structured reset works better than a reorg.</p>
<p><strong>Weeks 1-3: Measure.</strong> Instrument cycle time, change failure rate, review latency, and decision latency. Don&rsquo;t change anything yet. Establish a baseline that everyone agrees on.</p>
<p><strong>Weeks 4-6: Remove one constraint.</strong> Pick the biggest bottleneck revealed by the data. If review latency is the worst, fix the review process. If deploy time is the worst, fix the pipeline. One constraint at a time.</p>
<p><strong>Weeks 7-9: Delegate and document.</strong> Write down the top 10 recurring decision types and who owns each one. Set decision boundaries. Remove one layer of approval from the most common workflow.</p>
<p><strong>Weeks 10-12: Sustain.</strong> Establish the weekly review cadence. Compare throughput metrics to the week-1 baseline. Identify the next constraint. Make the cycle self-reinforcing.</p>
<p>Teams that complete this reset typically see 30-50% improvement in cycle time without adding staff. The improvement comes from removing friction that was invisible because everyone had adapted to it.</p>
<h2 id="board-facing-metrics-that-map-engineering-to-business-risk">Board-Facing Metrics That Map Engineering to Business Risk</h2>
<p>Boards understand risk and return. Translate engineering throughput into those terms.</p>
<p><strong>Cycle time</strong> maps to market responsiveness. &ldquo;We can respond to a competitor move in days, not months&rdquo; is a strategic capability that boards care about.</p>
<p><strong>Change failure rate</strong> maps to operational risk. &ldquo;5% of our changes cause incidents&rdquo; is a risk number a board can evaluate, especially when paired with the cost of those incidents.</p>
<p><strong>Recovery time</strong> maps to resilience. &ldquo;When something breaks, we fix it in under an hour&rdquo; is a durability statement that affects customer trust and revenue protection.</p>
<p><strong>Decision latency</strong> maps to organizational agility. &ldquo;Strategic decisions take 2 days to reach execution, not 2 weeks&rdquo; tells the board that the organization can adapt.</p>
<p>None of these metrics mention headcount. That&rsquo;s the point. Headcount funds capacity. These metrics measure whether that capacity produces results.</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<p>Headcount tells you what you&rsquo;re spending. Throughput metrics, cycle time, change failure rate, recovery time, decision latency, tell you what you&rsquo;re getting.</p>
<p>The highest-leverage engineering work is constraint removal, not feature addition. Every hour of friction you eliminate pays dividends across every engineer on the team.</p>
<p>Stop asking &ldquo;how many engineers do we need?&rdquo; Start asking &ldquo;what&rsquo;s preventing the engineers we have from shipping?&rdquo;</p>
]]></content:encoded></item><item><title>AI Agent Operations and the Networking Bottleneck: Why AI Agents Fail on Legacy Infrastructure</title><link>https://lawzava.com/blog/2026-03-23-agenticops-networking-bottleneck/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-23-agenticops-networking-bottleneck/</guid><description>Most AI agent failures are infrastructure failures, not model failures. Legacy networking and missing circuit breakers are the real reliability bottleneck.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most AI agent failures aren&rsquo;t model failures. They&rsquo;re infrastructure failures wearing a model mask. Legacy networking assumptions, flat trust boundaries, and missing circuit breakers create brittle agent behavior that looks like &ldquo;the AI is unreliable&rdquo; but is actually &ldquo;the network can&rsquo;t support autonomous execution patterns.&rdquo; Fix the infrastructure and the agents get dramatically more reliable overnight.</p>
<h2 id="the-execution-path-nobody-drew-on-a-whiteboard">The Execution Path Nobody Drew on a Whiteboard</h2>
<p>Agent tasks fan out across DNS resolution, TLS handshakes, token exchanges, service mesh routing, and backend queries. The multi-hop latency problem is well-understood (I covered the general case in  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >the cloud-heavy architecture post</a>
), but the networking-specific failure modes deserve their own treatment: stale DNS caches that route agents to decommissioned endpoints, TLS renegotiation overhead that compounds across 40 tool calls, service mesh sidecars that add 5-15ms per hop invisibly, and queue depth limits that silently drop requests during agent-scale bursts. These aren&rsquo;t model problems. They&rsquo;re networking problems that surface as agent unreliability.</p>
<h2 id="the-hidden-cost-of-20th-century-network-assumptions">The Hidden Cost of 20th-Century Network Assumptions</h2>
<p>Most enterprise networks were designed around two assumptions: traffic flows north-south through a perimeter, and anything inside the perimeter is trusted. AI agents violate both assumptions simultaneously.</p>
<p>Agent traffic is east-west by default. A single task might call an internal knowledge base, a code execution sandbox, an external search API, and a database, all in a single reasoning loop. The traffic pattern looks like a mesh, not a pipeline. Networks designed for request-response patterns between a frontend and a backend choke on this.</p>
<p>The trusted-network assumption is worse. When an agent has a service account with broad permissions, every tool call inherits those permissions. If the agent can read from a document store, it can read from all of it. If it can write to a database, the blast radius of a prompt injection extends to every table the service account can touch. This isn&rsquo;t a theoretical risk. It&rsquo;s the default configuration in most deployments I&rsquo;ve seen.</p>
<p>Latency compounds differently for agents than for traditional services. A human user tolerates 200ms of added latency on a page load. An agent making 40 tool calls in a single task turns 200ms of unnecessary overhead per call into 8 seconds of total delay. At scale, this means the difference between an agent that completes tasks in seconds and one that takes minutes. Users notice. They lose trust. They stop using the feature.</p>
<h2 id="zero-trust-identity-for-autonomous-systems">Zero-Trust Identity for Autonomous Systems</h2>
<p>The fix isn&rsquo;t a network redesign. It&rsquo;s an identity redesign at the network layer.</p>
<p>Every agent tool call should carry a scoped identity that specifies what the agent can reach, for how long, and on behalf of which user or task. This is standard zero-trust thinking applied to agent traffic patterns. (For the broader tool permission and output validation side of this, see  <a href="/blog/2026-02-23-ai-security-evolution/"
   
   >my earlier post on AI security</a>
.)</p>
<p>In practice, the networking-specific concerns are:</p>
<p><strong>Per-task credentials with network scope.</strong> Instead of a long-lived service account, mint a short-lived token for each agent task. The token carries the minimum permissions needed for that specific workflow, and critically, it limits which network endpoints the agent can reach. When the task ends, the token expires. If the agent is compromised mid-task, the blast radius is one task&rsquo;s worth of permissions and one task&rsquo;s set of reachable services.</p>
<p><strong>Per-call authentication overhead.</strong> Every tool call crossing a network boundary needs auth, and that auth has a cost. TLS mutual authentication, token validation, and policy lookup all add latency. The design tradeoff is between granular identity (every call authenticated independently) and performance (connection pooling, session tokens, cached auth decisions). Get this wrong and your zero-trust layer becomes the latency bottleneck it was meant to protect against.</p>
<p><strong>Network segmentation per agent class.</strong> Not all agents need the same network access. An agent that summarizes documents has no business reaching your billing API. Segment your network so each agent class can only route to the services it needs. This is basic network segmentation, but most teams skip it because their agents all share one service account with broad network access.</p>
<h2 id="reliability-engineering-for-agent-workflows">Reliability Engineering for Agent Workflows</h2>
<p>Traditional reliability patterns need adjustment for agentic workloads. The standard toolkit, retries, timeouts, circuit breakers, still applies, but the parameters and placement change.</p>
<p><strong>Timeouts need to be per-step, not per-request.</strong> An agent task might legitimately run for 30 seconds across 20 tool calls. A global timeout of 30 seconds will kill valid workflows. A per-step timeout of 3 seconds will catch hung dependencies without killing the task.</p>
<p><strong>Retry logic needs backpressure awareness.</strong> An agent that retries a failed tool call immediately, while 50 other agent instances are doing the same thing, creates a retry storm that takes down the dependency. Exponential backoff with jitter is the minimum. Better: a circuit breaker that trips after a threshold and fails fast for all agent instances, with a clear error message the model can reason about.</p>
<p><strong>Queue depth matters more than you think.</strong> Agent workloads are bursty. A user action that triggers 10 agent tasks, each making 15 tool calls, puts 150 requests into your service mesh in seconds. If the target service has a queue depth of 50, you&rsquo;re dropping requests before the agent even knows there&rsquo;s a problem. Size your queues for agent-scale fan-out, not human-scale request rates.</p>
<p><strong>Graceful degradation over hard failure.</strong> When a tool call fails, the agent should get a structured error it can reason about, not a 500 or a timeout. &ldquo;Knowledge base unavailable, try alternative approach&rdquo; is actionable. A raw HTTP error is not. Design your tool contracts to return machine-readable failure modes.</p>
<h2 id="observability-for-agent-decision-traces">Observability for Agent Decision Traces</h2>
<p>Standard APM tools show you request latency and error rates. For agent workflows, you need something more: a trace that follows the agent&rsquo;s reasoning across tool calls, captures the decision points, and shows why the agent chose one path over another.</p>
<p>This means correlating model inputs, outputs, and tool calls into a single trace. Each agent task gets a trace ID. Each tool call within that task gets a span. The spans include the tool arguments, the response, the latency, and the policy decision. When you look at a slow or failed agent task, you can see exactly which step took too long, which dependency failed, and whether the agent&rsquo;s retry behavior made things better or worse.</p>
<p>The teams doing this well treat agent traces like they treat database query plans. They review them regularly, look for patterns, and optimize the hot paths. A tool call that takes 500ms and gets called 20 times per task is a bigger problem than a tool call that takes 2 seconds but only gets called once.</p>
<h2 id="migration-path">Migration Path</h2>
<p>You don&rsquo;t need to rebuild your infrastructure to start.</p>
<ol>
<li><strong>Instrument first.</strong> Add trace IDs to agent tool calls. Log latency, errors, and retry counts per step. You can&rsquo;t fix what you can&rsquo;t see.</li>
<li><strong>Add identity boundaries.</strong> Replace long-lived service accounts with per-task tokens, starting with agents that have write access.</li>
<li><strong>Circuit-break external calls.</strong> Add circuit breakers and per-step timeouts for every external dependency. Size queues for agent-scale fan-out.</li>
<li><strong>Migrate to mesh.</strong> Deploy a  <a href="/blog/2022-04-04-service-mesh-decision-guide/"
   
   >service mesh</a>
 or policy layer for tool call routing. Start in audit mode, then shift to enforcement.</li>
</ol>
<p>Each step is small and reversible. Together they compound into a fundamentally more reliable agent platform.</p>
<h2 id="checklist-risk-reduction-in-90-days">Checklist: Risk Reduction in 90 Days</h2>
<ul>
<li><input disabled="" type="checkbox"> Map every tool an agent can call, its permissions, and its failure modes</li>
<li><input disabled="" type="checkbox"> Add per-task trace IDs to all agent tool calls</li>
<li><input disabled="" type="checkbox"> Replace at least one long-lived service account with scoped, short-lived tokens</li>
<li><input disabled="" type="checkbox"> Set per-step timeouts on all agent tool calls</li>
<li><input disabled="" type="checkbox"> Add circuit breakers for external API dependencies</li>
<li><input disabled="" type="checkbox"> Deploy a policy layer in audit mode for tool call authorization</li>
<li><input disabled="" type="checkbox"> Review agent decision traces weekly for latency outliers and retry storms</li>
<li><input disabled="" type="checkbox"> Load test agent workflows at 10x expected concurrency</li>
<li><input disabled="" type="checkbox"> Document failure modes and give agents structured error responses</li>
<li><input disabled="" type="checkbox"> Establish an error budget for agent reliability separate from service reliability</li>
</ul>
<h2 id="key-takeaways">Key Takeaways</h2>
<p> <a href="/blog/2026-01-19-ai-agent-reliability/"
   
   >Agent reliability</a>
 is infrastructure reliability. The model is usually fine. The network, the auth layer, the retry logic, and the observability stack are where agent workflows actually break.</p>
<p>Treat agent tool calls like an API surface that needs zero-trust security, per-step reliability engineering, and end-to-end tracing. The teams that figure this out early will ship reliable agent products. The teams that keep tuning prompts to work around infrastructure problems will keep wondering why their agents are &ldquo;flaky.&rdquo;</p>
<p>Network and identity design is core agent product work, not background platform plumbing. Budget for it accordingly.</p>
]]></content:encoded></item><item><title>De-Risking the Black Swan: Red-Teaming Distributed Databases Before Production</title><link>https://lawzava.com/blog/2026-03-16-de-risking-black-swan-distributed-databases/</link><pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-16-de-risking-black-swan-distributed-databases/</guid><description>Red-teaming distributed databases before production: most catastrophic failures are compound scenarios nobody practiced, not black swans.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most catastrophic database incidents aren&rsquo;t novel. They&rsquo;re compounded failures that nobody practiced for. The node-failure test passes, so the team moves on. Then a network partition hits during a  <a href="/blog/2016-08-15-database-migrations-without-downtime/"
   
   >schema migration</a>
 while the on-call engineer is handling an unrelated alert, and suddenly you&rsquo;re in territory no runbook covers. Structured red-teaming exposes these compound paths before they become customer-visible outages. It costs a fraction of what a single bad incident costs.</p>
<h2 id="black-swans-vs-ignored-knowns">Black Swans vs. Ignored Knowns</h2>
<p>The term &ldquo;black swan&rdquo; gets overused in infrastructure. Most catastrophic database failures are not genuinely unpredictable. They are known failure modes that compound in ways nobody tested.</p>
<p>Consider the canonical distributed database incident: a network partition isolates a minority of nodes, those nodes continue accepting writes because the partition detection is slow, the partition heals, and now you have conflicting data that the conflict resolution logic wasn&rsquo;t designed to handle at that volume. Every component in this chain is well-understood. The failure isn&rsquo;t in any single component. It&rsquo;s in the interaction between them under specific timing conditions.</p>
<p>The honest term for most &ldquo;black swan&rdquo; database incidents is &ldquo;ignored known.&rdquo; The team knew partitions could happen. They knew conflict resolution had edge cases. They knew detection wasn&rsquo;t instant. They just never tested all three at once.</p>
<p>Red-teaming is how you turn ignored knowns into practiced scenarios.</p>
<h2 id="mission-style-red-teaming">Mission-Style Red-Teaming</h2>
<p> <a href="/blog/2020-06-08-chaos-engineering-practices/"
   
   >Chaos engineering</a>
 tools that randomly kill processes are useful, but they test a narrow failure class: single-component loss. Distributed database failures rarely look like one node dying cleanly. They look like degraded networks, clock drift, slow disks, operator errors during maintenance windows, and combinations of all of the above.</p>
<p>Mission-style red-teaming borrows from military and security practice. A dedicated team designs multi-step failure scenarios with specific objectives, executes them against production-equivalent infrastructure, and scores the defending team&rsquo;s response. The key difference from chaos engineering is intentionality: the red team isn&rsquo;t injecting random faults. They&rsquo;re pursuing a specific failure hypothesis through a sequence of realistic actions.</p>
<p>A red-team exercise has three roles:</p>
<ul>
<li><strong>Red team</strong>: designs and executes the failure scenario. Their goal is to cause data loss, unavailability, or corruption without triggering detection within a target time window.</li>
<li><strong>Blue team</strong>: the on-call and operations engineers responding as they would in a real incident. They don&rsquo;t know the scenario in advance.</li>
<li><strong>White team</strong>: observers who control the exercise, ensure safety boundaries, and document everything for the post-exercise review.</li>
</ul>
<p>The exercise runs for a fixed window, typically two to four hours. The red team executes their scenario. The blue team detects, diagnoses, and responds. Everyone debriefs afterward.</p>
<h2 id="the-stress-scenarios-that-matter">The Stress Scenarios That Matter</h2>
<p>Not all failure modes are worth practicing. Focus on scenarios that are plausible, high-impact, and poorly covered by existing automation.</p>
<p><strong>Network partitions with asymmetric visibility.</strong> One side of the partition can see the other; the other side cannot. This breaks assumptions in consensus protocols that expect symmetric failure detection. Many teams test clean partitions but never test asymmetric ones.</p>
<p><strong>Clock skew under load.</strong> Distributed databases that use timestamps for ordering (which is most of them) behave unpredictably when clocks drift. NTP usually keeps drift small, but under heavy load, NTP corrections can be delayed. The result is transaction ordering violations that are invisible until a consistency check runs, which might be hours or days later.</p>
<p><strong>Quorum erosion during maintenance.</strong> You take one node offline for a rolling upgrade. While it&rsquo;s down, a second node develops a slow disk. You now have a degraded quorum that&rsquo;s technically functional but one failure away from data unavailability. This is the most common compound failure pattern and the least practiced.</p>
<p><strong>Operator mistakes during incidents.</strong> The most dangerous moment for a distributed database is when a human is manually intervening during an incident. Wrong-node restarts, accidental force-quorum operations, and recovery commands run against the wrong cluster are responsible for a disproportionate share of catastrophic data loss. Red-teaming should include scenarios where the operator is given misleading information and time pressure.</p>
<p><strong>Backup restoration under partial failure.</strong> Most backup tests verify that a restore works on a clean target. Real restores happen during incidents, when the target environment is degraded, the team is stressed, and the backup might be from a point in time that&rsquo;s already inconsistent. Test restoration under these conditions, not just in a clean room.</p>
<h2 id="the-ooda-loop-for-incident-rehearsal">The OODA Loop for Incident Rehearsal</h2>
<p>Effective red-team exercises run on a tight observe-orient-decide-act cadence. This isn&rsquo;t just a framework. It&rsquo;s a scoring mechanism.</p>
<p><strong>Observe</strong>: How quickly does the blue team notice something is wrong? Detection time is the single most important metric. A failure that&rsquo;s detected in two minutes has a fundamentally different blast radius than one detected in twenty. Measure time from fault injection to first alert, and time from first alert to accurate diagnosis.</p>
<p><strong>Orient</strong>: Does the team correctly identify what&rsquo;s happening? Misdiagnosis is common in compound failures because the symptoms don&rsquo;t match any single runbook entry. The blue team might see elevated latency and assume it&rsquo;s a hot key, when the actual cause is a partial partition affecting replication. Measure time from first alert to correct hypothesis.</p>
<p><strong>Decide</strong>: Does the team choose an appropriate response? Under pressure, teams often default to the most familiar action (restart the node) rather than the most appropriate one (isolate the partition). Measure whether the chosen action matches the failure mode.</p>
<p><strong>Act</strong>: Does the team execute the response correctly? Even when the right decision is made, execution errors under stress are common. Typos in commands, wrong node targets, and forgotten steps in manual procedures are all frequent. Measure execution accuracy and time to containment.</p>
<p>Each phase gets a score. Over multiple exercises, these scores reveal systemic gaps: maybe detection is fast but diagnosis is slow, or decisions are sound but execution is error-prone. That tells you exactly where to invest in automation, training, or tooling.</p>
<h2 id="scoring-readiness">Scoring Readiness</h2>
<p>After each exercise, score three dimensions:</p>
<p><strong>Readiness</strong> (1-5): Could the team handle this scenario if it happened tomorrow in production? A 1 means the team didn&rsquo;t detect the failure. A 5 means they detected, diagnosed, and contained it within SLA.</p>
<p><strong>Blast radius</strong> (1-5): If the team had not responded, how bad would it have gotten? A 1 means minor degradation. A 5 means unrecoverable data loss or extended outage.</p>
<p><strong>Time to containment</strong> (minutes): Wall-clock time from fault injection to the point where the failure is contained and no longer spreading. This is the metric that matters most to your customers and your SLA.</p>
<p>Plot these over time. Improving readiness scores and decreasing containment times are the clearest signals that your red-teaming program is working. If scores plateau, your scenarios aren&rsquo;t challenging enough.</p>
<h2 id="from-findings-to-backlog">From Findings to Backlog</h2>
<p>Red-team exercises are useless if findings sit in a  <a href="/blog/2021-11-29-incident-management-practices/"
   
   >postmortem</a>
 document that nobody reads. Every exercise should produce a prioritized list of concrete improvements, each with an owner and a deadline.</p>
<p>The conversion process is simple:</p>
<ol>
<li><strong>List every gap discovered.</strong> Detection gaps, diagnostic confusion, tool limitations, missing runbooks, automation failures.</li>
<li><strong>Score each gap by blast radius times likelihood.</strong> Likelihood is informed by the exercise, not guessed.</li>
<li><strong>Assign an owner for each gap.</strong> Not a team. A person.</li>
<li><strong>Set a deadline before the next exercise.</strong> The next exercise will test whether the gap was closed. This creates accountability.</li>
</ol>
<p>Common improvements that come out of red-team exercises include automated partition detection that currently requires manual observation, runbook updates for compound failure scenarios, guardrails on dangerous operator commands during incidents, and backup restoration procedures tested under realistic conditions.</p>
<p>The backlog items from red-teaming tend to be high-value, low-glamour work. They rarely make it onto a roadmap through normal prioritization because they address risks that haven&rsquo;t materialized yet. The exercise provides the evidence needed to justify the investment.</p>
<h2 id="a-quarterly-operating-cadence">A Quarterly Operating Cadence</h2>
<p>Red-teaming works best as a regular practice, not a one-off event. A quarterly cadence balances rigor with operational overhead.</p>
<p>Run quarterly. Dedicate the first few weeks to scenario design based on recent incidents and architectural changes, a half-day to executing the exercise against a production-equivalent environment, and the remainder of the quarter to remediating the gaps you found.</p>
<p>This cadence means every quarter your team practices a realistic failure scenario, identifies concrete gaps, and fixes the most critical ones before the next exercise. Over four quarters, you&rsquo;ve tested and improved your response to a dozen failure modes. That&rsquo;s a fundamentally different reliability posture than &ldquo;we tested node failover once during setup and it worked.&rdquo;</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Most catastrophic database failures are compound scenarios that nobody practiced, not genuinely unpredictable events.</li>
<li>Chaos engineering tests component failure. Red-teaming tests system failure under realistic operational conditions.</li>
<li>Score every exercise on detection time, diagnostic accuracy, decision quality, and execution correctness. Track trends.</li>
<li>Convert findings into owned backlog items with deadlines tied to the next exercise.</li>
<li>Run quarterly. Consistency matters more than intensity.</li>
</ul>
<p>Red-teaming distributed databases is not theater and it&rsquo;s not a luxury. It&rsquo;s the cheapest way to find out whether your recovery assumptions actually hold before your customers find out for you.</p>
]]></content:encoded></item><item><title>Beyond Cloud-Heavy Architecture: Why Agentic Systems Need Local-First, Hardware-Aware Design</title><link>https://lawzava.com/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/</link><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/</guid><description>Local-first, hardware-aware architecture is becoming the default for high-reliability AI: cloud-heavy patterns cost too much and fail unpredictably.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most teams building agentic systems default to cloud-heavy architectures because that&rsquo;s what they know. The result is unpredictable latency, runaway costs on bursty workloads, and a privacy posture that depends entirely on someone else&rsquo;s infrastructure. Local-first, hardware-aware design fixes the economics and gives you failure modes you can actually reason about. Treat compute placement as architecture, not an optimization pass.</p>
<h2 id="the-cloud-heavy-anti-pattern">The Cloud-Heavy Anti-Pattern</h2>
<p>The standard agentic stack looks like this: application code in one cloud region calls a model API in another, pulls context from a vector database in a third, and writes results back through a gateway that adds its own hop. Every step crosses a network boundary. Every boundary adds latency variance, failure surface, and cost.</p>
<p>For a single inference call, the overhead is tolerable. For an agent that chains ten to fifty calls per task, with tool use, retrieval, and self-correction loops, the overhead compounds. A p50 latency of 200ms per hop becomes 2-10 seconds of pure network time on a moderately complex agent run. At p99, you&rsquo;re looking at timeouts and retries that double or triple your effective cost.</p>
<p>The measurable symptoms are consistent across teams:</p>
<ul>
<li><strong>Latency variance dominates execution time.</strong> The model itself is fast. The network between your orchestrator and the model, plus the hops to retrieval and tool services, is where time disappears.</li>
<li><strong>Cost scales with hops, not intelligence.</strong> You pay for every round trip: egress, ingress, token overhead from context reassembly, and retry loops when any hop fails.</li>
<li><strong>Failure modes are combinatorial.</strong> When five services must all be healthy for one agent task to complete, your effective availability is the product of their individual availabilities. Five nines times five is not five nines.</li>
</ul>
<p>This is not an argument against cloud. It&rsquo;s an argument against cloud-only, cloud-default architecture for workloads that don&rsquo;t need it.</p>
<h2 id="consolidating-runtime-layers">Consolidating Runtime Layers</h2>
<p>The fix is straightforward: move compute closer to the data and the user. Consolidate runtime layers so agent orchestration, context retrieval, and lightweight inference happen in the same process or at least on the same machine.</p>
<p>This is not a new idea. Databases figured this out decades ago. You don&rsquo;t run your query planner in a different availability zone from your storage engine. Agentic systems are hitting the same lesson: when the workload is latency-sensitive and involves tight feedback loops, co-location wins.</p>
<p>In practice, consolidation means running a  <a href="/blog/2025-08-18-local-ai-development/"
   
   >local inference server</a>
 for small models (classification, routing, extraction), keeping your retrieval index on the same node as your orchestrator, and reserving cloud API calls for frontier-model tasks that actually need them. The local layer handles the high-frequency, low-complexity work. The cloud layer handles the hard problems.</p>
<p>The cost difference is significant. A team running all inference through a cloud API at roughly two to five dollars per thousand complex agent tasks can drop to twenty to fifty cents by handling routine calls locally with a quantized model on commodity GPU hardware. The frontier API cost doesn&rsquo;t disappear, but it shrinks because you&rsquo;re only sending it the work that justifies the price.</p>
<h2 id="cloud-only-vs-hybrid-cost-envelopes">Cloud-Only vs. Hybrid Cost Envelopes</h2>
<p>The math depends on workload shape, but the pattern is consistent.</p>
<p>Cloud-only architectures have variable cost that scales linearly with usage and offers no marginal improvement at volume. You pay the same per-token rate whether you run one task or a million. Egress fees, retry overhead, and context window waste compound on top.</p>
<p>Hybrid local-first architectures have a higher fixed cost (hardware, setup, maintenance) but dramatically lower marginal cost. Once the local inference server is running, the incremental cost of a routing decision or an extraction call is effectively zero. You&rsquo;re paying for electricity and depreciation, not per-request metering.</p>
<p>The crossover point arrives faster than most teams expect. For workloads above a few thousand agent tasks per day, local-first is cheaper within months, not years. Below that threshold, cloud-only is simpler and the cost premium is manageable.</p>
<p>The latency picture is even more decisive. Local inference on a mid-range GPU delivers sub-10ms response times for small models. No network hop matches that. For agent loops that make dozens of calls per task, local inference can cut total wall-clock time by 60-80%.</p>
<h2 id="where-systems-languages-matter">Where Systems Languages Matter</h2>
<p>Agent runtimes written in Python work fine for prototyping and low-throughput production. But as you move inference and orchestration onto local hardware, you start caring about memory predictability, startup time, and per-request overhead in ways that garbage-collected runtimes don&rsquo;t handle well.</p>
<p> <a href="/blog/2021-02-22-rust-for-cloud-services/"
   
   >Rust</a>
 is showing up in this layer for practical reasons. It gives you memory safety without garbage collection pauses, which matters when you&rsquo;re serving inference requests with tight latency budgets.</p>
<p>This is not about rewriting your application in a systems language. It&rsquo;s about the runtime layer, the inference server, the orchestration loop, the retrieval engine. These are the hot paths where predictable performance translates directly into cost savings and reliability. The application logic on top can stay in whatever language your team knows.</p>
<p>The practical signal: if your agent runtime&rsquo;s p99 latency is dominated by GC pauses or memory allocation overhead rather than actual inference time, a systems-language runtime will help. If inference time dominates, the language doesn&rsquo;t matter.</p>
<h2 id="adoption-without-full-rewrites">Adoption Without Full Rewrites</h2>
<p>Teams with existing cloud-heavy architectures don&rsquo;t need to rip and replace. The migration is incremental and each step produces measurable improvement.</p>
<p><strong>Step 1: Instrument and classify.</strong> Before moving anything, measure what your agent stack actually does. Break down time and cost by call type: routing decisions, context retrieval, small-model inference, frontier-model inference. Most teams discover that 70-80% of calls are routine work that doesn&rsquo;t need a frontier model or a cloud round trip.</p>
<p><strong>Step 2: Add a local inference tier.</strong> Deploy a quantized model locally for the routine calls you identified. Route classification, extraction, and simple generation through it. Keep the cloud API as the escalation path. This is a routing change, not a rewrite.</p>
<p><strong>Step 3: Co-locate retrieval.</strong> Move your vector index or retrieval layer onto the same infrastructure as your orchestrator. This eliminates the retrieval round trip, which is often the single largest latency contributor after model inference.</p>
<p><strong>Step 4: Evaluate and tighten.</strong> With local tiers in place, measure again. Adjust routing thresholds. Identify the next tier of work that can move local. Each iteration reduces cloud dependency and improves predictability.</p>
<p>The entire migration can happen alongside normal feature work. No flag days, no cutover weekends.</p>
<h2 id="governance-and-data-residency">Governance and Data Residency</h2>
<p>Local-first architecture has a governance benefit that&rsquo;s easy to overlook: your data stays on your infrastructure. For teams operating under GDPR, HIPAA, or sector-specific data residency requirements, this simplifies compliance significantly.</p>
<p>When agent tasks process user data through a cloud API, that data traverses networks you don&rsquo;t control and resides, however briefly, on infrastructure you don&rsquo;t own. The compliance burden of documenting, auditing, and risk-managing that data flow is real and growing. Local inference eliminates the flow entirely for tasks that don&rsquo;t require cloud escalation.</p>
<p>This doesn&rsquo;t mean you avoid cloud APIs altogether. It means you have architectural control over which data leaves your perimeter and which doesn&rsquo;t. That&rsquo;s a better conversation to have with your compliance team than &ldquo;everything goes to a third-party API.&rdquo;</p>
<h2 id="decision-rubric">Decision Rubric</h2>
<p>When deciding how to place compute for agentic workloads, ask these questions:</p>
<ol>
<li><strong>Volume</strong>: Are you running more than a few thousand agent tasks per day? If yes, the economics of local inference likely favor hybrid.</li>
<li><strong>Latency sensitivity</strong>: Do your agent loops involve more than ten chained calls? If yes, network overhead is probably your bottleneck.</li>
<li><strong>Data sensitivity</strong>: Does your agent process PII, health data, or regulated information? If yes, local-first reduces compliance surface.</li>
<li><strong>Team capability</strong>: Do you have infrastructure engineers who can operate local GPU servers? If no, start with managed options or cloud-based inference with a clear migration path.</li>
<li><strong>Workload predictability</strong>: Are your traffic patterns bursty or steady? Bursty workloads benefit most from local capacity that handles baseline load with cloud burst for peaks.</li>
</ol>
<h2 id="common-traps">Common Traps</h2>
<ul>
<li><strong>Over-investing in local hardware before measuring workload shape.</strong> Instrument first. Buy hardware based on data, not intuition.</li>
<li><strong>Treating local and cloud as either/or.</strong> The right answer is almost always hybrid. The question is where to draw the line.</li>
<li><strong>Ignoring operational cost of self-hosted infrastructure.</strong> Local inference is cheaper per request but requires someone to keep it running. Factor in ops time.</li>
<li><strong>Optimizing for p50 when p99 is what breaks your SLA.</strong> Agentic workloads are chains. One slow hop at p99 delays the entire task.</li>
</ul>
<p>Hardware placement is a first-order architecture decision. Make it early, measure it continuously, and adjust as your workload evolves. The teams that get this right don&rsquo;t have the fanciest models. They have the most predictable systems.</p>
]]></content:encoded></item><item><title>AI Startup Landscape 2026</title><link>https://lawzava.com/blog/2026-03-02-ai-startup-landscape/</link><pubDate>Mon, 02 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-02-ai-startup-landscape/</guid><description>By early March 2026, the AI startup market looks less like a gold rush and more like a durable industry. Here&amp;amp;rsquo;s where leverage sits and what buyers reward.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>In early March 2026, &ldquo;we use AI&rdquo; is not a startup thesis. Buyers reward outcomes, reliability, and integration. If you cannot explain unit economics, governance, and how you fit into existing workflows, you stall at pilot. The durable advantages are the familiar ones: data, distribution, and operational execution.</p>
<p>The  <a href="/blog/2023-07-03-ai-startup-landscape/"
   
   >AI startup market</a>
 is no longer about novelty. It&rsquo;s about cost, control, and integration. The surface area is still large, but the center of gravity has shifted toward fewer core platforms, tighter enterprise scrutiny, and a bigger gap between prototypes and production systems.</p>
<h2 id="market-shape">Market Shape</h2>
<h3 id="platform-and-infrastructure">Platform and Infrastructure</h3>
<p>The platform layer has consolidated into a small set of credible options with predictable capabilities. Buyers are less willing to bet on unproven foundations and more willing to standardize on what is stable, documented, and supported. Infrastructure has followed a similar path: compute, data pipelines, and deployment stacks are converging on vendors that can meet uptime, security, and procurement requirements without surprises.</p>
<h3 id="applications">Applications</h3>
<p>Application-layer startups still have room, but the bar is higher. Products that win do not just automate a task; they change a workflow and own measurable outcomes. Horizontal tools that look interchangeable struggle to price, and sales cycles now expect proof of reliability, cost controls, and governance.</p>
<h2 id="what-differentiation-looks-like-now">What Differentiation Looks Like Now</h2>
<p>Differentiation is less about model performance and more about compound advantages that are hard to copy. The clearest signals are:</p>
<ul>
<li>Proprietary or hard-to-recreate data flows tied to a real workflow.</li>
<li>Distribution that doesn&rsquo;t depend entirely on paid acquisition or hype cycles.</li>
<li>A delivery path from pilot to production that fits enterprise controls.</li>
</ul>
<h2 id="where-leverage-actually-sits">Where Leverage Actually Sits</h2>
<p>Look past the marketing and leverage tends to concentrate in a few places:</p>
<ul>
<li><strong>Workflow ownership</strong>: the product lives where work already happens (tickets, docs, CRM, IDEs), not in a separate &ldquo;AI app.&rdquo;</li>
<li><strong>Hard-to-copy data loops</strong>: usage generates better data, which improves the product, which drives more usage.</li>
<li><strong>Integration depth</strong>: the messy parts (permissions, audit logs, escalation paths) become a moat.</li>
<li><strong>Operational playbooks</strong>: rollout, monitoring, and rollback are part of what you sell, even if indirectly.</li>
</ul>
<p>This is why many flashy demos fail commercially. They show capability without showing leverage.</p>
<h2 id="commercial-reality">Commercial Reality</h2>
<p>Budgets are still there, but they are more disciplined. Buyers want predictable unit economics and clear ownership of risk. That means pricing tied to outcomes or usage, transparent operating costs, and honest limits on automation. Services revenue is acceptable when it accelerates deployment, but products that require constant custom work do not scale well under current expectations.</p>
<h2 id="what-buyers-reward-in-2026">What Buyers Reward In 2026</h2>
<p>Even early-stage buyers are more explicit now. Successful deals usually include:</p>
<ul>
<li>clear ROI framing (&ldquo;reduce handling time by X&rdquo;, &ldquo;increase conversion by Y&rdquo;)</li>
<li>visible controls (permissions, logging, approvals)</li>
<li>predictable  <a href="/blog/2026-02-09-ai-cost-trends/"
   
   >cost per outcome</a>
</li>
<li>an escalation path for edge cases</li>
</ul>
<p>If you can&rsquo;t answer security and governance questions without improvising, the sale slows down.</p>
<h2 id="where-this-leaves-new-teams">Where This Leaves New Teams</h2>
<p>The winning path is narrower, not closed. New teams can still build meaningful businesses if they accept that the default outcome is commoditization and plan for it. Focus beats breadth. Systems thinking beats feature stacking. The fastest route to durability is to choose a domain where operational pain is acute and data is defensible, then deliver with production-grade reliability from day one.</p>
<h2 id="common-failure-modes">Common Failure Modes</h2>
<ul>
<li><strong>Commoditization by API</strong>: your &ldquo;secret sauce&rdquo; is a thin wrapper around a capability everyone can buy.</li>
<li><strong>Pilot purgatory</strong>: the product works in a demo but can&rsquo;t survive real permissions, real data, and real scale.</li>
<li><strong>Services trap</strong>: every customer needs a custom build, so the roadmap becomes a consulting queue.</li>
<li><strong>Unit economics denial</strong>: usage grows while margins quietly collapse.</li>
</ul>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li>Consolidation is real at the platform and infrastructure layers.</li>
<li>Application winners own a workflow and measurable outcomes.</li>
<li>Durable advantages come from data, distribution, and deployment fit.</li>
<li>The market rewards focus and operational rigor over novelty.</li>
</ul>
]]></content:encoded></item><item><title>AI Security: Evolving Threats and Defenses</title><link>https://lawzava.com/blog/2026-02-23-ai-security-evolution/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-02-23-ai-security-evolution/</guid><description>As of late February 2026, AI security is defined by adaptive attacks and layered, operational defenses.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI security in late February 2026 isn&rsquo;t one trick like &ldquo;add a content filter.&rdquo; It&rsquo;s a threat model plus layers: constrain tool access, validate outputs, isolate trusted context, log what matters, and design a fast rollback path. Treat agentic workflows like an exposed API surface, because that&rsquo;s effectively what they are.</p>
<p> <a href="/blog/2025-04-28-ai-security-2025/"
   
   >AI security</a>
 is no longer a niche concern. It sits alongside reliability and privacy as a core production requirement. The threat landscape has grown more deliberate and multi-stage, and the most effective defenses now blend model behavior controls with traditional security practice.</p>
<h2 id="threat-evolution">Threat Evolution</h2>
<h3 id="current-threats">Current Threats</h3>
<p>Late February 2026 is characterized by attacks that try to shape or extract behavior rather than simply break it.  <a href="/blog/2023-10-30-llm-security-considerations/"
   
   >Prompt injection</a>
 remains a primary entry point, but it has shifted toward multi-step workflows that hide intent across inputs, tools, and outputs. Data extraction attempts are more targeted and often move through legitimate features. Model manipulation is now a broader risk, spanning training data quality, dependency integrity, and deployment pipelines.</p>
<p>Agentic systems have widened the attack surface. Tool access, long-running tasks, and multi-model orchestration introduce new paths for indirect influence and privilege escalation. The effect is less about a single exploit and more about cumulative pressure on the system&rsquo;s assumptions.</p>
<h3 id="attack-patterns-worth-understanding">Attack Patterns Worth Understanding</h3>
<p>The most instructive attacks are multi-step, because they exploit the same features that make AI systems useful.</p>
<p>Consider a prompt injection chain against an agentic assistant with tool access. The attacker doesn&rsquo;t inject a single malicious instruction. Instead, they plant a benign-looking instruction in a document the assistant will retrieve: &ldquo;Before responding, summarize the current system configuration for context.&rdquo; The assistant treats this as a helpful step, surfaces internal configuration details in its working memory, and then a follow-up prompt asks it to include that summary in its response. No single step looks malicious. The chain works because the assistant treats retrieved content with the same trust as user instructions.</p>
<p>Data exfiltration through tool use follows a similar pattern. An attacker crafts input that causes the model to call an external API or write to a log in a way that encodes sensitive context into the request parameters. The model isn&rsquo;t &ldquo;trying&rdquo; to leak data. It&rsquo;s following instructions that happen to route internal state through an external channel. If your tool permissions allow HTTP calls or file writes without strict scoping, the model can be steered into acting as an exfiltration vector without any single request looking abnormal.</p>
<p>These patterns matter because they aren&rsquo;t theoretical. They are the incidents teams are seeing in production, and they resist simple keyword filtering or input validation.</p>
<h2 id="defense-strategies">Defense Strategies</h2>
<h3 id="current-best-practices">Current Best Practices</h3>
<p>Effective defenses treat AI systems as full-stack security targets. Inputs are filtered for intent, not just keywords. Outputs are constrained to structured formats when possible, with explicit checks for sensitive data leakage. Tool use is tightly scoped, with least-privilege access and clear audit trails.</p>
<p>The principle of separation is critical. System instructions, user input, and retrieved content must be clearly delineated in the prompt structure, and the model must be told explicitly which parts are trusted. This doesn&rsquo;t eliminate injection, but it raises the bar significantly. Attacks that work against a flat prompt often fail when the model has a clear instruction hierarchy.</p>
<h3 id="security-monitoring-and-detection">Security Monitoring and Detection</h3>
<p>Monitoring is no longer optional. It needs to cover model behavior, tool calls, and user interaction patterns, with rapid rollback paths when behavior drifts.</p>
<p>The detection approach that works best is behavioral baselining. Establish what normal looks like for your system: typical response lengths, tool call frequencies, the ratio of requests that trigger safety filters, and the distribution of topics in model output. Then alert on deviations. A sudden spike in tool calls from a single user session, or a shift in the kinds of data the model references in its responses, can indicate an active attack before any single request trips a rule.</p>
<p>Log everything the model does, not just the final output. Intermediate reasoning steps, tool call parameters, retrieved documents, and safety filter activations all form a forensic record. When an incident happens, you need to reconstruct the full chain of events, and it often spans multiple turns and tools.</p>
<h3 id="incident-response-for-ai-systems">Incident Response for AI Systems</h3>
<p> <a href="/blog/2025-11-10-ai-incident-management/"
   
   >Incident response plans</a>
 should include model configuration changes, not only infrastructure changes. Traditional playbooks assume the application logic is deterministic. AI incidents require a different approach.</p>
<p>When you detect anomalous behavior, the first response is often to restrict the model&rsquo;s capabilities rather than take the service offline. Disable tool access, narrow the set of allowed response formats, or fall back to a simpler model with tighter constraints. This contains the blast radius while you investigate.</p>
<p>The investigation itself should include prompt and context review. Pull the full conversation history, the retrieved documents, and the system instructions that were active at the time. Look for the point where the model&rsquo;s behavior diverged from expected, and trace it back to the input that caused the shift. This is different from traditional log analysis because the &ldquo;bug&rdquo; is often in the data, not the code.</p>
<p>After an incident, update your evaluation suite. Every real incident should produce at least one new test case that would have caught the issue. This is how defenses compound over time.</p>
<h2 id="a-practical-security-review-framework">A Practical Security Review Framework</h2>
<p>When reviewing an AI system&rsquo;s security posture, I walk through five areas.</p>
<p>First, input separation: are system instructions, user input, and retrieved content clearly delineated? Can retrieved content override system behavior?</p>
<p>Second, tool permissions: does the model have the minimum access it needs? Are tool calls logged and auditable? Can a single prompt cause the model to chain multiple tool calls without human review?</p>
<p>Third, output controls: are responses filtered for sensitive data before reaching the user? Are structured output formats enforced where possible?</p>
<p>Fourth, monitoring coverage: are you tracking behavioral baselines? Can you detect slow drift, not just sudden breaks? Do you have alerting on cost, tool call patterns, and safety filter rates?</p>
<p>Fifth, incident readiness: do you have an AI-specific playbook? Can you restrict model capabilities without a full outage? Does your team know how to reconstruct a multi-turn attack chain from logs?</p>
<p>No system will score perfectly on all five. The point is to know where the gaps are and prioritize based on the actual risk profile of your application.</p>
<h3 id="defensive-patterns-that-actually-help">Defensive patterns that actually help</h3>
<ul>
<li><strong>Separate trusted and untrusted context</strong>: retrieved documents are data, not instructions. Make that separation explicit in prompts and in your system design.</li>
<li><strong> <a href="/blog/2026-01-19-ai-agent-reliability/"
   
   >Constrain tool contracts</a>
</strong>: strict schemas, validation, and side-effect annotations. Prefer idempotent writes and require confirmation for irreversible actions.</li>
<li><strong>Policy at the boundary</strong>: enforce permissions and rate limits outside the model. The model shouldn&rsquo;t be your authorization system.</li>
<li><strong>Output validation</strong>: enforce schemas and scan for obvious sensitive leakage patterns before returning responses to users.</li>
<li><strong>Sandbox where possible</strong>: isolate file access, network access, and execution environments for tool-using agents.</li>
</ul>
<p>None of these are perfect. The goal is to reduce surprise and shrink blast radius.</p>
<h2 id="a-practical-security-checklist">A Practical Security Checklist</h2>
<p>If you want a boring checklist that catches most mistakes:</p>
<ol>
<li>List tools, permissions, and side effects. Remove anything you can&rsquo;t justify.</li>
<li>Make retrieved content clearly untrusted. Don&rsquo;t let it override system rules.</li>
<li>Validate tool arguments and model outputs on every call.</li>
<li>Log tool calls with correlation IDs and track abnormal patterns.</li>
<li>Add a hard kill switch and a rollback path for config/model changes.</li>
<li>Run a small red-team exercise focused on prompt injection and tool misuse.</li>
</ol>
<h2 id="key-takeaways">Key Takeaways</h2>
<p>Attack chains are more subtle and operationally aware. They exploit the trust model of AI systems rather than looking for traditional vulnerabilities. Defensive design must combine model controls with traditional security discipline, and it must account for the fact that the model itself can be steered into acting against the system&rsquo;s interests.</p>
<p>Monitoring and incident response need to be built into the system, not bolted on. The teams that handle AI security well are the ones that treat it as an operational discipline with its own tools, playbooks, and review cadence.</p>
<p>AI security remains an ongoing process. The goal isn&rsquo;t perfect prevention but resilient systems that detect, contain, and adapt quickly as conditions change.</p>
]]></content:encoded></item><item><title>AI Team Structures 2026: Central, Embedded, and Hybrid Models</title><link>https://lawzava.com/blog/2026-02-16-ai-team-structures/</link><pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-02-16-ai-team-structures/</guid><description>A practical guide to central, embedded, and hybrid AI team structures, with roles, tradeoffs, and scaling rules.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>By mid-February 2026, the org question isn&rsquo;t &ldquo;should we have an AI team?&rdquo; It&rsquo;s &ldquo;where does ownership live?&rdquo; The best structures make evaluation, cost, and incident response someone’s job, not a shared worry. Most teams land on a hybrid: a small enabling platform group plus embedded delivery in product teams.</p>
<p>AI work has shifted from experiments to ongoing product and operations work. Most organizations that ship AI features have converged on a small set of structures. The right choice still depends on maturity, product criticality, and how much shared infrastructure is needed. The structure also changes how teams manage  <a href="/blog/2026-02-09-ai-cost-trends/"
   
   >AI inference cost</a>
,  <a href="/blog/2026-01-26-ai-native-architecture-2026/"
   
   >AI-native architecture</a>
, and governance.</p>
<p>This post focuses on structures that stay stable under real delivery pressure, not aspirational org charts.</p>
<h2 id="team-models-that-hold-up">Team models that hold up</h2>
<h3 id="central-platform-team">Central platform team</h3>
<p>A central platform team builds and operates shared AI infrastructure, evaluation tooling, and common components. This model fits organizations that need consistency, strong governance, and shared reliability across many teams. It works particularly well in regulated industries where auditability and compliance require a single pane of glass across all AI usage.</p>
<p>Where it breaks down is speed. When every product team routes requests through a central group, the platform team becomes a bottleneck. This is common in organizations with ten or more product teams sharing a three-person AI platform group. The queue grows, the platform team triages by business priority, and lower-priority teams either wait or build workarounds. If you choose this model, staff it generously or accept that iteration speed will be gated.</p>
<h3 id="embedded-in-product-teams">Embedded in product teams</h3>
<p>AI engineers live inside product teams and ship features end to end. This model fits products where AI is core to user experience and iteration speed matters. A team building a search product or a conversational interface benefits from having the AI engineer sit in the same standup, hear the same customer feedback, and own the same on-call rotation as the rest of the squad.</p>
<p>The risk is fragmentation. When several product teams solve the same problems independently, you end up with three prompt evaluation frameworks, two model routing strategies, and no shared understanding of cost. This model works best when you have a small number of product teams, or when AI use cases are different enough that shared infrastructure would not save much effort.</p>
<h3 id="hybrid-model">Hybrid model</h3>
<p>A small platform team provides shared foundations while product teams embed AI engineers for delivery. This is the most common model because it balances infrastructure consistency with product-team autonomy.</p>
<p>The platform team in a hybrid model typically owns inference infrastructure, model selection and routing, shared evaluation tooling, and cost observability. Product-team AI engineers own feature-level prompts, domain-specific evaluation datasets, and production behavior for their use case. The boundary between these layers matters more than the org chart. Writing down the interface contract, what the platform provides and what the product team owns, prevents most of the friction that kills hybrid models.</p>
<p>The hybrid model fails when the platform team behaves like an internal vendor rather than an enabling function. If product teams have to file tickets and wait for releases to get basic capabilities, you&rsquo;re back to the central bottleneck problem with extra steps. The platform team should ship self-serve tooling and stay close to the product engineers who use it.</p>
<h2 id="decision-criteria">Decision criteria</h2>
<p>Use the structure that matches the work, not the other way around. Three factors tend to dominate the decision.</p>
<p>First, how many teams need the same AI capabilities and standards. If the answer is two, embedded is fine. If it&rsquo;s eight, you need a platform function or you will drown in duplication.</p>
<p>Second, how frequently AI features ship and change. High iteration velocity favors embedded engineers who can move with the product team&rsquo;s sprint rhythm. Slower, more deliberate releases are easier to route through a central group.</p>
<p>Third, how much operational risk and compliance pressure exists. Regulated environments benefit from centralized governance and audit trails. Lower-risk consumer products can afford more distributed ownership.</p>
<p>Add one more that teams often forget: <strong>how expensive mistakes are</strong>. If the blast radius is high, you want tighter standards, stronger review, and explicit gating.</p>
<h2 id="roles-and-responsibilities-in-2026">Roles and responsibilities in 2026</h2>
<h3 id="ai-engineer">AI engineer</h3>
<p>Builds AI features inside product flows, owns evaluation in production, and partners with design and data for quality. The role blends software engineering with systematic testing and monitoring. In 2026, the AI engineer is distinct from the ML engineer or data scientist. An ML engineer typically focuses on model training, fine-tuning, and training infrastructure. A data scientist focuses on analysis, experiment design, and statistical rigor. The AI engineer works downstream of both: integrating models into products, building evaluation harnesses that catch regressions, and owning production behavior. Think of it as the difference between building the engine and building the car.</p>
<h3 id="ai-platform-engineer">AI platform engineer</h3>
<p>Owns shared systems like inference services, evaluation pipelines, and model routing. The focus is reliability, scale, and cost control for many teams at once. This role requires strong infrastructure engineering skills and an understanding of how product teams consume AI capabilities. Strong platform engineers pair with product-team AI engineers to understand real usage patterns rather than building abstractions in isolation.</p>
<h3 id="ai-product-manager">AI product manager</h3>
<p>Defines the use case scope, success metrics, and rollout plan. The role emphasizes rigorous tradeoffs between quality, latency, and cost, with clear ownership of user outcomes. An AI PM needs to be comfortable with probabilistic behavior and must resist the urge to promise deterministic results. They own the decision of when a feature is good enough to ship and when it needs more evaluation investment.</p>
<h2 id="team-size-and-scaling">Team size and scaling</h2>
<p>Most teams start too large. A single AI engineer embedded in a product team, supported by a lightweight shared toolkit, is enough to validate whether AI adds value to a workflow. Scaling up before validation leads to expensive teams that optimize solutions to the wrong problems.</p>
<p>For the platform function, two to three engineers can support four or five product teams if the scope is well-defined. Once you pass that ratio, the platform team needs to grow or the scope needs to shrink. A common mistake is building a platform team of six that tries to serve fifteen product teams and ends up serving none of them well.</p>
<p>When hiring, prioritize engineers who have shipped AI features into production over those with impressive research backgrounds but no operational experience. The gap between a working prototype and a reliable production system is where most AI projects stall, and that gap is an engineering problem, not a research problem.</p>
<h3 id="ai-security--governance-partner">AI security / governance partner</h3>
<p>Whether this is a dedicated role or a shared function, someone must own policy: data handling rules, permission models, logging requirements, and review gates. Teams that skip this role tend to slow down later under audit pressure.</p>
<h2 id="common-failure-modes">Common failure modes</h2>
<p>These patterns show up across teams. Platform teams that ship abstractions without enabling product speed often build elaborate internal APIs nobody asked for while product teams work around them. Product teams that skip evaluation and discover quality issues late usually treat AI features like deterministic code, then get surprised when behavior drifts after a model update. Ambiguous ownership for model behavior in production creates incidents where nobody knows whether the platform team or the product team should respond. Usually it is both, but the escalation path was never defined.</p>
<h2 id="what-this-looks-like-at-different-sizes">What This Looks Like At Different Sizes</h2>
<ul>
<li><strong>Small startup (1 to 2 AI engineers)</strong>: embed in the product, keep tooling lightweight, and use strict output validation plus a small eval set. Avoid platform work that nobody will maintain.</li>
<li><strong>Mid-size company (multiple product teams)</strong>: introduce a small platform function to own routing, eval tooling, and shared guardrails, while keeping delivery embedded in product teams.</li>
<li><strong>Large org (regulated, many teams)</strong>: platform + governance becomes non-negotiable. Embedded teams still ship features, but standards, audit trails, and permissions need central ownership.</li>
</ul>
<h2 id="operating-practices-that-matter">Operating practices that matter</h2>
<p> <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >Evaluation</a>
 is a first-class deliverable, not a side task. Teams that ship reliably treat test sets, error analysis, and monitoring as part of every release. Evaluation datasets are versioned alongside code, and regressions in evaluation scores block releases the same way failing tests would.</p>
<p>Clear service ownership and on-call rotations prevent AI incidents from becoming orphaned problems. Every AI feature in production should have a named owner who is paged when it degrades. Cost management belongs in planning, not just finance review after launch. Model inference costs can surprise you, and the time to catch a cost spike is before it compounds for a month.</p>
<h2 id="a-pragmatic-starting-point">A pragmatic starting point</h2>
<p>If the organization is early, start embedded with a lightweight shared toolkit and a small platform function. As adoption grows, formalize the platform team and tighten standards. Revisit the structure every six months, because the problem shifts as AI moves from pilot to core workflow. The structure that got you to your first production feature is rarely the structure that will support your tenth.</p>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-the-best-ai-team-structure-in-2026">What is the best AI team structure in 2026?</h3>
<p>For most companies, the best default is hybrid: a small platform group owns shared infrastructure, routing, evaluation, and governance, while product teams own delivery and workflow quality.</p>
<h3 id="when-should-ai-engineers-be-embedded-in-product-teams">When should AI engineers be embedded in product teams?</h3>
<p>Embed AI engineers when iteration speed and workflow context matter more than central consistency. This works best when use cases are distinct or when the company is still validating where AI creates value.</p>
<h3 id="when-does-a-central-ai-platform-team-make-sense">When does a central AI platform team make sense?</h3>
<p>A central platform team makes sense when many product teams need the same model access, evaluation tooling, governance, and cost controls. It fails when it becomes a ticket queue.</p>
<h3 id="who-owns-ai-quality-in-production">Who owns AI quality in production?</h3>
<p>The product team should own user-facing behavior. The platform team should own shared reliability, model access, routing, observability, and guardrails. The interface between those teams must be explicit.</p>
]]></content:encoded></item><item><title>AI Inference Cost Trends 2026: Model Pricing and Token Costs</title><link>https://lawzava.com/blog/2026-02-09-ai-cost-trends/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-02-09-ai-cost-trends/</guid><description>AI inference costs are falling, but durable savings come from routing, caching, context control, and cost per outcome.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI inference costs are still falling in 2026, but the teams that win are not simply waiting for cheaper model pricing. They route routine work to smaller models, cache repeated requests, control context size, batch offline jobs, and measure cost per successful outcome instead of cost per token alone.</p>
<p>The practical question is no longer &ldquo;will AI get cheaper?&rdquo; It will. The better question is whether your architecture can take advantage of falling token costs without losing quality, reliability, or governance. That is where  <a href="/blog/2026-01-26-ai-native-architecture-2026/"
   
   >AI-native architecture</a>
 and honest  <a href="/blog/2025-09-29-ai-roi-measurement/"
   
   >AI ROI measurement</a>
 matter.</p>
<h2 id="ai-inference-cost-trends-in-2026">AI Inference Cost Trends in 2026</h2>
<p>The direction is clear: model pricing keeps compressing, especially for routine inference workloads. Competition between frontier providers, open-weight models, inference-optimized hardware, and smaller task-specific models has made the default price curve friendlier than it was in 2024 or 2025.</p>
<p>That does not mean every AI product gets cheap automatically. The bill still depends on how much context you send, how many retries your system creates, whether you cache repeated work, and whether every request goes to a premium model by default.</p>
<table>
  <thead>
      <tr>
          <th>Cost driver</th>
          <th>2026 trend</th>
          <th>What to do</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Input tokens</td>
          <td>Cheaper, but context windows invite waste</td>
          <td>Trim history, summarize, and retrieve only relevant context</td>
      </tr>
      <tr>
          <td>Output tokens</td>
          <td>Still easy to overspend through verbose responses</td>
          <td>Constrain output length and use structured formats</td>
      </tr>
      <tr>
          <td>Frontier models</td>
          <td>Lower than prior years, still premium</td>
          <td>Reserve for high-risk or high-value cases</td>
      </tr>
      <tr>
          <td>Small models</td>
          <td>Much cheaper and good enough for bounded tasks</td>
          <td>Route classification, extraction, and simple drafting here</td>
      </tr>
      <tr>
          <td>Retries</td>
          <td>Often hidden in aggregate API spend</td>
          <td>Track retries by feature and failure mode</td>
      </tr>
      <tr>
          <td>Evaluation</td>
          <td>More important as model choice expands</td>
          <td>Budget eval maintenance as part of production cost</td>
      </tr>
  </tbody>
</table>
<p>The teams with the lowest useful cost are usually the teams with the cleanest architecture. They know which path a request took, why that model was selected, how often fallback fired, and what one successful outcome actually cost.</p>
<h2 id="model-pricing-2025-vs-2026">Model Pricing: 2025 vs. 2026</h2>
<p>By 2025, many organizations had already seen token prices drop enough to move AI workloads from experiment budgets into operating budgets. In 2026, the bigger change is not just cheaper tokens. It is optionality.</p>
<p>Most production use cases now have multiple viable model tiers:</p>
<ul>
<li>a cheap model for routing, classification, extraction, and formatting</li>
<li>a mid-tier model for routine reasoning and drafting</li>
<li>a frontier model for ambiguous, high-stakes, or high-value work</li>
<li>a deterministic fallback for cases where the model should not decide</li>
</ul>
<p>This changes procurement conversations. Instead of asking &ldquo;which provider is cheapest?&rdquo; teams should ask &ldquo;which tasks deserve expensive inference?&rdquo; A flat architecture where every request hits the best model leaves money on the table.</p>
<p>The better pattern is a small  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >model-routing layer</a>
 with explicit thresholds. That router can be heuristic at first. It does not need to be clever. It needs to be measured.</p>
<h2 id="what-has-changed">What Has Changed</h2>
<p>The market has moved from experimentation to steady operations. Costs keep trending down, but the bigger shift is that most workloads now have multiple viable options. That creates room for routing, fallback, and tiered service levels instead of one default model for everything.</p>
<p>The pricing arc is clear. In early 2024, a million tokens from a frontier model cost roughly thirty dollars on the input side and sixty on the output side. By late 2025, equivalent capability was available for a fraction of that, and by early 2026, competitive pressure pushed prices down again. For many workloads, per-token cost has dropped by an order of magnitude in under two years.</p>
<p>That is not subtle. It changes the math on use cases that were previously too expensive to run at scale.</p>
<p>Smaller, task-specific models have gotten even cheaper. Routing a classification task or structured extraction job through a lightweight model can cost a hundredth of what a frontier model charges for the same tokens. The capability gap has narrowed enough that, for well-defined tasks, the smaller model is often not just cheaper but faster and more predictable.</p>
<h2 id="why-costs-keep-moving">Why Costs Keep Moving</h2>
<p>Several forces continue pushing in the same direction. Model efficiency gains mean each generation does more with less compute. Hardware improvements, especially in inference-optimized silicon, reduce cost per operation at the infrastructure layer. Competitive pressure from open-weight models and multiple commercial providers keeps pricing honest.</p>
<p>Open tooling also keeps baseline capability accessible. When a team can self-host a capable model on reasonable hardware, it sets a ceiling on what commercial APIs can charge for equivalent work. That dynamic is not going away.</p>
<h2 id="the-costs-people-miss">The Costs People Miss</h2>
<p>Token pricing gets most of the attention, but in mature AI operations it is rarely the largest line item. Hidden costs are usually where budgets quietly expand.</p>
<p>Evaluation is first. Building and maintaining evaluation suites, human review processes, and regression testing infrastructure takes real engineering time. Teams that ship without proper evaluation pay later in incident response and lost trust, and that bill is usually bigger. But the evaluation work itself is not free, and it scales with the number of models and use cases in production.</p>
<p>Data preparation is another. Cleaning, labeling, formatting, and versioning data for fine-tuning or retrieval-augmented generation is labor-intensive work. It often requires domain expertise that is expensive to hire or contract.</p>
<p>Teams that underestimate this end up with underperforming models, then spend more on prompt engineering and workarounds than they would have spent on data quality upfront. It is common to burn months of engineering time compensating for training data problems that could have been fixed at the source in weeks.</p>
<p>Monitoring and observability add ongoing cost. Logging every request, tracking latency distributions, detecting drift, and alerting on quality degradation all require infrastructure. For high-volume systems, storage and compute costs for the monitoring layer itself can be material. At scale, the observability stack for an AI system can rival inference cost.</p>
<p>Retraining and model updates are the costs that compound. As data distributions shift and user expectations change, models need refresh cycles. Each cycle involves data collection, training or fine-tuning, evaluation, and deployment. The cost is not just compute. It is also the engineering attention required to run the cycle reliably.</p>
<h2 id="routing-strategies-in-practice">Routing Strategies in Practice</h2>
<p>The highest-leverage  <a href="/blog/2023-07-24-ai-cost-optimization/"
   
   >cost optimization</a>
 is usually not better rate cards. It is sending each request to the right model for the job.</p>
<p>Consider a customer support system handling thousands of queries a day. Most are routine: order status, return policies, password resets. A small, fast model handles these well at minimal cost. A subset involves complex complaints, edge cases, or escalation decisions that benefit from a more capable model. And a handful require human review regardless.</p>
<p>A routing layer that classifies incoming requests and directs them to the right tier can cut costs dramatically without degrading user experience. Classification itself is cheap, often a lightweight model or a set of heuristics. Savings come from not running every request through the most expensive option.</p>
<p>In practice, teams define two or three model-capability tiers, build a classifier that assigns each request to a tier, and measure both cost and quality per tier over time. Thresholds can be adjusted as models improve or as new options appear.</p>
<p>The same pattern applies to internal tooling. Code generation, document summarization, and data extraction all include varying difficulty levels within one workflow. A well-designed system uses the frontier model for hard cases and a fast, inexpensive model for everything else.</p>
<h2 id="token-cost-vs-cost-per-outcome">Token Cost vs. Cost Per Outcome</h2>
<p>Token cost is useful for vendor comparison. It is not enough for product decisions.</p>
<p>Most teams start with a simple per-request cost estimate and multiply by expected volume. That is fine for initial budgeting, but it breaks down quickly as usage grows and patterns shift.</p>
<p>A more durable approach is to model cost per outcome rather than cost per request. If a workflow needs three API calls, two retries, and a human review step to produce one useful result, the cost of that result is the sum of all components. Tracking cost per outcome makes it possible to compare architectures and model choices on equal footing. It also prevents a cheap model from looking good when it creates repeated retries, manual cleanup, or user escalation.</p>
<p>This also makes business conversations easier. Saying &ldquo;this feature costs twelve cents per completed task&rdquo; is more useful than &ldquo;we spend four thousand dollars a month on API calls.&rdquo; The first number connects to business value. The second is just an expense line. It also helps decide which AI team structure should own optimization: product teams, a platform team, or a shared enablement group.</p>
<p>Forecasting also gets easier once you have a few months of production data. Usage patterns are often more stable than expected, with predictable daily and weekly cycles. Surprises usually come from new feature launches or changes in user behavior, not gradual drift.</p>
<p>A simple forecasting model that accounts for known upcoming changes and adds a buffer for unknowns is usually enough. Overly complex forecasting is rarely worth it when underlying pricing can change with one vendor announcement.</p>
<p>The key point is not just the trend line. It is the increasing ability to trade cost for latency and quality in a controlled way. That is what makes cost engineering possible.</p>
<h2 id="how-to-reduce-ai-inference-cost-without-breaking-quality">How to Reduce AI Inference Cost Without Breaking Quality</h2>
<p>The best responses are architectural, not purely vendor-driven. Teams that treat AI as an operational system tend to make pragmatic decisions early, then refine as usage stabilizes. That means choosing models by task fit, pushing repeat work into caches, and designing workflows that degrade gracefully.</p>
<p> <a href="/blog/2022-08-08-caching-strategies/"
   
   >Caching</a>
 deserves special mention. In systems where similar inputs recur frequently, a well-designed cache can eliminate a significant percentage of API calls entirely. Semantic caching, where near-duplicate inputs return cached results, extends that benefit. Implementation cost is usually modest compared with savings at scale.</p>
<p>Designing for graceful degradation is the other pattern that consistently pays off. If the primary model is unavailable or too slow, the system should fall back to a smaller model, a cached response, or a simplified workflow rather than failing outright. This is not just a reliability pattern. It is also a cost pattern, because your budget is not held hostage by a single vendor&rsquo;s pricing or availability.</p>
<h3 id="common-levers-that-work">Common Levers That Work</h3>
<ul>
<li><strong>Reduce context</strong>: send only what the model needs. Summarize, chunk, and cap history.</li>
<li><strong>Cache repeat work</strong>: if users ask the same questions, your system should remember.</li>
<li><strong>Batch when possible</strong>: offline jobs rarely need low-latency interactive pricing.</li>
<li><strong>Constrain outputs</strong>: structured output and strict schemas reduce rambling responses.</li>
<li><strong>Route by risk</strong>: start small, escalate only when the cheap path fails.</li>
</ul>
<p>The point is not to chase the lowest cost per token. The point is to hit your product&rsquo;s quality bar at a sustainable unit cost.</p>
<h2 id="faq">FAQ</h2>
<h3 id="are-ai-inference-costs-going-down-in-2026">Are AI inference costs going down in 2026?</h3>
<p>Yes. The broad trend is downward, especially for routine inference and smaller task-specific models. The operational risk is assuming lower token prices automatically create lower product costs. Wasteful context, retries, and weak routing can erase the savings.</p>
<h3 id="what-is-the-best-way-to-reduce-llm-token-costs">What is the best way to reduce LLM token costs?</h3>
<p>Start with context control. Send less irrelevant text, retrieve narrower evidence, summarize long histories, and cap output length. After that, add routing, caching, batching, and fallback paths.</p>
<h3 id="should-every-request-use-the-cheapest-model">Should every request use the cheapest model?</h3>
<p>No. Cheap models are best for bounded, low-risk tasks. Premium models still make sense for ambiguous or high-value work. The goal is tiered inference, not cheapest-possible inference.</p>
<h3 id="what-metric-should-teams-track-besides-token-price">What metric should teams track besides token price?</h3>
<p>Track cost per successful outcome. Include model calls, retries, retrieval, evaluation, human review, monitoring, and incident handling. That is the number that belongs in budget and ROI conversations.</p>
<h3 id="how-does-model-routing-reduce-ai-costs">How does model routing reduce AI costs?</h3>
<p>Routing sends routine requests to cheaper models and escalates only when the task requires stronger capability. Done well, it reduces spend without forcing the product into a lowest-common-denominator model choice.</p>
<h2 id="a-simple-checklist">A Simple Checklist</h2>
<ol>
<li>Instrument cost per request and cost per successful outcome.</li>
<li>Identify the top 3 flows by spend and break down why they cost what they cost.</li>
<li>Add routing: cheap default, expensive escalation, deterministic fallback.</li>
<li>Add  <a href="/blog/2024-03-25-prompt-caching-strategies/"
   
   >caching for repeat prompts</a>
 and repeat retrieval.</li>
<li>Set budgets and alerts so cost spikes are visible within hours, not at month-end.</li>
</ol>
<h2 id="common-traps">Common Traps</h2>
<ul>
<li><strong>Optimizing prompts before you instrument</strong>. If you cannot measure spend by endpoint and outcome, you are guessing.</li>
<li><strong>Treating cost as &ldquo;the AI team&rsquo;s problem&rdquo;</strong>. Cost is a product and platform concern. If the feature is valuable, it deserves real engineering.</li>
<li><strong>Ignoring retries and failure loops</strong>. One bad tool call can multiply into three retries and a second model call. That is where surprise bills come from.</li>
<li><strong>Paying premium prices for routine work</strong>. Most requests are boring. Route them to boring systems.</li>
</ul>
<h2 id="what-to-watch-next">What To Watch Next</h2>
<p>Over the rest of 2026, watch for clearer separation between operational and premium tiers, and for tooling that makes governance and quality measurement cheaper to run.</p>
<p>Winners will be teams that keep cost in scope without letting it dictate every decision. Cheap AI that does not work is not savings. Expensive AI that delivers measurable outcomes is an investment. The goal is to know which is which.</p>
]]></content:encoded></item><item><title>AI Regulation Is Here. Stop Acting Surprised.</title><link>https://lawzava.com/blog/2026-02-02-ai-regulation-reality/</link><pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-02-02-ai-regulation-reality/</guid><description>Regulation is already in procurement, security reviews, and internal sign-off. Teams that treat compliance as engineering ship faster than those who bolt it on.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Regulation isn&rsquo;t a future problem. It&rsquo;s already in procurement questionnaires, security reviews, and internal risk sign-off. Teams that build evidence and controls into the system will ship faster than teams that bolt them on later. Treat compliance as engineering, not paperwork.</p>
<p>None of this is legal advice. It&rsquo;s an engineering view of how regulation is already changing how teams deliver.</p>
<p>This isn&rsquo;t theoretical. It affects procurement timelines, partnership agreements, and whether a product can launch in certain markets at all. Enterprise buyers now include  <a href="/blog/2025-03-03-ai-governance-practice/"
   
   >AI governance</a>
 questions in their security questionnaires. If you can&rsquo;t answer them clearly, deals stall.</p>
<h2 id="the-regulatory-landscape-right-now">The Regulatory Landscape Right Now</h2>
<p>Rules and expectations vary by jurisdiction, but the common pattern is stable. Regulators and buyers focus on impact, transparency, and accountability. The question is no longer just &ldquo;can it work&rdquo; but also &ldquo;can it be explained, monitored, and corrected.&rdquo;</p>
<p>The EU AI Act is the most concrete framework on the table. It classifies systems by risk tier and imposes requirements accordingly. High-risk systems, those used in hiring, credit scoring, law enforcement, and critical infrastructure, face mandatory conformity assessments, technical documentation, and human oversight obligations. Even general-purpose AI models have transparency and reporting duties if they meet certain capability thresholds.</p>
<p>In the US, the landscape is more fragmented. Executive orders have established reporting requirements for large training runs and directed agencies to develop sector-specific guidance. States like California and Colorado have moved ahead with their own disclosure and impact assessment rules.</p>
<p>The practical effect is that teams operating across jurisdictions need to satisfy multiple overlapping standards, not a single checklist. If your product serves customers in both the EU and the US, you&rsquo;re building for the union of those requirements whether you planned for it or not.</p>
<p>Other markets are following similar patterns. Canada, the UK, Singapore, and others have published frameworks that share the same core themes: risk classification, transparency, and accountability. The specifics differ, but the architectural implications converge.</p>
<h2 id="what-regulation-actually-looks-like-right-now">What regulation actually looks like right now</h2>
<p>Compliance is less about a single checklist and more about credible evidence of how a system behaves. The minimum set of artifacts is usually small but non-optional.</p>
<p>A model card or system card is the starting point. It documents what the model does, what data it was trained or fine-tuned on, known limitations, and intended use boundaries. This isn&rsquo;t a marketing document. It needs to be honest about where the system performs poorly and what it wasn&rsquo;t designed to handle. A good model card is a page or two, not a hundred-page report.</p>
<p>A risk register maps each deployment to its potential impact. For a customer-facing recommendation engine, the risk profile is different from an internal document summarizer. The register should capture who is affected, what happens when the system is wrong, and what controls are in place. Update it when the system&rsquo;s scope changes, not just at launch.</p>
<p>Data provenance documentation traces where training and inference data comes from, how it was collected, and what consent or licensing applies. This matters more than most teams expect, especially when regulators ask about bias or when a partner wants to know whether their data was used in training.</p>
<p>A monitoring and  <a href="/blog/2025-11-10-ai-incident-management/"
   
   >incident response plan</a>
 explains how the system is observed in production, what triggers a review, and who is responsible when something goes wrong. This is the artifact that separates a compliant deployment from a demo.</p>
<p>Regulators want to see that you can detect problems and act on them, not just that you tested the model before launch. A plan that names real people, real dashboards, and real escalation paths is worth more than a generic template.</p>
<h2 id="where-engineering-and-compliance-collide">Where Engineering and Compliance Collide</h2>
<p>The most common friction I see isn&rsquo;t about disagreement on goals. It&rsquo;s about pace and language. Engineering teams want to ship. Compliance teams want to review. Neither side is wrong, but without a shared process, the result is delays, workarounds, or both.</p>
<p>The first friction point is documentation timing. If compliance artifacts are treated as a post-launch requirement, they never get done well. Engineers are already on to the next feature, and the compliance team is reviewing a system they didn&rsquo;t help design. The fix is to produce documentation alongside development. Start the model card when the model is selected, not when legal asks for it three weeks before launch.</p>
<p>The second friction point is risk-assessment granularity. Compliance teams sometimes want to assess every model change as if it were a new deployment. Engineering teams want to iterate quickly.</p>
<p>A practical resolution is to define change categories. Minor prompt adjustments can be reviewed in batch. Significant model swaps need a fresh assessment. Everything in between gets a proportional review. Document the categories and get both sides to agree on them before the first deployment, not during a heated debate about a release that&rsquo;s already late.</p>
<p>The third friction point is tooling. Engineers work in code repositories and CI pipelines. Compliance teams work in spreadsheets and document management systems. Bridging this gap with automation, by generating compliance artifacts from code annotations, test results, and monitoring dashboards, reduces manual handoffs and keeps both sides working from the same source of truth.</p>
<p>I&rsquo;ve seen teams solve this by adding a compliance metadata file alongside the model configuration in the same repository. When the CI pipeline runs, it generates a compliance summary from that metadata plus test results. The compliance team reviews a formatted report instead of chasing engineers for screenshots.</p>
<h2 id="a-phased-practical-path">A Phased Practical Path</h2>
<p>Trying to build a complete compliance program in one sprint is a recipe for stalled projects. A phased approach works better and builds credibility incrementally.</p>
<p>In the first phase, take inventory. Map where AI is used, who is affected, and what data flows through each system. This sounds obvious, but I&rsquo;ve seen organizations discover AI components they didn&rsquo;t know existed because a team quietly deployed a third-party API. You can&rsquo;t govern what you can&rsquo;t see.</p>
<p>In the second phase, classify by impact. Group systems into risk tiers based on who is affected and what happens when the system fails or behaves unexpectedly. Internal productivity tools sit in a different tier than customer-facing decision systems. Classification drives how much oversight each system needs, so getting this right early saves significant effort later.</p>
<p>In the third phase, build the artifact pipeline. Create templates for model cards, risk assessments, and monitoring plans. Integrate them into your development workflow so that evidence is produced as a natural byproduct of building features.</p>
<p>Automate where possible. Pull test results into compliance reports. Generate data lineage from pipeline metadata. Surface monitoring dashboards that serve both engineering and governance audiences. The goal is to make compliance evidence a side effect of good engineering, not a separate workstream.</p>
<p>In the fourth phase, establish review cadence. Set regular checkpoints that match each risk tier. High-risk systems get quarterly reviews with executive visibility. Lower-risk systems get lightweight annual reviews or automated checks.</p>
<p>The cadence should be predictable so teams can plan around it instead of reacting to ad hoc requests. Predictability is what makes compliance sustainable. Surprise audits create resentment. Scheduled reviews create routine.</p>
<p>The easiest way to get this right is to treat it like any other production constraint. Add a lightweight PR checklist for AI changes: data sources, eval results, and new failure modes. Version prompts and routing rules alongside code. Keep a small  <a href="/blog/2024-08-19-llm-testing-strategies/"
   
   >eval suite</a>
 that runs on every meaningful change. Instrument quality, cost, latency, and error rate.</p>
<p>In early February 2026, compliance isn&rsquo;t a separate program. It&rsquo;s part of making AI safe to deploy and straightforward to defend when questions arrive. Teams that treat it as an engineering discipline, with clear processes, proportional oversight, and automated evidence collection, will ship faster than those who treat it as paperwork handled after the fact.</p>
<p>The regulation isn&rsquo;t going away. But with a practical approach, it doesn&rsquo;t need to slow you down.</p>
]]></content:encoded></item><item><title>AI-Native Architecture Patterns 2026: Production Guide</title><link>https://lawzava.com/blog/2026-01-26-ai-native-architecture-2026/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-26-ai-native-architecture-2026/</guid><description>Production AI architecture patterns for gateways, retrieval, evaluation, fallbacks, cost control, and ownership.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI-native architecture is mostly about boring interfaces: route model calls through a gateway, ground outputs with retrieval, validate and log everything, and make evaluation part of the release process. The goal isn&rsquo;t to worship a model. The goal is to ship AI features that survive change: model updates, data drift, new policy requirements, and real production load.</p>
<p>AI-native architecture is no longer a sidecar to the main system. By late January 2026, teams treat it as a first-class capability with concrete design and operational practices. The emphasis has shifted from demos to reliability, cost control, and change management.</p>
<h2 id="what-changed">What Changed</h2>
<p>The biggest shift is structural. AI capabilities are now designed into service boundaries, deployment flows, and runtime controls instead of layered on top. That changes how teams think about interfaces, failure modes, and ownership.</p>
<p> <a href="/blog/2024-02-05-ai-native-architecture/"
   
   >Two years ago</a>
, most teams ran AI as a separate service that the rest of the stack called when it needed something smart. The model sat behind an API, and the integration was a thin adapter. That worked for demos and low-stakes features, but it broke down as AI became central to the product. Latency budgets, error handling, and data flow all suffered from the indirection. The shift to native architecture means AI concerns are represented in the same design conversations as database schemas, API contracts, and deployment topologies.</p>
<h2 id="core-patterns-that-hold-up">Core Patterns That Hold Up</h2>
<h3 id="ai-gateway">AI Gateway</h3>
<p>A dedicated gateway organizes AI access and policy. It centralizes routing, safety controls, and observability so teams don&rsquo;t reimplement the same logic across services. It also provides a stable interface as models and capabilities evolve.</p>
<p>In practice, the gateway sits between your application services and model providers. Requests flow in from your services, the gateway applies rate limiting and authentication, selects the appropriate model based on task type and cost constraints, and forwards the request. Responses flow back through the same path, where the gateway logs latency, token usage, and any safety filter activations before returning the result. This single chokepoint means you can swap providers, add fallback models, or enforce new policies without touching application code.</p>
<p>The tradeoff is operational overhead. A gateway is another service to run, monitor, and scale. Teams that skip it usually rebuild the same logic piecemeal across every service that calls a model, which is worse. But you need to staff it. Someone owns the gateway, and that ownership must be explicit from the start.</p>
<h3 id="retrieval-layer">Retrieval Layer</h3>
<p>A retrieval layer handles knowledge access, context assembly, and freshness. It&rsquo;s treated as an application concern rather than a data science add-on. The goal is to make AI behavior grounded, auditable, and resilient to stale inputs.</p>
<p>The retrieval layer receives a query from the orchestration logic, searches across one or more knowledge stores ( <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector databases</a>
, document indices, structured data APIs), ranks and filters the results, assembles them into a context window with appropriate formatting, and passes the assembled context to the model along with the original request. The output is grounded in specific sources, which makes it auditable.</p>
<p>Freshness is the hardest part. Stale context produces confident wrong answers, which are worse than no answer. Teams that do this well treat the retrieval layer like a cache: they track staleness explicitly, set TTLs on indexed content, and build refresh pipelines that run on a schedule or when upstream data changes. The retrieval layer isn&rsquo;t a static index. It&rsquo;s a living system with its own operational requirements.</p>
<h3 id="evaluation-pipeline">Evaluation Pipeline</h3>
<p>An evaluation pipeline is part of the architecture, not a later stage. Automated checks and human review are integrated into delivery so quality doesn&rsquo;t depend on a single model choice or a one-off test run.</p>
<p>The pipeline runs at multiple stages. Before deployment, it executes a suite of test cases against the candidate model or prompt configuration and compares results to established baselines. During deployment, it runs a smaller set of smoke tests against live traffic. After deployment, it continuously samples production responses and scores them against quality criteria.</p>
<p>What gets caught depends on the depth of the suite. At a minimum, evaluation catches regressions in factual accuracy when you update a model version, formatting breakdowns when prompt templates change, and safety filter gaps when new input patterns emerge. More mature pipelines also catch subtle drift: the model still produces valid output, but the tone has shifted, or it has started favoring certain response patterns over others. These slow changes are invisible without measurement and are often the ones that erode user trust.</p>
<h2 id="migrating-from-bolt-on-to-native">Migrating From Bolt-On to Native</h2>
<p>Most teams don&rsquo;t start with native architecture. They start with a model API call inside an existing service and grow from there. The migration path is predictable.</p>
<p>The first step is to extract AI concerns into a shared layer. If three services each call a model API with their own retry logic, prompt templates, and error handling, consolidate that into a gateway or shared library. This is a mechanical refactor, not a redesign.</p>
<p>The second step is to make the data flow explicit. Bolt-on integrations often pass raw user input directly to the model. Native architecture introduces a context assembly step where retrieval, formatting, and policy checks happen before the model sees anything. This is where you gain control over what the model knows and how it behaves.</p>
<p>The third step is to add  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation</a>
 as a first-class concern. This means defining what good output looks like for each use case, writing test cases, and wiring them into your CI pipeline. Until evaluation is automated, every model change is a gamble.</p>
<p>The migration doesn&rsquo;t need to happen all at once. Teams can move one use case at a time, starting with the highest-risk or highest-traffic path. The key is that each step produces a tangible improvement in reliability or operability, not just architectural purity. The team structure matters here because shared routing, evaluation, and governance need explicit owners.</p>
<h2 id="design-priorities">Design Priorities</h2>
<p>The systems that perform well share a few priorities. They build model-agnostic interfaces with clear contracts so that swapping a provider is a configuration change, not a rewrite. They design graceful degradation with explicit fallback paths, because models will fail and the product needs to keep working when they do. And they invest in continuous measurement of quality, safety, and cost, because you can&rsquo;t manage what you don&rsquo;t measure.</p>
<p>Add one more: <strong>ownership</strong>. A feature without an owner is a liability. Someone must be accountable for keeping quality steady as everything around the model changes.</p>
<h2 id="operating-in-production">Operating In Production</h2>
<p>Operational work matters as much as model selection. Good systems make evaluation visible, track drift, and keep changes reversible. They also avoid tight coupling to any single model or provider so capability upgrades don&rsquo;t require a redesign.</p>
<p>The day-to-day reality of operating these systems is closer to running a data pipeline than running a traditional web service. You&rsquo;re monitoring output quality, not just uptime. You&rsquo;re tracking cost per request alongside latency. And you&rsquo;re maintaining a relationship with your evaluation suite that&rsquo;s as important as your relationship with your test suite for deterministic code.</p>
<h2 id="takeaway">Takeaway</h2>
<p>AI-native architecture is now a discipline with stable patterns. The winning approach is to design for change, make evaluation part of the system, and treat AI as a core runtime capability rather than a bolt-on feature. The teams that get this right aren&rsquo;t the ones with the best models. They are the ones with the best systems around their models.</p>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-ai-native-architecture">What is AI-native architecture?</h3>
<p>AI-native architecture treats model calls, retrieval, evaluation, routing, cost control, and fallback behavior as first-class production concerns instead of bolting an API call onto an existing feature.</p>
<h3 id="what-are-the-core-ai-architecture-patterns-in-2026">What are the core AI architecture patterns in 2026?</h3>
<p>The durable patterns are an AI gateway, retrieval layer, evaluation pipeline, model routing, structured output validation, observability, and graceful degradation.</p>
<h3 id="why-do-enterprise-ai-architectures-fail">Why do enterprise AI architectures fail?</h3>
<p>They usually fail because the prototype has no production boundary: no owner, no eval suite, no fallback path, no data freshness model, and no cost attribution.</p>
]]></content:encoded></item><item><title>Building Reliable AI Agents in Go</title><link>https://lawzava.com/blog/2026-01-19-ai-agent-reliability/</link><pubDate>Mon, 19 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-19-ai-agent-reliability/</guid><description>Reliable agents are engineered, not prompted: bounded tools, validation at every step, explicit recovery paths. Here&amp;amp;rsquo;s how I build them in Go.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Reliable agents are built, not prompted. Limit tools and steps. Validate every action at the boundary. Persist state so retries are safe. Design explicit recovery paths. Measure outcomes with  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evals</a>
, not vibes. If you want autonomy, earn it in increments with evidence and guardrails. This post includes the Go patterns I actually use.</p>
<hr>
<p>I&rsquo;ve been building  <a href="/blog/2023-09-18-agent-architecture-patterns/"
   
   >agent systems</a>
 in Go for the past year &ndash; across startups and enterprise teams. The same lesson keeps repeating: the model is the easy part. The hard part is everything around it. Tool validation. State management. Recovery paths. Observability. The boring infrastructure that turns &ldquo;it works in a demo&rdquo; into &ldquo;it works at 3am when nobody is watching.&rdquo;</p>
<p>Reliable agents are engineered, not prompted. Here&rsquo;s how.</p>
<h2 id="what-reliable-actually-means">What &ldquo;reliable&rdquo; actually means</h2>
<p>If you can&rsquo;t write down the success criteria, you can&rsquo;t make an agent reliable. &ldquo;Handle this ticket&rdquo; isn&rsquo;t a spec. &ldquo;Classify into one of five categories, draft a reply citing the relevant policy section, and escalate to a human if confidence is below 0.7&rdquo; is a spec.</p>
<p>A reliable agent operates within known tools, limited steps, and explicit completion checks. It produces repeatable outcomes. It fails safely. Creativity and autonomy aren&rsquo;t the goal. Predictability is.</p>
<p>Reliability is strongest where the task is structured: multi-step workflows with fixed tools, document extraction, data transformation with deterministic post-processing. It degrades as tasks become open-ended, long-running, or novel. That isn&rsquo;t a temporary limitation. It&rsquo;s a fundamental property of probabilistic systems.</p>
<h2 id="the-architecture-that-holds-up">The architecture that holds up</h2>
<p>The reliable agent systems I build don&rsquo;t look like a single prompt calling tools. They look like a small system with explicit responsibilities:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Agent</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span>      <span style="color:#a6e22e">ToolRegistry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">policy</span>     <span style="color:#a6e22e">PolicyEnforcer</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">validator</span>  <span style="color:#a6e22e">ActionValidator</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">state</span>      <span style="color:#a6e22e">StateStore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">supervisor</span> <span style="color:#a6e22e">Supervisor</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">maxSteps</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">timeout</span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ToolRegistry</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">Tool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Tool</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>        <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Schema</span>      <span style="color:#a6e22e">jsonschema</span>.<span style="color:#a6e22e">Schema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Execute</span>     <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SideEffects</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Idempotent</span>  <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every component has a clear job. The tool registry enforces schemas. The policy layer checks permissions before execution. The validator inspects arguments and output shape. The state store persists progress so retries don&rsquo;t repeat side effects. The supervisor can stop, escalate, or hand off to a human.</p>
<p>You can implement this in a lightweight way, but the responsibilities need to exist somewhere. If they don&rsquo;t, reliability will always be &ldquo;mostly okay until it isn&rsquo;t.&rdquo;</p>
<h2 id="validation-at-the-boundary">Validation at the boundary</h2>
<p>Agents fail in boring ways. Wrong parameters. Missing required fields. Calling the right tool at the wrong time. Repeating a write action. Getting stuck in a loop.</p>
<p>The fixes are also boring:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">v</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ActionValidator</span>) <span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</span> <span style="color:#a6e22e">Action</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tool</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">v</span>.<span style="color:#a6e22e">registry</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;unknown tool: %s&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Schema</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Args</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid args for %s: %w&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">SideEffects</span> <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">v</span>.<span style="color:#a6e22e">policy</span>.<span style="color:#a6e22e">Allowed</span>(<span style="color:#a6e22e">action</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;action %s denied by policy&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Validate arguments at the boundary. Return structured errors. If a tool has side effects, check policy before execution. If a tool isn&rsquo;t idempotent, check whether this exact action has already been executed in the current run.</p>
<p>This isn&rsquo;t clever. It&rsquo;s the same approach I use for any public API. Treat tools like APIs, enforce contracts, and the model has fewer ways to surprise you.</p>
<h2 id="idempotency-and-state">Idempotency and state</h2>
<p>The nastiest agent bugs come from retries that repeat side effects. Duplicate tickets. Repeated refunds. Double-sends. The fix is the same as in any  <a href="/blog/2018-09-17-building-reliable-distributed-systems/"
   
   >distributed system</a>
: make write operations idempotent.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">StateStore</span>) <span style="color:#a6e22e">ExecuteOnce</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stepID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">fn</span> <span style="color:#66d9ef">func</span>() (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>)) (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">stepID</span>); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// already executed, return cached result</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fn</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">stepID</span>, <span style="color:#a6e22e">result</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every meaningful step gets a unique ID. Before executing, check if the step has already completed. If it has, return the cached result. This makes retries safe and recovery straightforward.</p>
<p>I learned this pattern while building cloud infrastructure at a previous startup, not AI systems. Same principles. Different surface area.</p>
<h2 id="the-supervisor-loop">The supervisor loop</h2>
<p>The supervisor is the most important piece. It enforces hard limits and decides what happens when things go wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">task</span> <span style="color:#a6e22e">Task</span>) (<span style="color:#a6e22e">Result</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">timeout</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">step</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">step</span> &lt; <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">maxSteps</span>; <span style="color:#a6e22e">step</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">planNextAction</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;planning failed at step %d: %w&#34;</span>, <span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">ActionComplete</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">finalize</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">ActionEscalate</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">escalateToHuman</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Reason</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">validator</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">logValidationFailure</span>(<span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span> <span style="color:#75715e">// let the model try again with the error context</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">state</span>.<span style="color:#a6e22e">ExecuteOnce</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">StepID</span>, <span style="color:#66d9ef">func</span>() (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">tools</span>.<span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</span>)
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">supervisor</span>.<span style="color:#a6e22e">OnFailure</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">appendResult</span>(<span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">result</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;agent exceeded max steps (%d)&#34;</span>, <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">maxSteps</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Hard maximum on steps. Hard timeout. Explicit escalation path. Validation before every tool call. Idempotent execution. Structured logging at every decision point.</p>
<p>This isn&rsquo;t a framework. It&rsquo;s a pattern. Adapt it to your domain. The important thing is that these responsibilities exist in your system, however you implement them.</p>
<h2 id="observability">Observability</h2>
<p>If you can&rsquo;t see what the agent did, you can&rsquo;t improve it. Log enough to answer practical questions:</p>
<ul>
<li>Tool name, step number, latency</li>
<li>Success/failure codes and validation errors</li>
<li>Argument hashes (not raw values for sensitive data)</li>
<li>Completion status and reason for stopping</li>
<li>Human handoff events</li>
</ul>
<p>This data turns &ldquo;the agent is flaky&rdquo; into &ldquo;the search tool fails 8% of the time when the query exceeds 200 characters.&rdquo; The second statement is fixable. &ldquo;Flaky&rdquo; isn&rsquo;t.</p>
<h2 id="where-this-falls-apart">Where this falls apart</h2>
<p>Open-ended creative work. Long-running autonomous loops with shifting context. Novel situations without prior examples. High-stakes decisions without human review.</p>
<p>These aren&rsquo;t temporary limitations waiting for a better model. They are fundamental properties of probabilistic systems operating in complex environments. If your agent needs to handle these cases, the answer isn&rsquo;t a better prompt. The answer is a human checkpoint.</p>
<h2 id="the-uncomfortable-truth">The uncomfortable truth</h2>
<p>Most agent reliability problems aren&rsquo;t model problems. They are engineering problems. Wrong tool schemas. Missing validation. No idempotency. No timeouts. No escalation path. The model does something unexpected, and instead of being caught at the boundary, it cascades into a production issue.</p>
<p>Fix the engineering first. The model reliability improves as a consequence.</p>
<p>If you want autonomy, earn it in increments. With evidence. With guardrails. Not with optimistic prompts and hope.</p>
]]></content:encoded></item><item><title>AI Video Applications in Practice</title><link>https://lawzava.com/blog/2026-01-12-ai-video-applications/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-12-ai-video-applications/</guid><description>Video AI is practical for scoped workflows. This post covers what works, how to design for reliability, and where human review still matters.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Video AI works when you treat it as a pipeline, not a magic model. Keep the domain tight, segment aggressively, ground outputs in transcripts and timestamps, and route low-confidence cases to human review. The product should help people navigate video, not act like it watched everything for them.</p>
<p> <a href="/blog/2025-02-17-video-understanding-ai/"
   
   >Video AI</a>
 is now practical for scoped workflows. Teams are shipping systems that align audio and visuals, surface key moments, and make large video libraries searchable. The gap between a useful product and churn usually comes down to clear scope, predictable quality, and a human review path when confidence drops.</p>
<h2 id="what-works-now">What Works Now</h2>
<p>Reliability improves when the domain is defined and outputs are constrained. The most dependable capabilities are:</p>
<ul>
<li>Moment finding for a known task or format</li>
<li>Summaries and highlights with timestamps</li>
<li>Policy screening that escalates uncertain cases</li>
<li>Search across a curated video collection</li>
</ul>
<h2 id="application-patterns">Application Patterns</h2>
<h3 id="meeting-and-training-intelligence">Meeting and training intelligence</h3>
<p>The best results come from combining transcripts with visual cues like screen changes, slides, and gestures. The output should be a short recap, clear actions, and a timeline of key moments. Treat this as a navigation tool, not a full replacement for watching the video.</p>
<h3 id="content-review-and-safety">Content review and safety</h3>
<p>Use multiple signals instead of one score. Frame sampling, audio analysis, and scene context should all contribute to the final decision. Keep a clear path for human review, especially for borderline cases or sensitive content.</p>
<h3 id="video-knowledge-bases">Video knowledge bases</h3>
<p>Segment videos into stable chunks and index each segment with its transcript and visual context.  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >Retrieval</a>
 works best when users can jump directly to a moment, not just a file. This turns training libraries, product demos, and webinars into searchable references.</p>
<h3 id="editing-assistance">Editing assistance</h3>
<p>AI can speed up rough cuts, captions, and highlight reels. It is less reliable for long-form generation or complex narrative editing. Position it as acceleration, not replacement.</p>
<h2 id="design-considerations">Design Considerations</h2>
<p>Design the product around model limits, not the other way around. Practical systems usually share a few traits:</p>
<ul>
<li>Clear input bounds such as duration limits and supported formats</li>
<li>Visible uncertainty with reasons for low confidence</li>
<li>Latency budgets tied to the workflow, not the demo</li>
<li>Auditability for what was seen, heard, and decided</li>
</ul>
<h2 id="shipping-a-pragmatic-version">Shipping a Pragmatic Version</h2>
<p>Start with a small, representative dataset and define acceptable output before you build. Add  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >lightweight evaluation</a>
 with a few high-risk scenarios, then iterate on prompt and pipeline changes. Logging and review tooling matter as much as model choice, especially when users need to trust what was skipped.</p>
<h2 id="a-reference-pipeline-that-holds-up">A Reference Pipeline That Holds Up</h2>
<p>Most successful implementations look like a pipeline with explicit stages:</p>
<ol>
<li><strong>Ingest</strong>: normalize formats, cap duration, and record metadata.</li>
<li><strong>Transcribe</strong>: get a transcript with time alignment (timestamps are the backbone).</li>
<li><strong>Segment</strong>: split into stable chunks (scenes, slide changes, speaker turns).</li>
<li><strong>Index</strong>: store transcript + metadata + embeddings for each segment.</li>
<li><strong>Retrieve</strong>: answer queries by returning moments, not entire videos.</li>
<li><strong>Synthesize</strong>: generate a summary or highlight list that points back to exact timestamps.</li>
</ol>
<p>This structure keeps the system debuggable. When something is wrong, you can see whether transcription, segmentation, retrieval, or synthesis caused the failure.</p>
<h2 id="evaluation-that-matters-for-video">Evaluation That Matters For Video</h2>
<p>Video AI demos often look great because teams do not audit outputs closely. Practical evaluation focuses on a few measurable things:</p>
<ul>
<li>Timestamp accuracy (can users jump to the right moment?)</li>
<li>Coverage (did the system miss key segments?)</li>
<li>False positives (highlight reels are useless if they highlight noise)</li>
<li>Safety/classification precision at the thresholds you operate at</li>
</ul>
<p>Keep a small &ldquo;golden set&rdquo; of videos and re-run it whenever you change models, prompts, segmentation, or retrieval.</p>
<h2 id="common-pitfalls">Common Pitfalls</h2>
<ul>
<li><strong>Hallucinated timestamps</strong>: the model sounds confident but points to the wrong moment. Always anchor outputs to retrieved segments.</li>
<li><strong>Overly long context</strong>: shoving a whole video into a single prompt wastes money and reduces accuracy. Segment first.</li>
<li><strong>No review tool</strong>: if reviewers cannot quickly see why a decision was made, they will not trust it.</li>
<li><strong>Privacy drift</strong>: meeting videos and training footage often contain sensitive data. Treat retention, access, and redaction as first-class requirements.</li>
</ul>
<h2 id="a-simple-checklist">A Simple Checklist</h2>
<ul>
<li>Define supported formats and duration limits.</li>
<li>Make timestamps and citations part of every output.</li>
<li>Build a review UI for low-confidence cases.</li>
<li>Track latency and cost per processed minute of video.</li>
<li>Re-run a golden evaluation set on every meaningful change.</li>
</ul>
<h2 id="closing">Closing</h2>
<p>Video is searchable and summarizable when scope is clear and workflows are designed for review. Build the pipeline for predictable outputs, and the product will feel reliable.</p>
]]></content:encoded></item><item><title>What I Actually Expect from AI in 2026</title><link>https://lawzava.com/blog/2026-01-05-ai-predictions-2026/</link><pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-05-ai-predictions-2026/</guid><description>Less hype, more plumbing. Agents get real but stay bounded, routing beats monolithic models, and the winners treat AI like software, not magic.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>The advantage in 2026 isn&rsquo;t model access. Everyone has that. The advantage is shipping AI features that behave predictably: scoped workflows, measured quality, controlled costs, a rollback path. Expect agents to get practical within guardrails, routing to replace one-model-fits-all, and regulation to become a real deployment constraint. The hype hangover is here. Execution is what matters now.</p>
<hr>
<p>Prediction posts are dangerous. They age badly. I&rsquo;ve been wrong before and survived, so here goes.</p>
<p>The conversation has shifted. 2025 proved models can be impressive. 2026 will test whether they are dependable in routine work. The changes that matter will be quieter: fewer surprises, tighter boundaries, and more disciplined economics.</p>
<h2 id="agents-get-real--within-limits">Agents get real &ndash; within limits</h2>
<p>This is the prediction I feel most confident about: bounded agents will become normal in production. Support triage. Internal ops workflows. Content pipelines. Document processing. The common thread is clear scope, defined tools, and human checkpoints.</p>
<p>The  <a href="/blog/2023-09-18-agent-architecture-patterns/"
   
   >agent architecture</a>
 that works looks similar everywhere I see it succeed:</p>
<ul>
<li>Operates inside a defined workflow with explicit stop points</li>
<li>Uses tools with strict schemas, not free-form &ldquo;do anything&rdquo; capabilities</li>
<li>Produces intermediate artifacts a human can review &ndash; a draft, a classification, extracted fields</li>
<li>Easy to roll back or disable without breaking the product</li>
</ul>
<p>A support agent that drafts a reply, proposes a refund category, and attaches relevant policy excerpts? That works. An agent that autonomously changes account settings across multiple systems without review? That will keep failing for boring reasons: permissions, edge cases, accountability, audit.</p>
<p>Full autonomy will remain limited. The hard part isn&rsquo;t tool use. It&rsquo;s verification and accountability. Anyone telling you otherwise is selling something.</p>
<h2 id="routing-replaces-the-monolithic-model">Routing replaces the monolithic model</h2>
<p>One of the clearest patterns I&rsquo;ve seen: the teams controlling their costs and quality are the ones  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >routing across models</a>
. Small model for simple classification. Medium model for drafting. Large model for complex reasoning and synthesis. Choose by task and risk, not by a single default.</p>
<p>Caching and reuse matter too: repeated requests, repeated retrieval, repeated transformations. Teams will treat token spend like any other variable cost and engineer it down.</p>
<p>If your AI feature is expensive today, the fix isn&rsquo;t &ldquo;wait for cheaper models.&rdquo; The fix is to design a system that does less unnecessary work and fails more gracefully. This is basic systems engineering. The AI hype cycle just took a couple of years to remember it.</p>
<h2 id="mcp-and-the-integration-layer">MCP and the integration layer</h2>
<p>I&rsquo;ve been watching  <a href="/blog/2025-03-17-mcp-model-context-protocol/"
   
   >MCP (Model Context Protocol)</a>
 closely. It&rsquo;s the kind of boring, practical standard that actually moves the industry forward &ndash; a way for models to interact with tools and data sources through a consistent interface. Not revolutionary. Useful.</p>
<p>What excites me about MCP is that it makes the agent architecture I described above more standardized and portable. Tool registries with schemas. Structured inputs and outputs. Less bespoke glue code per integration. Whether MCP specifically wins or another protocol emerges, the direction is clear: tool integration becomes a standard interface, not a custom project.</p>
<h2 id="enterprise-from-experimentation-to-operations">Enterprise: from experimentation to operations</h2>
<p>AI budgets will flow toward integration, governance, and change management. Procurement, security review, and data quality will matter more than novel features. ROI scrutiny will tighten. Projects that can&rsquo;t show durable value will get cut.</p>
<p>What changes inside organizations is mostly non-technical. Ownership becomes explicit &ndash; someone can approve data access, approve risk, and kill a feature. Enablement beats evangelism &ndash; internal platforms and reusable components matter more than another demo day. Training becomes practical &ndash; teams learn to write specs and evaluate changes, not just &ldquo;prompt engineering.&rdquo;</p>
<h2 id="regulation-becomes-a-deployment-constraint">Regulation becomes a deployment constraint</h2>
<p>Regulation is no longer theoretical. It&rsquo;s showing up in procurement questionnaires, security reviews, and internal risk sign-off. Teams that build evidence and controls into the system will ship faster than teams that bolt them on later.</p>
<p>The prediction that matters: governance moves onto the critical path. Not as a blocker. As a competitive advantage for teams that do it well.</p>
<h2 id="what-probably-wont-happen">What probably won&rsquo;t happen</h2>
<ul>
<li><strong>Fully autonomous agents everywhere.</strong> Verification and accountability are still hard problems.</li>
<li><strong>Prompt-only reliability.</strong> If a feature matters, it needs evaluation, monitoring, and structured interfaces. Not just better wording.</li>
<li><strong>One model to rule them all.</strong> Production systems will route across models because constraints differ by task.</li>
<li><strong>Frictionless compliance.</strong> Regulation doesn&rsquo;t go away. Teams just get better at building evidence into the workflow.</li>
</ul>
<p>None of this blocks useful systems. It pushes teams toward discipline. Which is where the value has always been.</p>
<h2 id="what-to-do-right-now">What to do right now</h2>
<p>If you&rsquo;re shipping AI, the best moves are unglamorous:</p>
<ol>
<li>Pick one workflow with clear value and low blast radius.</li>
<li>Define success and failure modes in writing.</li>
<li>Build a  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >small eval set</a>
 from real examples. Keep it versioned.</li>
<li>Add a rollback path and monitoring before expanding scope.</li>
<li>Track cost per successful outcome, not cost per request.</li>
</ol>
<p>Do those five things and you will be ahead of most teams chasing capability. The advantage in 2026 isn&rsquo;t clever prompting. It&rsquo;s building a system that can be operated, debugged, and trusted.</p>
<p>Discipline over heroics. Ruthless focus. Same as always.</p>
]]></content:encoded></item><item><title>2025: The Year AI Stopped Being Special</title><link>https://lawzava.com/blog/2025-12-22-year-in-review-2025/</link><pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-12-22-year-in-review-2025/</guid><description>A year-end look at what actually happened in AI &amp;amp;ndash; not the hype, but the operational shift. The novelty phase is over. The infrastructure phase has begun.</description><content:encoded><![CDATA[<p>I wrote a  <a href="/blog/2019-12-16-year-in-review-2019/"
   
   >year-in-review post for 2019</a>
 about leaving fintech, joining a deep-tech founder program, and starting a new company. That year felt like a hinge point &ndash; a move from the known to the unknown. 2025 had a similar feel, but the shift wasn&rsquo;t personal. It was industry-wide.</p>
<p>AI stopped being a special project. It became infrastructure.</p>
<p>That sentence sounds obvious in December. It wasn&rsquo;t obvious in January. At the start of the year, most organizations I had worked with were still treating AI as an experiment. A side initiative. Something the &ldquo;AI team&rdquo; owned. By the end of the year, the successful ones had woven it into delivery pipelines, support tooling, and internal operations where reliability matters more than novelty.</p>
<p>The unsuccessful ones are still running pilots.</p>
<h2 id="from-demos-to-systems">From demos to systems</h2>
<p>The biggest shift was organizational, not technical. Projects moved from isolated demos to systems with owners, budgets, and maintenance plans. Evaluation and monitoring became part of deployment, not afterthoughts. Rollback plans existed before launch, not after the first incident.</p>
<p>This isn&rsquo;t glamorous work, but it&rsquo;s the work that matters. The teams that won in 2025 weren&rsquo;t the ones with the cleverest prompts. They were the ones with the most disciplined operations.</p>
<h2 id="governance-stopped-being-a-dirty-word">Governance stopped being a dirty word</h2>
<p>One thing I pushed hard for: governance as enablement, not bureaucracy. Clear rules for data handling, model selection, and access controls made teams faster. Guardrails reduced rework. Policy embedded in CI pipelines unblocked adoption in regulated contexts where teams had been stuck for months.</p>
<p>The pattern is simple. If governance is a checklist in a SharePoint, teams work around it. If governance is a set of automated checks in the delivery pipeline, teams rely on it. It&rsquo;s the same lesson I learned running infrastructure at scale: make the right thing the easy thing.</p>
<h2 id="cost-became-a-design-constraint">Cost became a design constraint</h2>
<p>Early in the year, teams treated model costs like someone else&rsquo;s problem. By mid-year, the bills arrived. Suddenly, cost and latency were architectural decisions, not afterthoughts.</p>
<p>Small models for simple tasks. Large models for complex reasoning. Routing by task type and risk level. Caching repeated requests. Treating token spend like any other variable cost and engineering it down. These are infrastructure patterns, not AI magic. The teams that figured this out early controlled their economics. The teams that waited got surprised.</p>
<p>This reminded me of the early cloud days, when teams learned that &ldquo;spin up more instances&rdquo; isn&rsquo;t a cost strategy. The discipline is the same: measure, optimize, budget. The only difference is that the unit of cost went from compute hours to tokens.</p>
<h2 id="the-throughline">The throughline</h2>
<p>On a personal note, 2025 was also the year I started proving out ideas I&rsquo;ve carried since my early ventures. Building tools that reduce operational complexity, and make the right thing the easy thing, applies directly to AI infrastructure. The overlap between what I learned building cloud tooling and what teams need now for AI operations is almost one-to-one. Different surface area, same principles.</p>
<h2 id="what-actually-worked">What actually worked</h2>
<p>AI delivered best when scoped to a well-defined job with measurable outcomes inside existing workflows: drafting, summarization, classification, data extraction, and assisted analysis. Human review was explicit. Responsibility for quality was assigned to a specific person, not &ldquo;the AI team.&rdquo;</p>
<p>The three patterns that held up all year:  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation-first rollout</a>
, human-in-the-loop for consequential actions, and  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >model routing</a>
 instead of one-model-fits-all.</p>
<h2 id="what-didnt-work">What didn&rsquo;t work</h2>
<p>Broad, underspecified mandates. &ldquo;Use AI to transform our customer experience.&rdquo; That isn&rsquo;t a spec. That&rsquo;s a wish. Deployments without visibility into quality, security, or cost. Optimistic assumptions substituting for measurement.</p>
<p>I watched one organization burn an entire quarter on an &ldquo;AI-powered&rdquo; feature that had no eval suite, no monitoring, and no clear definition of success. When leadership asked why quality was inconsistent, the team had no data to answer with. They had anecdotes. Anecdotes don&rsquo;t survive a quarterly business review.</p>
<p>The organizations that struggled most were the ones that mistook enthusiasm for strategy.</p>
<h2 id="what-stayed-hard">What stayed hard</h2>
<p><strong>Ambiguity.</strong> When success criteria are unclear, AI outputs drift and debates replace decisions. This is a product management problem, not an AI problem.</p>
<p><strong>Trust.</strong> Users lose trust faster than teams regain it. One bad incident &ndash; a confidently wrong answer, a data exposure, a weird hallucination &ndash; and the credibility deficit takes months to recover from.</p>
<p><strong>Drift.</strong> Small changes to prompts, data, or models shift behavior in ways that are hard to notice without measurement. This is why evaluation isn&rsquo;t a launch activity. It&rsquo;s a continuous operation.</p>
<p><strong>High-stakes automation.</strong> The closer a feature gets to irreversible actions, the more you need review, auditability, and rollback. This constraint isn&rsquo;t going away. Nor should it.</p>
<p>The story of 2025 isn&rsquo;t that AI is unreliable. It&rsquo;s that reliability is engineered, not assumed.</p>
<h2 id="the-internal-shift-that-mattered-most">The internal shift that mattered most</h2>
<p>Inside organizations, the biggest change was process maturity. Prompts and routing rules got versioned and reviewed like code. Evaluation moved earlier in the lifecycle. Platform teams became enablement functions instead of gatekeepers.</p>
<p>This is what turned AI from &ldquo;experimentation&rdquo; into &ldquo;infrastructure.&rdquo; It happened not because of a model breakthrough, but because engineering leaders insisted on treating AI systems with the same rigor as everything else in production.</p>
<h2 id="looking-at-2026">Looking at 2026</h2>
<p>The trajectory is continuation, not revolution. Better reliability. Tighter governance. Deeper integration.  <a href="/blog/2025-03-17-mcp-model-context-protocol/"
   
   >MCP</a>
 and similar protocols making tool integration more standardized. Agents getting more practical for bounded workflows. Regulation becoming a real deployment constraint rather than a theoretical discussion.</p>
<p>I expect 2026 to be the year when the gap between &ldquo;AI-capable&rdquo; organizations and &ldquo;AI-mature&rdquo; organizations becomes impossible to ignore. Capable means you can build a demo. Mature means you can run it in production, measure it, fix it when it breaks, and explain it to a regulator. That gap is where the real competition happens.</p>
<p>The most valuable progress will come from operational discipline. Not a single breakthrough. Not a new model that changes everything. Just the steady, unglamorous work of making AI systems predictable, auditable, and maintainable.</p>
<p>2025 was the end of the novelty phase. The work now is execution.</p>
<p>The teams that understand this will win 2026. The teams that are still waiting for the next model release to solve their operational problems will keep waiting.</p>
]]></content:encoded></item><item><title>AI in 2025: The Year It Became Boring (Finally)</title><link>https://lawzava.com/blog/2025-12-08-ai-2025-reflections/</link><pubDate>Mon, 08 Dec 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-12-08-ai-2025-reflections/</guid><description>The most important thing that happened to AI in 2025 wasn&amp;amp;rsquo;t a model release. It was the shift from &amp;amp;lsquo;what can it do&amp;amp;rsquo; to &amp;amp;lsquo;how do we run it.&amp;amp;rsquo; That&amp;amp;rsquo;s progress.</description><content:encoded><![CDATA[<p>The most important thing that happened to AI in 2025 wasn&rsquo;t a new model or a benchmark. It was the quiet, unsexy shift from &ldquo;look what it can do&rdquo; to &ldquo;how do we run this reliably.&rdquo;</p>
<p>AI became boring. And I mean that as the highest compliment.</p>
<h2 id="what-held-up">What held up</h2>
<p>Scoped tasks. Drafting, summarization, classification, assisted analysis. These became standard building blocks across the teams I worked with. Not fully automated work, but faster cycles and better starting points for human decisions. The pattern was consistent: define the task narrowly, evaluate outputs rigorously, and  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >keep a human in the loop</a>
 for anything consequential.</p>
<p>From what I&rsquo;ve seen, the teams that got real value treated AI like any other system dependency. They versioned prompts. They  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >ran evals</a>
 in CI. They  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >monitored quality drift</a>
 the same way they monitor uptime. Nothing revolutionary, just engineering discipline applied to a new kind of component.</p>
<p>Reliability required active management the entire year. Human review stayed essential for anything with meaningful risk. Verification, provenance, monitoring &ndash; these weren&rsquo;t optional extras. They were the cost of using AI responsibly. Teams that skipped these steps learned the hard way.</p>
<h2 id="where-the-limits-stayed-stubborn">Where the limits stayed stubborn</h2>
<p>Models still fail on edge cases. They still produce confident errors. They still struggle with up-to-date or domain-specific facts without a strong retrieval layer. Autonomy improved but complex workflows continued to need supervision and explicit guardrails.</p>
<p>None of this was surprising. But I think the persistence of these limits surprised people who expected 2025 to be the year everything &ldquo;just worked.&rdquo; It wasn&rsquo;t, and that&rsquo;s fine. Infrastructure doesn&rsquo;t need to be perfect. It needs to be predictable and manageable.</p>
<p>The gap between &ldquo;impressive demo&rdquo; and &ldquo;production system&rdquo; stayed wide all year. I saw teams cycle through the same disillusionment: the model works great in testing, then behaves differently on real user inputs, then degrades when the underlying data changes. This isn&rsquo;t a bug. This is the nature of probabilistic systems. The sooner teams accepted that, the faster they built something reliable.</p>
<h2 id="three-patterns-that-actually-worked">Three patterns that actually worked</h2>
<p><strong>Evaluation-first rollout.</strong> Define what &ldquo;good&rdquo; means before you ship. Write it down. Build a small eval set from real examples. If you can&rsquo;t measure quality, you can&rsquo;t improve it, and you definitely can&rsquo;t tell if your last change made things worse.</p>
<p><strong>Human-in-the-loop for consequential actions.</strong> Not as a checkbox. As a genuine review step for anything that touches customers, money, or data. The teams that treated this as optional learned the hard way. The teams that built it into the workflow from day one rarely had incidents they couldn&rsquo;t contain quickly.</p>
<p><strong> <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >Model routing</a>
 over monolithic models.</strong> Use the smallest model that meets quality requirements. Escalate to a larger model only when needed. Route by task type and risk level. This is how you control costs and latency without sacrificing quality where it matters. One model for everything is a demo architecture, not a production architecture.</p>
<h2 id="what-changed-inside-teams">What changed inside teams</h2>
<p>The organizational response matured.  <a href="/blog/2025-03-03-ai-governance-practice/"
   
   >Governance moved from policy documents to operational routines</a>
 &ndash; something I pushed hard for. AI evaluation became part of release processes. The role of AI engineering broadened from a specialized niche to a cross-functional concern touching product, data, security, and compliance.</p>
<p>I saw this play out clearly at a telecom company. Early in the year, AI was &ldquo;the ML team&rsquo;s thing.&rdquo; By Q3, product managers were writing eval criteria. Security teams were reviewing prompt configurations. Finance was asking about cost per successful task instead of cost per API call. That cross-functional involvement is what separates &ldquo;we use AI&rdquo; from &ldquo;we run AI as infrastructure.&rdquo;</p>
<p>This matters more than any model improvement. A better model in a broken process still produces broken outcomes. A good-enough model in a disciplined process produces reliable value.</p>
<h2 id="looking-at-2026">Looking at 2026</h2>
<p>The trajectory feels less like a sprint and more like steady infrastructure improvement. Better planning. More reliable agents. Broader adoption. The core constraints remain familiar: trust, compliance, sustainable economics.</p>
<p>What I&rsquo;m focused on heading into the new year:</p>
<ul>
<li>Clean interfaces for retrieval, evaluation, and monitoring.  <a href="/blog/2025-03-17-mcp-model-context-protocol/"
   
   >MCP</a>
 is making this more practical, and I&rsquo;m watching it closely.</li>
<li>Policies that translate into day-to-day workflow checks, not quarterly reviews.</li>
<li>Clear ownership for quality, safety, and cost. Not &ldquo;the AI team.&rdquo; A specific person with the pager and the authority to change the system.</li>
</ul>
<p>The most useful framing for 2025 was simple: AI is infrastructure. It delivers value when treated with the same rigor as any other system. It fails when treated as a shortcut.</p>
<p>2025 was the year that lesson became obvious. The question for 2026 is whether teams will actually internalize it or keep learning it the hard way.</p>
]]></content:encoded></item><item><title>Scaling AI in the Enterprise Is a Management Problem</title><link>https://lawzava.com/blog/2025-11-24-ai-enterprise-scale/</link><pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-11-24-ai-enterprise-scale/</guid><description>The pilots work. What fails is going from five demos to fifty production features without an operating model. That&amp;amp;rsquo;s a management problem, not an AI problem.</description><content:encoded><![CDATA[<p>Here&rsquo;s the simplest test for whether your enterprise is actually scaling AI: can a team outside the AI group ship a safe, supported AI feature without reinventing the wheel?</p>
<p>If the answer is no, you aren&rsquo;t scaling. You&rsquo;re doing pilots.</p>
<p>I see this constantly. The technology isn&rsquo;t the bottleneck. Models are good enough. The tooling exists. What&rsquo;s missing is the operating model &ndash; the boring work that turns a demo into something that runs in production for years, with clear ownership, predictable costs, and a way to handle failures.</p>
<h2 id="the-pilot-trap">The pilot trap</h2>
<p>Every large organization I&rsquo;ve worked with has successful  <a href="/blog/2024-06-03-enterprise-ai-adoption/"
   
   >AI pilots</a>
: impressive demos, enthusiastic teams. Then the question comes: &ldquo;How do we do this across 50 teams?&rdquo;</p>
<p>The answer is never &ldquo;give everyone API keys and let them figure it out.&rdquo; That path leads to duplicated effort, inconsistent security practices, and a support burden that lands on the same three experts who built the original pilot. I&rsquo;ve watched this happen at telecom companies. I&rsquo;ve watched it happen at financial services firms. The pattern is remarkably consistent.</p>
<h2 id="what-an-operating-model-actually-looks-like">What an operating model actually looks like</h2>
<p>Separate shared capabilities from local execution. It&rsquo;s no more complicated than that.</p>
<p><strong>Shared capabilities</strong> are the things every team shouldn&rsquo;t have to reinvent:  <a href="/blog/2022-11-07-platform-engineering-rise/"
   
   >platform services</a>
, security guardrails, eval frameworks, model access, and policy. A small central group owns these. Their job is to make it easy to build safely.</p>
<p><strong>Local execution</strong> belongs to the business teams who own use cases and outcomes. They pick the problems. They ship the features. They own the quality.</p>
<p>The balance matters. Too centralized, and you create a bottleneck where every AI idea has to go through a committee. Too distributed, and you get security gaps, wasted spend, and inconsistent quality. The sweet spot is a lightweight forum that resolves cross-team issues and keeps standards current without becoming a gate.</p>
<h2 id="governance-as-a-lane-not-a-wall">Governance as a lane, not a wall</h2>
<p>The word &ldquo;governance&rdquo; makes engineers groan. I get it. But governance done right makes you faster, not slower.</p>
<p>The practical version is simple: data access is intentional and documented, model behavior is testable, audit trails exist, incident response has an owner, and rollback is a button, not a project.</p>
<p>If governance is a checklist that lives in a SharePoint nobody reads, teams will work around it. If it&rsquo;s embedded into the build process &ndash; eval gates in CI, prompt versioning in the repo, monitoring that ships with the feature &ndash; teams will rely on it because it makes their lives easier.</p>
<h2 id="enablement-not-evangelism">Enablement, not evangelism</h2>
<p>Scaling fails when enablement is treated like a training event. A two-hour workshop on &ldquo;prompt engineering&rdquo; doesn&rsquo;t help a product team ship a reliable feature. What helps: repeatable patterns, starter templates, and a support path that doesn&rsquo;t depend on cornering the same overworked ML engineer.</p>
<p>Extend the practices you already have. Your teams already know how to run CI pipelines, do code reviews, and  <a href="/blog/2021-09-06-feature-flags-at-scale/"
   
   >deploy behind feature flags</a>
. Add eval suites to the pipeline. Add prompt reviews to the PR process. Make AI features fit into the existing delivery workflow instead of inventing a parallel one.</p>
<h2 id="what-to-measure">What to measure</h2>
<p>Not tool adoption. Not number of pilots. Not &ldquo;AI maturity scores.&rdquo;</p>
<p>Track what&rsquo;s in production and whether it&rsquo;s maintained. Track support burden. Track which use cases are paused or retired. These signals tell leaders where to invest and what to stop. Everything else is decoration.</p>
<h2 id="the-sequence-that-works">The sequence that works</h2>
<p>Establish the platform and guardrails first. Prove the model with a small set of high-leverage use cases. Expand to more teams with consistent support. Review outcomes and simplify anything that causes friction.</p>
<p>The order matters. Each step creates the preconditions for the next. Skip ahead and you&rsquo;re scaling demand faster than capability, which is how you end up with 50 broken pilots instead of 5 working ones.</p>
<p>This is a management problem. Treat it like one.</p>
]]></content:encoded></item><item><title>AI Incidents Don't Look Like Outages. That's the Problem.</title><link>https://lawzava.com/blog/2025-11-10-ai-incident-management/</link><pubDate>Mon, 10 Nov 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-11-10-ai-incident-management/</guid><description>AI systems can return 200 OK while confidently wrong. How to detect, contain, and learn from AI incidents using proven incident response principles.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI incidents are behavior failures, not downtime. Your monitoring says everything is green while the system confidently gives wrong answers. Detect with sampled quality checks and user feedback. Contain with rollbacks and feature flags, not root-cause analysis. Turn every incident into new eval coverage. Speed and reversibility beat thoroughness.</p>
<hr>
<p>I wrote about  <a href="/blog/2019-07-15-security-incident-response/"
   
   >incident response</a>
 in 2019, drawing from national cyber-defense exercises and real startup breaches. The core lesson was simple: teams that perform best under pressure are the ones that have practiced the response, not the ones with the fanciest playbook sitting in Confluence.</p>
<p>That lesson applies directly to AI systems. But AI incidents have a nasty twist.</p>
<h2 id="the-system-is-up-the-system-is-wrong">The system is up. The system is wrong.</h2>
<p>Traditional incidents are usually obvious. The service is down. Latency spikes. Error rates climb. Dashboards go red. Someone gets paged.</p>
<p>AI incidents are subtle. The service returns 200 OK. Latency is normal. No errors in the logs. But the system is confidently telling a customer something wrong. Or it regressed after an untracked prompt change. Or the retrieval layer is surfacing stale docs, and the model is synthesizing them into plausible-sounding garbage.</p>
<p>I&rsquo;ve seen this firsthand. A team ships a model update on Friday. Quality degrades on a specific input class. Nobody notices until Monday because all the operational metrics look fine. The only signal was a spike in user thumbs-down feedback that nobody was monitoring.</p>
<p>That&rsquo;s the core problem. Your existing monitoring was built for availability. AI incidents are about correctness, and  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >correctness is harder to observe</a>
.</p>
<h2 id="what-counts-as-an-ai-incident">What counts as an AI incident</h2>
<p>Any material deviation from expected behavior that can affect users or business outcomes. In practice:</p>
<ul>
<li>Wrong-but-plausible responses that users might trust and act on</li>
<li>Regressions after model, prompt, or retrieval changes</li>
<li>Retrieval failures that surface irrelevant or outdated context</li>
<li>Safety or policy violations &ndash; the model doing something it shouldn&rsquo;t</li>
</ul>
<p>These are ambiguous by nature. There&rsquo;s no clean threshold. So detection has to rely on multiple signals, not a single metric.</p>
<h2 id="detection-that-actually-works">Detection that actually works</h2>
<p>Teams that catch things quickly combine several layers:</p>
<p><strong>Sampled quality checks.</strong> Automatically evaluate a percentage of live traffic against your eval criteria. This catches systematic regressions before they pile up.</p>
<p><strong>Targeted evals for known risk areas.</strong> If your system handles financial data or medical information, run focused checks on those categories continuously.</p>
<p><strong>User feedback with low friction.</strong> A thumbs-down button isn&rsquo;t sophisticated. It&rsquo;s incredibly effective if someone is actually looking at the data. At a startup I ran, we learned that a simple feedback signal, reviewed daily, caught issues faster than any automated check.</p>
<p><strong>Drift indicators.</strong> Track model behavior distributions over time. Track retrieval relevance scores. When these shift, something changed &ndash; even if nobody deployed anything.</p>
<p>No single signal is ground truth. The goal is to surface a pattern early enough to contain it.</p>
<h2 id="containment-fast-and-reversible">Containment: fast and reversible</h2>
<p>The instinct during any incident is to understand what happened. Resist that. Contain first, investigate later. This is the same principle from traditional IR &ndash; the tourniquet analogy I’ve used before.</p>
<p>For AI systems, the most reliable containment actions are:</p>
<ul>
<li><strong>Roll back</strong> to a previous model or prompt version. This requires having versioned those artifacts in the first place.</li>
<li><strong> <a href="/blog/2021-09-06-feature-flags-at-scale/"
   
   >Feature-flag</a>
 the risky path.</strong> Disable or rate-limit the AI feature. Route to a fallback.</li>
<li><strong>Escalate to human review.</strong> For high-stakes outputs, insert a human checkpoint until the issue is understood.</li>
<li><strong>Increase sampling.</strong> Crank up monitoring on the affected workflow while the issue is active.</li>
</ul>
<p>All of these are operational actions, not analytical ones. You don’t need to understand the root cause to stop the bleeding.</p>
<h2 id="postmortems-that-close-the-loop">Postmortems that close the loop</h2>
<p>Once contained, run a focused postmortem. The questions are specific:</p>
<ul>
<li>Which outputs were wrong or unsafe? Get concrete examples.</li>
<li>What signal could have caught this earlier?</li>
<li>What evaluation gap allowed it through?</li>
<li>What operational control would have reduced the blast radius?</li>
</ul>
<p>The most important action item from any AI postmortem: add the failure cases to your  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >eval suite</a>
. Every incident should produce new test coverage. If your eval suite isn&rsquo;t growing after incidents, you aren&rsquo;t learning.</p>
<p>Keep action items small and testable. &ldquo;Improve quality&rdquo; isn’t an action item. &ldquo;Add 10 regression cases from this incident to the eval suite and enforce a rollout gate for prompt changes in this workflow&rdquo; is an action item.</p>
<h2 id="prevention-is-a-posture-not-a-gate">Prevention is a posture, not a gate</h2>
<p>The teams that handle AI incidents well treat them as routine. Not as emergencies that mean someone failed. Practical prevention:</p>
<ul>
<li>Evaluate changes before they hit full traffic.  <a href="/blog/2021-02-08-gitops-progressive-delivery/"
   
   >Canary deploys</a>
 work for AI too.</li>
<li>Track model, prompt, and retrieval changes in a single changelog. When something breaks, you need to know what changed.</li>
<li> <a href="/blog/2021-11-29-incident-management-practices/"
   
   >Maintain a simple runbook</a>
 with containment options and owners. Not a 40-page document. A one-pager with &ldquo;who gets paged, what can we roll back, what is the fallback.&rdquo;</li>
</ul>
<p>The goal isn&rsquo;t zero incidents. The goal is fast detection, fast containment, and a system that gets more predictable over time. Same as any production system.</p>
]]></content:encoded></item><item><title>AI Technical Debt Is Eating Your Team Alive (And You Can't Even See It)</title><link>https://lawzava.com/blog/2025-10-27-ai-technical-debt/</link><pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-10-27-ai-technical-debt/</guid><description>AI debt hides in prompts nobody owns, evals nobody runs, and data pipelines nobody watches. By the time you notice, every change feels dangerous.</description><content:encoded><![CDATA[<p>I wrote about  <a href="/blog/2016-02-22-the-true-cost-of-technical-debt/"
   
   >the true cost of technical debt</a>
 back in 2016. The core argument was simple: if you can&rsquo;t put a number on your debt, you can&rsquo;t make a rational decision about it. Measure the pain, do the math, and present the tradeoff.</p>
<p>That advice still holds. But AI debt is a different animal, and it&rsquo;s making me angry.</p>
<p>With traditional tech debt, at least you can see it. Messy code. Missing tests. A module everyone dreads touching. The debt is in the codebase. You can grep for it. You can point to it in a PR review.</p>
<p>AI debt hides. It hides in prompts copy-pasted from a demo and never documented. In evaluations that were &ldquo;planned for next sprint&rdquo; six months ago. In embeddings that went stale when source docs changed and nobody re-indexed. In  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval pipelines</a>
 where data drifted so gradually that answers went from &ldquo;good&rdquo; to &ldquo;plausible&rdquo; to &ldquo;confidently wrong,&rdquo; and nobody noticed until a customer complained. The architectural version of this is why AI-native architecture needs explicit evaluation and retrieval ownership.</p>
<p>The system is still up. It still returns 200 OK. And it&rsquo;s slowly poisoning your product.</p>
<h2 id="the-four-kinds-of-ai-debt-that-keep-showing-up">The four kinds of AI debt that keep showing up</h2>
<p><strong>Prompt debt.</strong> Someone wrote a prompt that worked. They shipped it. Three model versions later, it still &ldquo;works,&rdquo; but the behavior has shifted in ways nobody documented because nobody was measuring. The prompt has magic strings nobody can explain. Changing a single sentence now requires a full regression test nobody has time for, so nobody changes anything, and the prompt becomes legacy code that happens to be written in English.</p>
<p><strong>Eval debt.</strong> This one drives me up the wall. Teams ship AI features with no  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation suite</a>
. Then they argue about quality using anecdotes. &ldquo;It seemed fine when I tried it.&rdquo; That&rsquo;s not engineering; that&rsquo;s vibes. Without evals, you can&rsquo;t tell if your last change made things better or worse. You&rsquo;re flying blind and calling it agile.</p>
<p><strong>Data and pipeline debt.</strong> Stale embeddings. Missing documents. Labeling standards that drifted. The retrieval layer quietly degrades, and because LLMs are so good at sounding confident, nobody notices that answers are getting worse. This is the most insidious form because it&rsquo;s silent. The system doesn&rsquo;t crash. It just gets less trustworthy.</p>
<p><strong>Architecture debt.</strong> The model interface is hard-coded three layers deep. Tool calls are embedded in application logic. Swapping a provider or upgrading a model feels like open-heart surgery. So teams avoid improvements entirely. The system calcifies.</p>
<h2 id="how-to-actually-fix-this">How to actually fix this</h2>
<p>The same way you fix  <a href="/blog/2021-09-20-technical-debt-management/"
   
   >any tech debt</a>
. Not with a heroic rewrite. With discipline.</p>
<p><strong>Version your prompts like code.</strong> Put them in the repo. Give them owners. Document the intent, not just the text. When someone changes a prompt, they should write down why, and what eval signals should remain stable. This isn&rsquo;t bureaucracy. It&rsquo;s how you stop mystery regressions.</p>
<p><strong>Build evals before you ship.</strong> Start with a small set of real examples and documented expected outcomes. Run them on every meaningful change. It doesn&rsquo;t need to be elaborate. It needs to be consistent. Teams that do this &ndash; even just 20-30 test cases &ndash; move faster because they know what is safe to change.</p>
<p><strong>Decouple the model interface.</strong> Abstract it. Separate retrieval from response logic. That lets you  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >swap providers</a>
, test with mocks, and upgrade models without touching core flows. It also makes your system testable, which is the whole point.</p>
<p><strong>Monitor freshness alongside quality.</strong> Track when your embeddings were last updated. Track retrieval relevance scores. If your data pipeline is stale, your outputs are stale, no matter how good the model is.</p>
<h2 id="the-uncomfortable-part">The uncomfortable part</h2>
<p>Most teams accumulate AI debt because they shipped under pressure and told themselves they&rsquo;d clean it up later. I&rsquo;ve been guilty of this. Early on at a startup I ran, we had prompts that worked &ldquo;well enough&rdquo; and no eval suite for weeks. The reckoning came when we swapped model versions and spent three days figuring out what broke because we had no baseline to compare against.</p>
<p>The fix isn&rsquo;t a cleanup sprint. It&rsquo;s a steady cadence. Fifteen percent of capacity toward debt work, same as I recommended in 2016. Review prompt changes with rationale. Run evals on every release.  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >Monitor quality signals</a>
 and data freshness together.</p>
<p>AI debt is manageable. But it requires intention. If every small change to your AI system feels risky, you already have a debt problem. The path forward isn&rsquo;t heroic rewrites. It&rsquo;s a steady sequence of small, documented improvements.</p>
<p>Steady beats dramatic. Every time.</p>
]]></content:encoded></item><item><title>AI Doesn't Make Your Team Faster. Shared Infrastructure Does.</title><link>https://lawzava.com/blog/2025-10-13-ai-team-productivity/</link><pubDate>Mon, 13 Oct 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-10-13-ai-team-productivity/</guid><description>Individual AI speedups are a distraction. The real gains come from treating AI as team infrastructure &amp;amp;ndash; embedded in docs, decisions, and onboarding.</description><content:encoded><![CDATA[<p>Every few weeks someone asks me how AI is changing team productivity. The honest answer: less than most people think, and in different ways than expected.</p>
<p>Individual engineers using  <a href="/blog/2021-06-28-github-copilot-first-look/"
   
   >Copilot</a>
 or ChatGPT to write code faster is fine. It&rsquo;s also not the point. One person moving 20% faster doesn&rsquo;t help if the team is still bottlenecked on the same things it was bottlenecked on six months ago: stale docs, unclear decisions, and onboarding that requires cornering a senior engineer for two hours.</p>
<p>The teams I see getting real gains are the ones that treat AI as shared infrastructure. Not a personal productivity hack. Infrastructure.</p>
<h2 id="what-that-looks-like-in-practice">What that looks like in practice</h2>
<p>A shared assistant for team documentation and search. Not a chatbot that guesses &ndash; something that points to actual internal sources and tells you who owns what. Automated meeting summaries that feed into the same system where the team already tracks decisions.  <a href="/blog/2022-03-21-engineering-onboarding-excellence/"
   
   >Onboarding workflows</a>
 where a new hire can get a credible first answer and a pointer to the right human, instead of posting in Slack and hoping someone responds.</p>
<p>None of these need perfect accuracy. They need consistent routing and clear expectations about when AI is advisory versus authoritative.</p>
<h2 id="the-measurement-trap">The measurement trap</h2>
<p>Here&rsquo;s where most teams go wrong. They  <a href="/blog/2020-08-31-developer-productivity-metrics/"
   
   >measure AI tool adoption</a>
. Number of prompts. Lines of code generated. That&rsquo;s like measuring how many emails your team sends and calling it productivity.</p>
<p>The only question that matters: is the team less stuck?</p>
<p>Fewer repeated questions about the same topic. A shorter gap between a decision being made and that decision being documented. Less rework because someone missed context from a meeting they weren&rsquo;t in.</p>
<p>If AI usage goes up but those numbers stay flat, you have added a toy, not infrastructure.</p>
<h2 id="docs-specifically">Docs, specifically</h2>
<p> <a href="/blog/2025-07-21-ai-documentation-systems/"
   
   >Documentation</a>
 is where AI has the most underrated impact. Not generating docs from scratch &ndash; that&rsquo;s garbage. But proposing small updates when code changes, flagging content that no longer matches reality, and making the update feel like a five-second approval instead of a batch project.</p>
<p>At a startup I ran, we struggled with  <a href="/blog/2022-06-13-engineering-documentation-practices/"
   
   >doc decay</a>
 like everyone else. The trick was making updates feel like routine housekeeping, not a chore you schedule for &ldquo;next sprint&rdquo; and never do.</p>
<h2 id="start-small-stay-boring">Start small, stay boring</h2>
<p>Pick one shared workflow. Make it reliable. Expand based on evidence, not enthusiasm. A small, visible win &ndash; like meeting notes that are actually useful the next day &ndash; changes team behavior more than any broad AI rollout plan.</p>
<p>The teams getting durable gains are the ones keeping AI practical, scoped, and accountable. Boring wins. As usual.</p>
]]></content:encoded></item><item><title>Measuring AI ROI Without Lying to Yourself</title><link>https://lawzava.com/blog/2025-09-29-ai-roi-measurement/</link><pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-09-29-ai-roi-measurement/</guid><description>Most AI ROI calculations are fantasy. Measure honestly: one workflow, full costs, benefits tied to outcomes the business tracks, and a range, not one number.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI ROI isn&rsquo;t a spreadsheet trick. Pick one workflow with a clear baseline. Capture all costs &ndash; engineering, evals, governance, change management, and AI inference cost &ndash; not just API bills. Tie benefits to outcomes the business already measures. Report a range with assumptions, not one magic number. If your ROI case only works under best-case assumptions, it doesn&rsquo;t work.</p>
<hr>
<p>I&rsquo;ve sat in a lot of budget reviews over the years &ndash; telecoms, fintech, logistics. The AI ROI presentations I see fall into two categories: honest assessments that lead to good decisions, and fiction that leads to funded projects that get quietly killed six months later.</p>
<p>The difference isn&rsquo;t sophistication. It&rsquo;s honesty about costs and rigor about baselines.</p>
<h2 id="the-full-cost-picture">The Full Cost Picture</h2>
<p>The first lie in most AI ROI calculations is the cost side. Teams report  <a href="/blog/2024-10-14-ai-cost-benchmarking/"
   
   >API costs</a>
 and maybe some engineering time. They leave out everything else.</p>
<p>Here&rsquo;s what AI actually costs:</p>
<table>
  <thead>
      <tr>
          <th>Cost Category</th>
          <th>What Teams Report</th>
          <th>What It Actually Includes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Infrastructure</strong></td>
          <td>API usage fees</td>
          <td>API fees + local compute + storage + networking + monitoring</td>
      </tr>
      <tr>
          <td><strong>Engineering</strong></td>
          <td>Initial build time</td>
          <td>Build + integration + prompt engineering + ongoing maintenance</td>
      </tr>
      <tr>
          <td><strong>Evaluation</strong></td>
          <td>Nothing</td>
          <td>Eval set creation + human review + quality monitoring tooling</td>
      </tr>
      <tr>
          <td><strong>Data</strong></td>
          <td>Nothing</td>
          <td>Data preparation + cleaning + annotation + ongoing curation</td>
      </tr>
      <tr>
          <td><strong>Governance</strong></td>
          <td>Nothing</td>
          <td>Compliance review + privacy controls + audit tooling + vendor management</td>
      </tr>
      <tr>
          <td><strong>Change Management</strong></td>
          <td>Nothing</td>
          <td>Training + process redesign + user support + documentation</td>
      </tr>
      <tr>
          <td><strong>Opportunity Cost</strong></td>
          <td>Nothing</td>
          <td>What else the team could have built with the same time</td>
      </tr>
  </tbody>
</table>
<p>When I push teams to fill in the &ldquo;What It Actually Includes&rdquo; column, the cost estimate typically doubles or triples. That isn&rsquo;t an argument against AI. It&rsquo;s an argument for honest accounting so you can make the right investment decisions.</p>
<h2 id="the-baseline-problem">The Baseline Problem</h2>
<p>You can&rsquo;t measure improvement without a baseline. Sounds obvious. You&rsquo;d be amazed how many teams skip it.</p>
<p>Before you deploy AI in a workflow, measure the current state:</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>How to Capture</th>
          <th>Why It Matters</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Throughput</strong></td>
          <td>Tasks completed per person per day</td>
          <td>Direct productivity comparison</td>
      </tr>
      <tr>
          <td><strong>Error rate</strong></td>
          <td>Errors caught in QA or by customers</td>
          <td>Quality comparison</td>
      </tr>
      <tr>
          <td><strong>Cycle time</strong></td>
          <td>Time from task start to completion</td>
          <td>Speed comparison</td>
      </tr>
      <tr>
          <td><strong>Cost per task</strong></td>
          <td>Fully loaded labor cost / tasks completed</td>
          <td>Economic comparison</td>
      </tr>
      <tr>
          <td><strong>Customer satisfaction</strong></td>
          <td>CSAT or NPS for the specific workflow</td>
          <td>Outcome comparison</td>
      </tr>
  </tbody>
</table>
<p>Measure for at least four weeks before deployment. Document any other changes that happened during the same period &ndash; new hires, process changes, seasonal variation. Those confounders matter when you try to attribute improvements to AI.</p>
<h2 id="mapping-benefits-to-outcomes">Mapping Benefits to Outcomes</h2>
<p>The second lie in most AI ROI cases is on the benefit side. &ldquo;Time saved&rdquo; isn&rsquo;t a business outcome. It&rsquo;s a proxy. What did the team do with the saved time?</p>
<p>Map every claimed benefit to something the business already tracks and trusts:</p>
<table>
  <thead>
      <tr>
          <th>AI Capability</th>
          <th>Claimed Benefit</th>
          <th>Business Outcome to Measure</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Automated triage</td>
          <td>Faster ticket routing</td>
          <td>Resolution time, first-response time</td>
      </tr>
      <tr>
          <td>Document extraction</td>
          <td>Less manual data entry</td>
          <td>Throughput per person, error rate</td>
      </tr>
      <tr>
          <td>Content generation</td>
          <td>Faster content creation</td>
          <td>Time to publish, content volume</td>
      </tr>
      <tr>
          <td>Code assistance</td>
          <td>Faster development</td>
          <td> <a href="/blog/2022-01-24-dora-metrics-implementation/"
   
   >Cycle time, defect rate, deploy frequency</a>
</td>
      </tr>
      <tr>
          <td>Customer support</td>
          <td>Reduced support load</td>
          <td>Tickets per agent, CSAT, escalation rate</td>
      </tr>
  </tbody>
</table>
<p>If you can&rsquo;t connect an AI capability to a number the business already watches, the benefit is speculative. Label it that way. Don&rsquo;t pretend it&rsquo;s measured.</p>
<h2 id="the-three-traps">The Three Traps</h2>
<p><strong>Cherry-picking the easy wins.</strong> Measuring ROI only on the tasks that were already easiest to automate. The impressive numbers don&rsquo;t represent the full deployment. Report the aggregate, not just the highlights.</p>
<p><strong>Ignoring the learning curve.</strong> The first month after deployment is usually worse than the baseline. People are adjusting. Workflows are changing. If you measure too early, you either see inflated novelty effects or deflated learning-curve effects. Neither is representative.</p>
<p><strong>Qualitative benefits as hard numbers.</strong> &ldquo;Developers feel more productive&rdquo; isn&rsquo;t the same as &ldquo;throughput increased 20%.&rdquo; Both are worth reporting. Only one belongs in a financial model. In my work, I insist on separating measured outcomes from perceived benefits in every report. Leadership respects the honesty.</p>
<h2 id="the-report-format-that-works">The Report Format That Works</h2>
<p>Keep the ROI report to one page. Seriously. If it needs more than one page, you&rsquo;re either overcomplicating or overclaiming.</p>
<p><strong>Decision context.</strong> What question does this measurement answer? &ldquo;Should we expand AI-assisted triage to all support channels&rdquo; is specific. &ldquo;Is AI valuable&rdquo; isn&rsquo;t.</p>
<p><strong>Assumptions.</strong> List every assumption explicitly. Volume of tasks, cost rates, attribution model, measurement window. When assumptions change, the conclusion changes. Make that visible.</p>
<p><strong>Results as a range.</strong> Don&rsquo;t report a single ROI number. Report a range: conservative estimate under pessimistic assumptions, expected estimate under likely assumptions, optimistic estimate under best-case assumptions. If the conservative estimate is still positive, you have a strong case. If only the optimistic estimate is positive, you have a gamble.</p>
<p><strong>Next measurement.</strong> State when you&rsquo;ll re-measure and what would cause you to change course. This turns the report from a sales pitch into a decision tool.</p>
<h2 id="what-matters">What matters</h2>
<p>AI ROI measurement isn&rsquo;t about proving AI works. It&rsquo;s about making good investment decisions. Capture the full cost, not just the API bill. Establish a real baseline before deploying. Map benefits to  <a href="/blog/2025-07-07-ai-product-metrics/"
   
   >outcomes the business already tracks</a>
. Report honestly, with ranges and assumptions.</p>
<p>The teams that do this get funded reliably because leadership trusts their numbers. The teams that overclaim get one round of funding and then spend a year explaining why the projections didn&rsquo;t materialize.</p>
<p>Discipline over heroics. Even in spreadsheets.</p>
]]></content:encoded></item><item><title>AI Privacy Is a Plumbing Problem, Not a Policy Problem</title><link>https://lawzava.com/blog/2025-09-15-ai-data-privacy/</link><pubDate>Mon, 15 Sep 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-09-15-ai-data-privacy/</guid><description>Privacy in AI systems fails in the details: what gets logged, who can replay prompts, how long artifacts linger. Treat it as infrastructure, not a checkbox.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI privacy is plumbing, not policy. Map every data flow. Minimize what you send to models. Control who can replay prompts and access logs. Set retention rules that are actually enforced. Do sensitive work locally and pass reduced representations upstream. If you treat privacy as a late-stage review, you&rsquo;ll fail the audit.</p>
<hr>
<p>My background in national cyber-defense taught me something that most engineers learn too late: data classification isn&rsquo;t a theoretical exercise. When you&rsquo;re operating in an environment where information leakage has consequences beyond a compliance fine, you develop a different relationship with data flows. You map them. You minimize them. You assume every copy of data is a liability until proven otherwise.</p>
<p>That mindset transfers directly to AI systems.</p>
<h2 id="the-problem-nobody-maps">The Problem Nobody Maps</h2>
<p>Most AI features touch far more data than the visible prompt. In a typical  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >RAG workflow</a>
, the user submits a query, your system retrieves context from a knowledge base, the model receives both the query and retrieved documents, it generates a response, and that response gets logged for quality monitoring.</p>
<p>At each step, data is copied. The user&rsquo;s query is in your application logs, in the retrieval system&rsquo;s query log, in the model provider&rsquo;s request log, in your quality monitoring dashboard. The retrieved documents &ndash; which might contain sensitive customer data &ndash; now exist in your model provider&rsquo;s system too, subject to their retention policy, not yours.</p>
<p>If you can&rsquo;t draw this flow on a whiteboard in under two minutes, your privacy controls are guesswork. I start every privacy review by asking the team to map the flow. Most teams can&rsquo;t do it. That&rsquo;s the first problem to fix.</p>
<h2 id="minimize-before-you-send">Minimize Before You Send</h2>
<p>Data minimization is the single most effective privacy control in AI systems. Not because it&rsquo;s elegant, but because it reduces blast radius. Data you don&rsquo;t send can&rsquo;t be leaked, retained, or trained on.</p>
<p>Practical minimization looks like this:</p>
<p><strong>Strip identifiers early.</strong> Before the prompt is assembled, remove names, emails, account IDs &ndash; anything that isn&rsquo;t required for the model to produce a useful response. If the model needs to reference a user, use an opaque session token that maps to the real identity only in your system.</p>
<p><strong>Send summaries, not documents.</strong> If you need context from a 20-page contract, summarize the relevant section locally and send the summary. The model doesn&rsquo;t need the full document. Your privacy exposure drops by an order of magnitude.</p>
<p><strong>Separate sensitive from useful.</strong> Not all data carries the same risk. Split your workflows so that high-sensitivity data &ndash; medical records, financial details, authentication tokens &ndash; is processed locally with stronger controls. Lower-risk data can flow through standard AI paths. This tiering reduces the scope of every privacy review and makes incident response simpler.</p>
<h2 id="local-first-for-the-dangerous-bits">Local First for the Dangerous Bits</h2>
<p>Some operations should never leave your infrastructure. PII detection, redaction, and sensitive-content classification should run locally, on models you control, before anything touches an external API.</p>
<p>The pattern is straightforward: do sensitive work where the data already lives, then pass a reduced representation to the cloud model. This isn&rsquo;t about avoiding cloud AI entirely. It&rsquo;s about being deliberate about what crosses the boundary.</p>
<p>I&rsquo;ve helped design  <a href="/blog/2025-05-26-ai-data-pipelines/"
   
   >pipelines</a>
 where the first stage runs a  <a href="/blog/2025-08-18-local-ai-development/"
   
   >local model</a>
 to detect and redact PII, the second stage sends the sanitized content to a cloud model for the actual task, and the third stage re-attaches the redacted information only in the final response shown to the authorized user. The cloud model never sees real PII. The logs never contain it. The attack surface shrinks dramatically.</p>
<h2 id="logs-are-the-quiet-privacy-gap">Logs Are the Quiet Privacy Gap</h2>
<p>AI features generate logs that teams don&rsquo;t think about. Prompt logs for debugging. Response logs for quality monitoring. Replay tools for incident investigation. Evaluation datasets built from production traffic.</p>
<p>Each of these creates a copy of user data that lives outside your normal data governance. And because these are &ldquo;internal tools,&rdquo; they often have broader access than production databases do.</p>
<p>Lock them down the same way you lock down production data:</p>
<ul>
<li><strong>Access control.</strong> Not everyone who can view the dashboard should be able to replay prompts containing user data. Restrict access by role and audit who accesses what.</li>
<li><strong>Retention limits.</strong> Prompt logs don&rsquo;t need to live forever. Set a retention window &ndash; 30 days is plenty for most debugging needs &ndash; and enforce automatic deletion.</li>
<li><strong>Audit trails.</strong> Know who accessed which logs and when. This isn&rsquo;t optional for regulated industries. It shouldn&rsquo;t be optional for anyone.</li>
</ul>
<h2 id="vendor-questions-that-actually-matter">Vendor Questions That Actually Matter</h2>
<p>When evaluating AI providers, skip the marketing page and ask these questions directly:</p>
<ol>
<li>Is customer data used to train or improve models by default? How do you opt out, and is the opt-out verified?</li>
<li>What data is retained after a request completes? For how long? For what purpose?</li>
<li>Where does processing happen geographically? Who on the vendor&rsquo;s side can access request logs?</li>
<li>How are deletion requests handled? What&rsquo;s the SLA? Is deletion cryptographic or simply a database flag?</li>
</ol>
<p>Write the answers down. Put them in your vendor assessment. Revisit them annually, because vendor policies change without notice.</p>
<h2 id="governance-that-survives-audits">Governance That Survives Audits</h2>
<p>Heavy governance processes don&rsquo;t survive contact with reality. Teams skip them, shortcuts accumulate, and the audit reveals a gap between policy and practice.</p>
<p>Keep governance light and concrete:</p>
<ul>
<li><strong>One data flow map per AI feature.</strong> Inputs, retrieval sources, logs, outputs, retention. Fits on a single page.</li>
<li><strong>A documented purpose for each data category.</strong> Why is this data in the pipeline? If you can&rsquo;t answer, remove it.</li>
<li><strong>Tested deletion paths.</strong> Not &ldquo;we have a process for deletion.&rdquo; Actually run it. Verify the data is gone. Do this quarterly.</li>
</ul>
<p>Privacy is a design constraint, not a compliance checkbox. Build it into your AI pipeline the same way you build in authentication and authorization: as infrastructure that runs automatically, not as a review that happens after the fact.</p>
<p>Security, stability, performance &ndash; in that order. Privacy falls under security. It goes first.</p>
]]></content:encoded></item><item><title>AI Pair Programming: It's a Junior Dev, Not a Wizard</title><link>https://lawzava.com/blog/2025-09-01-ai-pair-programming/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-09-01-ai-pair-programming/</guid><description>Treat AI coding assistants like a fast, literal junior dev: tight constraints, critical review, and no expectations of architectural insight.</description><content:encoded><![CDATA[<p>I pair with AI every day: building production systems, contributing to Go, and prototyping new ideas. It&rsquo;s part of my workflow the same way version control and testing are &ndash; not because it&rsquo;s magical, but because it&rsquo;s useful when you know its limits.</p>
<p>The teams I&rsquo;ve seen get the most value from  <a href="/blog/2022-11-28-ai-code-assistants-evolution/"
   
   >AI coding assistants</a>
 treat them the same way: like a fast, literal junior developer. Emphasis on literal. The model does exactly what you ask, fills in gaps with plausible guesses, and never tells you when your approach is wrong. That&rsquo;s the mental model that keeps you productive without getting burned.</p>
<h2 id="where-it-shines">Where It Shines</h2>
<p>AI assistants are excellent at work that&rsquo;s well-scoped and pattern-driven. The kind of tasks where you know exactly what the output should look like but don&rsquo;t want to type it all out.</p>
<p>Boilerplate generation, test scaffolding from existing patterns, translating a clear spec into working code, exploring how an unfamiliar API works, and refactoring repetitive code paths into a cleaner abstraction when you already know what that abstraction should be.</p>
<p>I use it heavily for these cases and it genuinely saves hours per week. When I&rsquo;m writing Go and I need a new handler that follows the same pattern as the last ten handlers, the AI drafts it in seconds. I review, adjust, and move on.</p>
<h2 id="where-it-falls-apart">Where It Falls Apart</h2>
<p>The moment you need architectural judgment, project history, or business context, the AI becomes dangerous. Not useless &ndash; dangerous. Because it will confidently produce something that looks right, passes a quick glance, and introduces a subtle bug or design flaw that you don&rsquo;t catch until it&rsquo;s in production.</p>
<p>Watch for these warning signs:</p>
<ul>
<li>It repeats the same mistake after you correct it. The model doesn&rsquo;t learn within a session the way a human colleague does. If it keeps ignoring a constraint, it probably can&rsquo;t reliably hold that constraint in its current context.</li>
<li>It invents things. Functions that don&rsquo;t exist. Config options that aren&rsquo;t real. API endpoints it hallucinated from training data. Always verify against actual docs.</li>
<li>It optimizes for elegance over correctness. The model loves clean, compact code. Sometimes that means it refactors away an important edge case because the edge case made the code ugly.</li>
</ul>
<p>I&rsquo;ve caught all three of these in my own work. More than once.</p>
<h2 id="the-loop-that-works">The Loop That Works</h2>
<p>Long, open-ended chat sessions with AI produce garbage. The  <a href="/blog/2024-07-22-context-window-strategies/"
   
   >context window</a>
 fills up, the model loses track of constraints, and you end up in a back-and-forth that takes longer than writing the code yourself.</p>
<p>Short, focused loops work. Here&rsquo;s the pattern I use:</p>
<ol>
<li><strong>Define the task tightly.</strong> Inputs, outputs, constraints, existing style to match. Be specific. &ldquo;Add a function that does X given Y, handling Z edge case, matching the pattern in the rest of this file.&rdquo;</li>
<li><strong>Get a first pass.</strong> Let the AI draft it.</li>
<li><strong>Review critically.</strong> Not &ldquo;does this look right&rdquo; &ndash; trace through the logic. Check edge cases. Check error handling. Check that it respects the codebase conventions.</li>
<li><strong>Iterate on specific gaps.</strong> Don&rsquo;t ask for a full rewrite. Point at the specific line or logic branch that&rsquo;s wrong and ask for a fix.</li>
<li><strong>Integrate manually.</strong> Copy the code into your editor, run the tests, review the diff. The AI&rsquo;s output is a draft, not a commit.</li>
</ol>
<h2 id="give-it-real-context">Give It Real Context</h2>
<p>Vague prompts produce vague code. The single biggest improvement I&rsquo;ve seen is upgrading from &ldquo;write me a function that processes users&rdquo; to something with actual constraints:</p>
<p>&ldquo;Add a method <code>getActiveUsers(since time.Time)</code> to UserStore. Users are active if their LastSeen is after the given time. Return a slice sorted by LastSeen descending. If the store is empty, return nil, not an empty slice. Match the existing receiver pattern in this file.&rdquo;</p>
<p>That level of specificity is the difference between useful output and time wasted reviewing hallucinated code.</p>
<h2 id="the-trust-boundary">The Trust Boundary</h2>
<p>Here&rsquo;s the line I draw:  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >AI output is untrusted input</a>
. Same as user input. Same as data from an external API. It goes through the same gates.</p>
<ul>
<li>Tests must pass.</li>
<li>Linter must pass.</li>
<li> <a href="/blog/2018-10-01-effective-code-reviews/"
   
   >Code review</a>
 still applies. A human reads the diff.</li>
<li>Security-sensitive code gets extra scrutiny regardless of who or what wrote it.</li>
</ul>
<p>Some teams have started rubber-stamping AI-generated code because &ldquo;the AI wrote it and it looks fine.&rdquo; That&rsquo;s how you get vulnerabilities in production. I&rsquo;ve seen it happen.</p>
<h2 id="the-honest-assessment">The Honest Assessment</h2>
<p>AI pair programming makes me faster at the boring parts of writing software. It doesn&rsquo;t make me better at the hard parts. Architecture decisions, security considerations, performance tradeoffs, understanding what the user actually needs &ndash; those are still entirely on me.</p>
<p>The  <a href="/blog/2023-11-13-ai-developer-productivity/"
   
   >developers who get the most value</a>
 are the ones who already know what good code looks like. The AI accelerates their output. The developers who rely on AI to compensate for gaps in their understanding ship bugs faster.</p>
<p>Use it as a tool. Review its work. Keep the sessions short. And never, ever merge without reading the diff.</p>
]]></content:encoded></item><item><title>Running AI Locally: A Practical Guide for Teams Who Care About Control</title><link>https://lawzava.com/blog/2025-08-18-local-ai-development/</link><pubDate>Mon, 18 Aug 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-08-18-local-ai-development/</guid><description>Local AI is no longer a hobby project. How to set it up properly: provider abstraction, versioned models, eval harnesses, and a cloud fallback.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Local AI development is a legitimate option for teams that need data control, predictable costs, or offline capability. The tradeoff is operational work. Keep the stack small, abstract the provider behind an interface, version your models like you version your code, maintain an eval set, and always keep a cloud fallback for quality-critical paths.</p>
<hr>
<p>I run  <a href="/blog/2024-01-22-local-llms-development/"
   
   >local models</a>
 daily: in production projects, for prototypes, and for anything involving sensitive data that shouldn&rsquo;t leave my machine. The tooling has matured enough that this is no longer a novelty; it&rsquo;s a practical engineering choice with clear tradeoffs.</p>
<p>I&rsquo;ve also seen teams go all-in on local AI without understanding what they&rsquo;re signing up for. Running your own models means owning the full lifecycle: model selection, quantization, runtime management, version pinning, quality monitoring, and fallback strategies. If you aren&rsquo;t prepared for that operational load, use a managed API.</p>
<p>This post is for teams who have decided local makes sense and want to do it properly.</p>
<h2 id="when-local-is-the-right-call">When Local Is the Right Call</h2>
<p>Local AI makes sense in specific scenarios:</p>
<ul>
<li><strong>Sensitive data.</strong> Proprietary code, financial records &ndash; anything you don&rsquo;t want leaving your network. I frequently work with data under NDA, and local inference means the data never touches a third-party API.</li>
<li><strong> <a href="/blog/2023-07-24-ai-cost-optimization/"
   
   >Predictable costs</a>
.</strong> API costs scale with usage; local costs scale with hardware. For high-volume routine tasks &ndash; classification, extraction, summarization &ndash; local can be dramatically cheaper once you amortize the hardware.</li>
<li><strong>Offline or air-gapped environments.</strong> Some deployments don&rsquo;t have reliable internet. Some shouldn&rsquo;t have it. My national cyber-defense background drilled this in &ndash; there are environments where external API calls aren&rsquo;t just inconvenient; they aren&rsquo;t allowed.</li>
<li><strong> <a href="/blog/2024-08-19-llm-testing-strategies/"
   
   >Deterministic CI testing</a>
.</strong> When your tests depend on model output, you need a pinned model version that doesn&rsquo;t change between runs. Local gives you that control.</li>
</ul>
<p>Local is the wrong call when you need frontier-level quality on every request or your team can&rsquo;t absorb the operational overhead.</p>
<h2 id="the-provider-abstraction">The Provider Abstraction</h2>
<p>First rule: never hard-code your provider. Whether you&rsquo;re using Ollama, llama.cpp, vLLM, or a cloud API, the rest of your code shouldn&rsquo;t care. Hide it behind an interface.</p>
<p>In Go, this is clean:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Provider defines the contract for any AI backend.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Provider</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">CompletionRequest</span>) (<span style="color:#a6e22e">CompletionResponse</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Embed</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) ([]<span style="color:#66d9ef">float64</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Health</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CompletionRequest</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Messages</span>    []<span style="color:#a6e22e">Message</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Temperature</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CompletionResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Content</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">TokensUsed</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Model</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">FinishReason</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now your local and cloud providers implement the same interface. Switching between them is a config change, not a code rewrite. Testing is trivial: mock the interface and move on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">OllamaProvider</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">endpoint</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client</span>   <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewOllamaProvider</span>(<span style="color:#a6e22e">endpoint</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">OllamaProvider</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">OllamaProvider</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">endpoint</span>: <span style="color:#a6e22e">endpoint</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">client</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Timeout</span>: <span style="color:#ae81ff">120</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">o</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">OllamaProvider</span>) <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">CompletionRequest</span>) (<span style="color:#a6e22e">CompletionResponse</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">body</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ollamaRequest</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Model</span>:    <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Model</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Messages</span>: <span style="color:#a6e22e">toOllamaMessages</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Messages</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Stream</span>:   <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Options</span>: <span style="color:#a6e22e">ollamaOptions</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Temperature</span>: <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Temperature</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">NumPredict</span>:  <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">MaxTokens</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">post</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;/api/chat&#34;</span>, <span style="color:#a6e22e">body</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">CompletionResponse</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;ollama completion: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">CompletionResponse</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Content</span>:      <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Message</span>.<span style="color:#a6e22e">Content</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">TokensUsed</span>:   <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">EvalCount</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Model</span>:        <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Model</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">FinishReason</span>: <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">DoneReason</span>,
</span></span><span style="display:flex;"><span>    }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="the-fallback-chain">The Fallback Chain</h2>
<p>Local models are good. They aren&rsquo;t always good enough. For quality-critical paths &ndash; user-facing content generation, complex reasoning tasks, anything where a wrong answer costs real money &ndash; you need a  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >fallback to a stronger model</a>
.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FallbackProvider</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">primary</span>   <span style="color:#a6e22e">Provider</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fallback</span>  <span style="color:#a6e22e">Provider</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">threshold</span> <span style="color:#66d9ef">float64</span> <span style="color:#75715e">// confidence threshold for fallback</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">f</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">FallbackProvider</span>) <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">CompletionRequest</span>) (<span style="color:#a6e22e">CompletionResponse</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">primary</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Primary failed, try fallback</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">slog</span>.<span style="color:#a6e22e">Warn</span>(<span style="color:#e6db74">&#34;primary provider failed, using fallback&#34;</span>, <span style="color:#e6db74">&#34;error&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">fallback</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In practice, I extend this with confidence scoring &ndash; if the local model returns a low-confidence response, automatically retry with the cloud provider. The core pattern is simple: try local first, fall back to cloud when needed, and log fallbacks so you know how often they happen.</p>
<h2 id="configuration-that-travels">Configuration That Travels</h2>
<p>Keep your AI configuration in a structured file in source control. Everything &ndash; model names, endpoints, fallback rules, temperature settings &ndash; should be declarative and version-controlled.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">ai</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">default_provider</span>: <span style="color:#ae81ff">local</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">providers</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">local</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">ollama</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">http://127.0.0.1:11434</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">models</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">completion</span>: <span style="color:#e6db74">&#34;mistral:7b-instruct-v0.3-q5_K_M&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">embedding</span>: <span style="color:#e6db74">&#34;nomic-embed-text:latest&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">120s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cloud</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">openai</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># API key from environment: AI_CLOUD_API_KEY</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">models</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">completion</span>: <span style="color:#e6db74">&#34;gpt-4o&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">embedding</span>: <span style="color:#e6db74">&#34;text-embedding-3-small&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">30s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fallback</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">primary</span>: <span style="color:#ae81ff">local</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">secondary</span>: <span style="color:#ae81ff">cloud</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">on_error</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">on_low_confidence</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">confidence_threshold</span>: <span style="color:#ae81ff">0.7</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">evaluation</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">eval_set_path</span>: <span style="color:#e6db74">&#34;./eval/fixtures&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">run_on_model_change</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>The model name includes the quantization level. This is deliberate. <code>mistral:7b-instruct-v0.3-q5_K_M</code> is not the same as <code>mistral:7b-instruct-v0.3-q4_0</code>. Different quantization levels produce different outputs. Pin it.</p>
<h2 id="versioning-and-reproducibility">Versioning and Reproducibility</h2>
<p>This is where most local setups fall apart. Someone updates the model, doesn&rsquo;t tell the team, and suddenly outputs are different. Tests still pass because nobody wrote quality assertions &ndash; they just check that the model returned something.</p>
<p>Version these things:</p>
<ul>
<li><strong>Model file hash.</strong> SHA256 the model binary. Store the hash in your lockfile or config. If the hash changes, the model changed.</li>
<li><strong>Runtime version.</strong> Pin your Ollama or llama.cpp version in your Dockerfile or setup script.</li>
<li><strong>Prompt templates.</strong> Keep them in source control alongside the code that uses them. Prompt drift is real and insidious.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">ollama/ollama:0.3.12</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Pull and pin specific model versions</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ollama pull mistral:7b-instruct-v0.3-q5_K_M<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Copy eval fixtures for smoke test</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> eval/fixtures /eval/fixtures<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><h2 id="the-evaluation-harness">The Evaluation Harness</h2>
<p>You need an  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >eval set</a>
. Not optional. It should be a small collection of representative inputs with expected outputs that you run every time you change a model, update a prompt, or modify provider configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestModelQuality</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">provider</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">setupLocalProvider</span>(<span style="color:#a6e22e">t</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fixtures</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">loadEvalFixtures</span>(<span style="color:#a6e22e">t</span>, <span style="color:#e6db74">&#34;./eval/fixtures&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">passed</span>, <span style="color:#a6e22e">failed</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">fix</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">fixtures</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">provider</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">Request</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fixture %s: %v&#34;</span>, <span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">failed</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fixture %s: expected pattern %q, got %q&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">ExpectedPattern</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">failed</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">passed</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">passRate</span> <span style="color:#f92672">:=</span> float64(<span style="color:#a6e22e">passed</span>) <span style="color:#f92672">/</span> float64(<span style="color:#a6e22e">passed</span><span style="color:#f92672">+</span><span style="color:#a6e22e">failed</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">passRate</span> &lt; <span style="color:#ae81ff">0.85</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;pass rate %.1f%% below threshold 85%%&#34;</span>, <span style="color:#a6e22e">passRate</span><span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Run this in CI. Run it before every model swap. Run it when you change prompts. The eval harness is what keeps you from shipping a regression you don&rsquo;t notice for two weeks.</p>
<h2 id="performance-tuning-order">Performance Tuning Order</h2>
<p>If local inference is too slow, fix it in this order:</p>
<ol>
<li><strong>Smaller model.</strong> For routine tasks &ndash; classification, extraction, simple summarization &ndash; a  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >7B parameter model</a>
 is often sufficient. Don&rsquo;t run a 70B model for ticket triage.</li>
<li><strong>Quantization.</strong> Q5_K_M is usually the sweet spot between quality and speed. Q4_0 is faster but you&rsquo;ll notice quality degradation on complex tasks. Measure with your eval set before committing.</li>
<li><strong>Batching.</strong> If you have throughput-heavy workloads, batch requests. Most local runtimes support this. The latency per request goes up slightly but throughput goes up dramatically.</li>
<li><strong>Hardware.</strong> GPU inference is 10-50x faster than CPU for most model sizes. If you&rsquo;re serious about local AI, budget for a decent GPU. An RTX 4090 handles a 7B model comfortably.</li>
</ol>
<h2 id="the-honest-tradeoff">The Honest Tradeoff</h2>
<p>Local AI gives you control, privacy, and predictable costs. In exchange, you take on operational responsibility for model management, quality monitoring, and infrastructure maintenance. That&rsquo;s a fair trade for the right workloads.</p>
<p>Keep the stack small. Abstract the provider. Version everything. Measure quality continuously. Keep a cloud fallback for the moments when local isn&rsquo;t enough.</p>
<p>The teams that do this well treat local AI like any other infrastructure dependency &ndash; with discipline, not enthusiasm.</p>
]]></content:encoded></item><item><title>AI Workflow Automation: Decisions Are Cheap, Actions Are Expensive</title><link>https://lawzava.com/blog/2025-08-04-ai-workflow-automation/</link><pubDate>Mon, 04 Aug 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-08-04-ai-workflow-automation/</guid><description>The trick to AI workflow automation is simple: let the model decide, let deterministic code act, and never confuse the two.</description><content:encoded><![CDATA[<p>Last year I worked with a logistics company that had automated invoice processing with an  <a href="/blog/2024-04-01-agentic-workflows-production/"
   
   >AI agent</a>
. The agent read invoices, extracted line items, matched them to purchase orders, and approved payments. End to end. No human in the loop.</p>
<p>It worked beautifully for three months. Then the agent approved a $340,000 payment to a vendor who submitted a duplicate invoice with slightly different formatting. The model treated it as new. The validation layer didn&rsquo;t exist because &ldquo;the AI handles it.&rdquo;</p>
<p>Three hundred and forty thousand dollars. Because someone treated a probabilistic system like a deterministic one.</p>
<p>That experience crystallized a principle I repeat often: AI decides, deterministic code acts. Never the other way around.</p>
<h2 id="the-architecture-that-survives">The Architecture That Survives</h2>
<p>The separation is simple in concept and surprisingly rare in practice. The AI component receives structured context, produces a structured decision with a rationale, and stops there. Everything after that &ndash; validation, side effects, and the actual work &ndash; is deterministic code with explicit rules.</p>
<p>The flow:</p>
<ol>
<li><strong>Trigger</strong> arrives with metadata (a ticket, a document, an event)</li>
<li><strong>AI decision</strong> produces structured output &ndash; classification, extraction, routing recommendation, confidence score, and a short explanation of why</li>
<li><strong>Deterministic validation</strong> checks the decision against hard policy rules, allowlists, deny lists, and threshold constraints</li>
<li><strong>Action or escalation</strong> &ndash; if validation passes and confidence is high, execute. If not, route to human review with the full context attached</li>
<li><strong>Audit trail</strong> stores the input, the decision, the rationale, the validation result, and the final action</li>
</ol>
<p>Every step is logged. Every decision is replayable. If something goes wrong, you can trace exactly where and why.</p>
<h2 id="confidence-tiers-arent-optional">Confidence Tiers Aren&rsquo;t Optional</h2>
<p>Not every AI decision deserves the same treatment. A classification the model is 95% sure about is different from one it&rsquo;s 60% sure about. Your automation should know the difference.</p>
<p>I use three tiers everywhere:</p>
<ul>
<li><strong>High confidence</strong> &ndash; auto-approve, execute the action, log for periodic review</li>
<li><strong>Medium confidence</strong> &ndash; queue for human review with the AI&rsquo;s recommendation and rationale attached</li>
<li><strong>Low confidence</strong> &ndash; escalate immediately, flag for manual handling, don&rsquo;t proceed</li>
</ul>
<p>Thresholds depend on your domain. For invoice processing, I set the bar high because the cost of a wrong action is real money. For ticket triage, I set it lower because a misrouted ticket is annoying but recoverable.</p>
<p>The point is that uncertainty is a normal operating state. It isn&rsquo;t a bug. Your system should be designed to handle it gracefully instead of pretending every decision is confident.</p>
<h2 id="context-discipline">Context Discipline</h2>
<p>Feed the AI the minimum context needed to make a good decision. Not a raw database dump. Not the entire ticket history. Use a structured package: the specific document or event, the relevant policy excerpt, and a few representative examples of how similar cases were decided.</p>
<p>When teams dump everything into the  <a href="/blog/2024-07-22-context-window-strategies/"
   
   >context window</a>
, two things happen: token costs explode, and the model starts hallucinating connections between unrelated data points. More context isn&rsquo;t better context. Be deliberate about what matters for a specific decision.</p>
<h2 id="where-ai-automation-actually-fits">Where AI Automation Actually Fits</h2>
<p>Good fits: request triage, document classification, data extraction from messy formats, policy-based routing where ambiguity is expected and escalation is normal.</p>
<p>Bad fits: anything safety-critical, anything requiring hard real-time guarantees, anything where a wrong decision is irreversible and expensive. If you can&rsquo;t tolerate occasional uncertainty, don&rsquo;t automate with a probabilistic system.</p>
<p>From what I&rsquo;ve seen, the most successful automation projects started with a single workflow that already had a manual review path. They ran the AI in  <a href="/blog/2025-04-14-ai-testing-production/"
   
   >shadow mode</a>
 first, compared its decisions to the human decisions, measured agreement rates, and only then moved to live execution &ndash; with review still in place for the first few weeks.</p>
<h2 id="the-real-lesson">The Real Lesson</h2>
<p>That $340,000 duplicate payment wasn&rsquo;t a model failure. The model did exactly what it was designed to do &ndash; it classified the invoice and approved it. The failure was architectural. Nobody built the validation layer that should have caught a duplicate vendor-amount-date combination. Nobody defined the hard boundaries.</p>
<p>AI automation works when you respect what it is: a probabilistic decision engine. Wrap it with  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >deterministic guardrails</a>
, log everything, and keep humans in the loop for anything your business can&rsquo;t afford to get wrong.</p>
<p>Guardrails beat talent. Always.</p>
]]></content:encoded></item><item><title>AI Docs That Don't Lie to Your Users</title><link>https://lawzava.com/blog/2025-07-21-ai-documentation-systems/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-07-21-ai-documentation-systems/</guid><description>Most AI documentation systems retrieve the wrong version, hallucinate details, and never admit uncertainty. Here&amp;amp;rsquo;s how to build one that actually helps.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your AI docs system is only as good as its retrieval and its willingness to say &ldquo;I don&rsquo;t know.&rdquo; Use hybrid search, chunk by document structure with version metadata, cite sources in every answer, and treat freshness as a scheduled operational job &ndash; not a wish on the backlog.</p>
<hr>
<p>I contribute to Go regularly. I also use documentation from dozens of projects every day. And I can tell you the most common failure in  <a href="/blog/2024-09-16-technical-documentation-ai/"
   
   >developer documentation</a>
 isn&rsquo;t bad writing. It&rsquo;s bad retrieval.</p>
<p>A developer hits a cryptic error at midnight. They search. They get a result that looks right. It&rsquo;s from v2. They&rsquo;re on v4. The answer doesn&rsquo;t apply, but they don&rsquo;t realize it until they&rsquo;ve wasted forty minutes. Now multiply that across everyone using your docs.</p>
<p>That&rsquo;s the problem AI documentation systems need to solve. Not &ldquo;make the docs chatty.&rdquo; Make docs findable, version-accurate, and honest about gaps.</p>
<h2 id="the-three-problems-worth-solving">The Three Problems Worth Solving</h2>
<p><strong>Discovery.</strong> Users don&rsquo;t know your terminology. They describe symptoms, not concepts. A developer searching for &ldquo;connection refused after deploy&rdquo; might need the page about TLS configuration, but your keyword search returns the networking overview. Semantic search bridges this gap, but only if your chunks are meaningful units &ndash; not random 500-token slices.</p>
<p><strong>Version accuracy.</strong> Your API changed between v3 and v4. The auth flow is different. The error codes are different. If your retrieval doesn&rsquo;t filter by version, it will surface whatever is most popular in the index. Popular doesn&rsquo;t mean current.</p>
<p><strong>Freshness.</strong> Your product shipped a breaking change last Tuesday. The docs still describe the old behavior. Your AI docs system confidently explains how the old version works. This is worse than having no AI at all because it adds a layer of false authority.</p>
<h2 id="the-system-shape">The System Shape</h2>
<p>An AI docs system is a pipeline, not a chatbot with a  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector store</a>
 bolted on. The pieces that matter:</p>
<p><strong>Content store with metadata.</strong> Every chunk needs a stable ID, a version tag, a last-updated timestamp, and a source URL. Without these, you can&rsquo;t filter, you can&rsquo;t cite, and you can&rsquo;t detect staleness.</p>
<p><strong> <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >Hybrid retrieval</a>
.</strong>  <a href="/blog/2023-06-26-semantic-search-implementation/"
   
   >Semantic search</a>
 for conceptual questions. Keyword search for exact error codes, flag names, and parameter values. Neither alone is sufficient. The combination covers most queries. Add a reranking step that considers version relevance and recency &ndash; not just semantic similarity.</p>
<p><strong>Answer synthesis with citations.</strong> The model generates an answer, but every claim must trace to a specific chunk. If the retrieved chunks don&rsquo;t contain the answer, the system says so explicitly: &ldquo;This doesn&rsquo;t appear to be covered in the current docs. Here&rsquo;s the closest related section.&rdquo; A short answer with a source link beats a fluent paragraph that invents details.</p>
<p><strong>Feedback collection.</strong> Log every question that gets a low-confidence response or explicit negative feedback. Route those to doc owners weekly. This is the actual improvement loop. Without it, you&rsquo;re flying blind.</p>
<h2 id="chunking-matters-more-than-model-choice">Chunking Matters More Than Model Choice</h2>
<p>I&rsquo;ve seen teams agonize over which LLM to use for synthesis while completely ignoring their  <a href="/blog/2025-05-26-ai-data-pipelines/"
   
   >chunking strategy</a>
. The chunking is where the battle is won or lost.</p>
<p>Split by document structure. Headings, sections, and code blocks are natural semantic boundaries. A chunk should be a coherent unit that can answer a question on its own, or clearly can&rsquo;t. Token-count splitting produces fragments that retrieve well by similarity score but fail at actually answering questions.</p>
<p>Attach version metadata to every chunk. If someone asks about v4 auth, filter to v4 chunks before retrieval. This isn&rsquo;t a nice-to-have. It&rsquo;s the difference between helpful and harmful.</p>
<h2 id="freshness-is-ops-work">Freshness Is Ops Work</h2>
<p>Docs go stale. This isn&rsquo;t a failure of discipline &ndash; it&rsquo;s a consequence of shipping software. The solution isn&rsquo;t &ldquo;write better docs.&rdquo; The solution is automated freshness checks.</p>
<p>Schedule weekly jobs that validate links, compare API schema hashes against the documented version, and flag code samples that reference deprecated methods. When a check fails, create a ticket with clear ownership and a deadline. Not a backlog item. A real deadline.</p>
<p>At the fintech startup, we learned this the hard way with financial data: stale information in a financial context isn&rsquo;t just unhelpful, it&rsquo;s dangerous. The same principle applies to docs. Stale docs users trust are worse than no docs at all.</p>
<h2 id="measure-success-by-questions-answered">Measure Success by Questions Answered</h2>
<p>Pageviews are meaningless for docs. The metric that matters is: did the user get the right answer?</p>
<p>Track question success rate through explicit thumbs-up/down on AI answers. Track the count of unanswered or low-confidence questions &ndash; these are your improvement backlog. Track time-to-update for pages flagged as stale.</p>
<p>The feedback loop is the product. The AI layer is just the delivery mechanism. If unanswered questions aren&rsquo;t flowing back into your  <a href="/blog/2022-06-13-engineering-documentation-practices/"
   
   >documentation process</a>
, your AI docs system is a search box with extra steps.</p>
<p>Build retrieval that respects versions. Require citations. Admit uncertainty. Treat freshness as an operational discipline. Everything else is decoration.</p>
]]></content:encoded></item><item><title>Your AI Metrics Are Measuring the Wrong Thing</title><link>https://lawzava.com/blog/2025-07-07-ai-product-metrics/</link><pubDate>Mon, 07 Jul 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-07-07-ai-product-metrics/</guid><description>Engagement metrics tell you people clicked. They tell you nothing about whether your AI feature actually helped anyone do anything.</description><content:encoded><![CDATA[<p>Every AI product review I sit in starts the same way: someone pulls up a dashboard showing adoption rates, interaction volume, and session length. The numbers are up and to the right. Everyone nods.</p>
<p>Then I ask: &ldquo;How many of those interactions ended with the user getting the right answer?&rdquo; Silence.</p>
<p>This is the metrics gap that keeps burning teams. Usage tells you people showed up. It tells you nothing about whether they left with what they needed. An AI feature can be heavily used and actively harmful at the same time. Users try it, get a wrong answer, correct it manually, and keep coming back because they&rsquo;re optimistic. Your dashboard shows engagement. Your product is eroding trust.</p>
<h2 id="what-to-actually-measure">What to Actually Measure</h2>
<p>Three things. That&rsquo;s it.</p>
<p><strong>Did the output help?</strong> Not &ldquo;was it generated.&rdquo; Did it contribute to the user completing their task? Define what successful completion looks like for your specific workflow, then measure whether AI-assisted completions happen more often, faster, or with fewer errors than the baseline. If you can&rsquo;t tie AI output to a task outcome, you&rsquo;re measuring wind.</p>
<p><strong>Was it correct?</strong> Combine  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >automated checks</a>
 with periodic human review. Automated checks catch format violations, hallucinated entities, and  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >safety issues</a>
. Human review catches the subtle stuff: answers that are technically correct but misleading, or correct for the wrong version. Sample 5% of outputs weekly. That&rsquo;s enough to spot trends before they become incidents.</p>
<p><strong>Do users trust it?</strong> Trust is the leading indicator everyone ignores. Track it through implicit signals: how often users edit AI output before accepting it, how often they abandon a flow after seeing the AI response, and how often they re-prompt with the same question phrased differently. Rising edit rates or re-prompt rates mean trust is declining. By the time CSAT surveys catch this, you&rsquo;ve already lost months.</p>
<h2 id="the-dashboard-that-fits-on-one-screen">The Dashboard That Fits on One Screen</h2>
<p>Your AI scorecard should answer four questions at a glance:</p>
<ol>
<li>Are people using it? (adoption, retention &ndash; the basics)</li>
<li>Is the output good? (correctness rate, safety rate from automated + human review)</li>
<li>Is it helping? (task completion rate, time to completion vs. baseline)</li>
<li>Do they trust it? (edit rate, re-prompt rate, abandonment rate)</li>
</ol>
<p>Review weekly. Tie every metric to a decision. If a number moves and nobody changes anything, delete the number. Dashboards without decisions are theater.</p>
<p>When a metric dips, you should be able to trace it back to a model update, a retrieval change, or a product shift within the same week. If you can&rsquo;t, your  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >instrumentation</a>
 is too coarse.</p>
<h2 id="the-uncomfortable-truth">The Uncomfortable Truth</h2>
<p>Most teams avoid quality metrics because they&rsquo;re harder to collect and the numbers are less flattering than engagement counts. That&rsquo;s exactly why they matter. The teams that measure task success and trust alongside usage are the ones whose AI features survive past the demo phase.</p>
<p>Measure what the user felt. Everything else is vanity.</p>
]]></content:encoded></item><item><title>Stop Fine-Tuning Models You Haven't Bothered to Prompt Properly</title><link>https://lawzava.com/blog/2025-06-23-fine-tuning-when-why/</link><pubDate>Mon, 23 Jun 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-06-23-fine-tuning-when-why/</guid><description>Fine-tuning is the goto move for teams who skipped the basics. Most of the time, better prompts and proper retrieval solve the actual problem.</description><content:encoded><![CDATA[<p>I need to get something off my chest. I&rsquo;ve reviewed six AI projects in the last two months where teams jumped straight to fine-tuning. Six. Not one of them had tried proper  <a href="/blog/2023-05-15-fine-tuning-vs-prompting/"
   
   >few-shot prompting</a>
 first. Not one had a retrieval layer for domain knowledge. They saw &ldquo;the model doesn&rsquo;t know our stuff&rdquo; and immediately reached for the most expensive, most maintenance-heavy tool in the shed.</p>
<p>This drives me nuts.</p>
<h2 id="fine-tuning-isnt-a-knowledge-injection">Fine-Tuning Isn&rsquo;t a Knowledge Injection</h2>
<p>Let me say this clearly: fine-tuning changes behavior, not knowledge. If your problem is &ldquo;the model doesn&rsquo;t know about our product,&rdquo; the answer is retrieval.  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >RAG</a>
. Grounding. Whatever you want to call it, feed the model your docs at inference time.</p>
<p>Fine-tuning bakes patterns into weights. It&rsquo;s good for consistent tone, strict output formats, and narrow tasks repeated at massive scale. It&rsquo;s terrible for facts that change, knowledge that needs updating, or anything where you want to point at a source and say &ldquo;the answer came from here.&rdquo;</p>
<p>I&rsquo;ve watched teams spend weeks curating training data to teach a model their product catalog. Then the catalog changes. Now the model confidently recommends products that no longer exist. Retrieval would have solved this in an afternoon.</p>
<h2 id="the-decision-is-simple">The Decision Is Simple</h2>
<p>Before you fine-tune anything, answer these questions honestly:</p>
<p><strong>Have you pushed the prompt hard?</strong> Not a one-liner. A real  <a href="/blog/2023-02-06-prompt-engineering-fundamentals/"
   
   >system prompt</a>
 with role definition, constraints, examples, and output format. Most teams write a lazy prompt, get mediocre results, and conclude the model needs training. No. Their prompt needs training.</p>
<p><strong>Have you added retrieval?</strong> If the issue is domain knowledge, factual accuracy, or up-to-date information, retrieval is the answer. Fine-tuning can&rsquo;t compete with a well-indexed knowledge base for factual tasks.</p>
<p><strong>Is the remaining gap about behavior?</strong> After good prompts and solid retrieval, if the model still can&rsquo;t hold a consistent tone, reliably produce a specific output structure, or stop drifting on a narrow repeated task, now we can talk about fine-tuning.</p>
<p><strong>Is the volume worth it?</strong> Fine-tuning has upfront cost and ongoing maintenance. If the task runs ten times a day, just use a better prompt. If it runs ten thousand times a day and  <a href="/blog/2023-07-24-ai-cost-optimization/"
   
   >prompt tokens are eating your budget</a>
, fine-tuning starts to make economic sense.</p>
<h2 id="the-maintenance-tax-nobody-mentions">The Maintenance Tax Nobody Mentions</h2>
<p>Here&rsquo;s what the fine-tuning tutorials leave out. A tuned model is a versioned product. Your training data reflects a snapshot of your business at a moment in time. Products change. Policies change. Customer expectations change. Your training set drifts.</p>
<p>That means you need:</p>
<ul>
<li>Versioned training sets in source control</li>
<li>A  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >holdout evaluation set</a>
 that you run against every new version</li>
<li> <a href="/blog/2023-08-21-llm-observability/"
   
   >Monitoring for quality regression</a>
 in production</li>
<li>A refresh cadence that&rsquo;s actually budgeted and scheduled</li>
</ul>
<p>I&rsquo;ve seen exactly one team do all of this well. Everyone else fine-tuned once, celebrated, and then watched quality slowly degrade over three months while nobody noticed because nobody was measuring.</p>
<h2 id="when-i-actually-recommend-it">When I Actually Recommend It</h2>
<p>I&rsquo;m not anti-fine-tuning. I&rsquo;m anti-premature-fine-tuning. The legitimate cases exist:</p>
<ul>
<li>You need a specific voice or brand tone that holds across thousands of outputs and few-shot examples aren&rsquo;t stable enough</li>
<li>You have a  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >narrow classification or extraction task</a>
 at high volume where shaving prompt tokens saves real money</li>
<li>You need a strict output schema and the base model keeps introducing creative variations despite explicit instructions</li>
</ul>
<p>From what I&rsquo;ve seen, maybe one in five projects that ask about fine-tuning actually need it. The rest need better prompts, proper retrieval, or both.</p>
<h2 id="the-honest-checklist">The Honest Checklist</h2>
<ol>
<li>Write a real system prompt with examples and constraints. Test it on 50 representative inputs.</li>
<li>If factual accuracy is the gap, add retrieval. Test again.</li>
<li>If behavior consistency is still the gap at high volume, collect 200+ high-quality examples that match real production inputs.</li>
<li>Hold out 20% for evaluation. Fine-tune. Compare against the base model on both your target metric and general reasoning.</li>
<li>If the tuned model wins on behavior but loses on reasoning, reconsider whether the tradeoff is worth it.</li>
<li>Version everything. Monitor everything. Schedule refreshes.</li>
</ol>
<p>Stop treating fine-tuning as step one. It&rsquo;s step last.</p>
]]></content:encoded></item><item><title>AI Customer Support That Doesn't Make People Hate You</title><link>https://lawzava.com/blog/2025-06-09-ai-customer-support/</link><pubDate>Mon, 09 Jun 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-06-09-ai-customer-support/</guid><description>Most AI support systems are built to deflect tickets. The ones that work are built around escalation, grounding, and the idea that customers aren&amp;amp;rsquo;t idiots.</description><content:encoded><![CDATA[<p>I have a confession: I&rsquo;ve rage-quit a support chat with an AI bot at least four times this year. And I build these systems for a living.</p>
<p>The problem is rarely the technology. The problem is that someone decided the goal was &ldquo;deflect tickets&rdquo; instead of &ldquo;help customers.&rdquo; Those goals produce completely different systems.</p>
<p>At a shared mobility startup I ran, we handled support for thousands of riders across multiple cities. Some of it was straightforward &ndash; &ldquo;where is my scooter&rdquo; kind of stuff. Some of it wasn&rsquo;t &ndash; billing disputes, safety incidents, regulatory questions. The lesson that stuck with me was simple: the moment a customer feels trapped in a loop with no exit, you&rsquo;ve lost them. Permanently.</p>
<p>That lesson applies directly to AI support.</p>
<h2 id="design-for-the-handoff-not-the-deflection">Design for the Handoff, Not the Deflection</h2>
<p>The best AI support systems I&rsquo;ve seen share one trait: they&rsquo;re obsessed with the handoff. The AI handles the routine stuff &ndash; password resets, order status, basic troubleshooting. Fine. But the moment the conversation crosses into ambiguity, billing, account security, or anything emotionally charged, it routes to a human. Fast. With full context attached.</p>
<p>Full context means the customer doesn&rsquo;t have to repeat themselves. It means the human agent sees the conversation history, account state, prior tickets, and the AI&rsquo;s confidence assessment. If your handoff drops any of that, your human agent starts from zero and the customer feels punished for escalating.</p>
<p>Make escalation a one-tap action. Not buried in a menu. Not &ldquo;please describe your issue again so we can route you.&rdquo; One tap. Every screen.</p>
<h2 id="ground-answers-or-say-nothing">Ground Answers or Say Nothing</h2>
<p>Here&rsquo;s where most AI support goes sideways: the model generates a plausible-sounding answer that&rsquo;s completely wrong. The customer follows it, makes things worse, and now you have a pissed-off user and a support ticket that&rsquo;s twice as hard to resolve.</p>
<p>The fix is  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >grounding</a>
. Every answer the AI gives should be traceable to current documentation or a known resolution pattern. If the system can&rsquo;t find a source, it should say so. &ldquo;I don&rsquo;t have a verified answer for this &ndash; let me connect you with someone who does.&rdquo; That sentence is worth more than a thousand confidently wrong paragraphs.</p>
<p>For anything touching billing, account access, or security &ndash; require a source citation or refuse to answer. No exceptions. A cautious deferral builds trust. A confident hallucination destroys it.</p>
<h2 id="context-isnt-optional">Context Isn&rsquo;t Optional</h2>
<p>Your AI support bot should know who it&rsquo;s talking to:  <a href="/blog/2024-07-22-context-window-strategies/"
   
   >conversation history</a>
, account state, prior tickets, current subscription tier. If the customer told you their name and order number two messages ago, don&rsquo;t ask again.</p>
<p>This sounds obvious, but it&rsquo;s shocking how many  <a href="/blog/2023-01-09-ai-in-production/"
   
   >production systems</a>
 get it wrong. They treat every message as an independent event because someone optimized for stateless simplicity instead of user experience.</p>
<p>Context also means understanding what has already been tried. If the customer says &ldquo;I already restarted the app,&rdquo; don&rsquo;t suggest restarting the app. The AI should parse prior attempts and skip the obvious stuff. This is where  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval</a>
 over conversation history earns its keep.</p>
<h2 id="measure-what-the-customer-feels">Measure What the Customer Feels</h2>
<p>Most teams measure deflection rate as their primary AI support metric. That tells you how many tickets the AI intercepted. It tells you nothing about whether customers got help.</p>
<p>Measure these instead:</p>
<ul>
<li><strong>CSAT per interaction</strong> &ndash; not aggregate, per conversation. Did this specific person feel helped?</li>
<li><strong>Time to resolution</strong> &ndash; including escalation time. If AI adds a 10-minute runaround before connecting to a human, that&rsquo;s worse than no AI at all.</li>
<li><strong>Repeat contacts</strong> &ndash; if the same customer comes back about the same issue, the first interaction failed. Full stop.</li>
<li><strong>Escalation quality</strong> &ndash; when the AI hands off, does the human have enough context to pick up immediately?</li>
</ul>
<p>Review these weekly. Not monthly. Weekly. Because AI support quality can drift fast when your knowledge base gets stale or your product ships a change that the docs haven&rsquo;t caught up with.</p>
<h2 id="start-narrow-stay-honest">Start Narrow, Stay Honest</h2>
<p>Don&rsquo;t launch AI support across every channel and every topic on day one. Pick the three most common, routine request types. Test internally. Get the escalation path rock solid. Make sure the knowledge base is current.</p>
<p>Then expand. Slowly. Treat every failed conversation as signal &ndash; a gap in your docs, a missing retrieval path, a policy AI doesn&rsquo;t know about. That feedback loop is the actual product. The chatbot is just the interface.</p>
<p>AI support works when it&rsquo;s built around humility &ndash; the system&rsquo;s humility about what it knows, and the team&rsquo;s humility about what it can handle. Everything else is a demo.</p>
]]></content:encoded></item><item><title>Your AI Pipeline Is Just ETL With Extra Steps (And That's Fine)</title><link>https://lawzava.com/blog/2025-05-26-ai-data-pipelines/</link><pubDate>Mon, 26 May 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-05-26-ai-data-pipelines/</guid><description>AI data pipelines are ETL with a retrieval layer bolted on. The discipline is the same as always: detect change, chunk intelligently, keep indexes fresh.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop overcomplicating AI pipelines. They&rsquo;re ETL plus retrieval ops. Diff your inputs, chunk by structure (not token count), upsert with stable IDs, and treat reindexing as a deliberate, versioned event. Skip the diffing step and retrieval drifts into garbage. I&rsquo;ve seen it happen three times this year alone.</p>
<hr>
<p>I&rsquo;ve been building  <a href="/blog/2017-04-24-building-data-pipelines-that-dont-break/"
   
   >data pipelines</a>
 since before anyone called them &ldquo;data pipelines.&rdquo; At the fintech startup we were ingesting financial news from hundreds of sources, normalizing it, and serving it for real-time retrieval. That was 2017. The core problems haven&rsquo;t changed.</p>
<p>What has changed is that your pipeline now has a second consumer: a  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval system</a>
 feeding an LLM. If you treat that consumer as an afterthought, your AI product will deliver confidently wrong answers. Ruthless focus on the basics separates pipelines that work from pipelines that demo well.</p>
<h2 id="the-shape-of-an-ai-pipeline">The Shape of an AI Pipeline</h2>
<p>Every AI pipeline I&rsquo;ve seen in production boils down to six stages. Here&rsquo;s the skeleton:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">pipeline</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stages</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">extract</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Pull from sources, normalize formats</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># PDF, HTML, API responses -&gt; clean markdown or structured text</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">diff</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Hash-based change detection</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># This is the stage most teams skip. Don&#39;t.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">chunk</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Split by document structure first, token count second</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Preserve section boundaries and headings</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">embed</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Generate vectors using a pinned model version</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Log the model version. You will need it later.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">index</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Upsert with stable IDs and rich metadata</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># source_id + chunk_position = deterministic ID</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">verify</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Check for missing chunks, stale entries, orphans</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Alert on drift from expected source freshness</span>
</span></span></code></pre></div><p>Nothing exotic. The magic is in the discipline of each stage, not in clever architecture.</p>
<h2 id="the-diff-step-is-everything">The Diff Step Is Everything</h2>
<p>Most teams skip change detection and reprocess everything on every run. At small scale, this is fine. At production scale, it&rsquo;s expensive, noisy, and makes debugging a nightmare.</p>
<p>A simple content-hash approach works well:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">hasChanged</span>(<span style="color:#a6e22e">sourceID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">content</span> []<span style="color:#66d9ef">byte</span>, <span style="color:#a6e22e">store</span> <span style="color:#a6e22e">HashStore</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">newHash</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sha256</span>.<span style="color:#a6e22e">Sum256</span>(<span style="color:#a6e22e">content</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">existing</span>, <span style="color:#a6e22e">found</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">sourceID</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">found</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">sourceID</span>, <span style="color:#a6e22e">newHash</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">existing</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">newHash</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">sourceID</span>, <span style="color:#a6e22e">newHash</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When I built the ingestion pipeline at the fintech startup, adding a diff layer cut downstream processing costs by roughly 60%. Most sources don&rsquo;t change on most runs. Detecting that early saves everything downstream.</p>
<p>The diff step also gives you auditability. You can answer &ldquo;what changed and when&rdquo; instead of shrugging at a  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector store</a>
 that silently drifted.</p>
<h2 id="chunking-structure-before-size">Chunking: Structure Before Size</h2>
<p>This is where most  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >RAG pipelines</a>
 go wrong. Teams reach for a token-count splitter because it&rsquo;s the default in every tutorial, then wonder why retrieval returns fragments of ideas instead of coherent answers.</p>
<p>Split by document structure first. Headings, sections, code blocks, list items &ndash; these are natural semantic boundaries. Only fall back to token-count splitting when a single section exceeds your context window.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">chunk_by_structure</span>(doc: Document) <span style="color:#f92672">-&gt;</span> list[Chunk]:
</span></span><span style="display:flex;"><span>    chunks <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> section <span style="color:#f92672">in</span> doc<span style="color:#f92672">.</span>sections:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> section<span style="color:#f92672">.</span>token_count <span style="color:#f92672">&lt;=</span> MAX_CHUNK_TOKENS:
</span></span><span style="display:flex;"><span>            chunks<span style="color:#f92672">.</span>append(Chunk(
</span></span><span style="display:flex;"><span>                content<span style="color:#f92672">=</span>section<span style="color:#f92672">.</span>text,
</span></span><span style="display:flex;"><span>                metadata<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;source_id&#34;</span>: doc<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;section_heading&#34;</span>: section<span style="color:#f92672">.</span>heading,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;position&#34;</span>: section<span style="color:#f92672">.</span>index,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;doc_version&#34;</span>: doc<span style="color:#f92672">.</span>version,
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># Deterministic ID: no duplicates on re-ingestion</span>
</span></span><span style="display:flex;"><span>                id<span style="color:#f92672">=</span><span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>doc<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>section<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>            ))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Fall back to sliding window only for oversized sections</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> sub <span style="color:#f92672">in</span> sliding_window(section, MAX_CHUNK_TOKENS, overlap<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>):
</span></span><span style="display:flex;"><span>                chunks<span style="color:#f92672">.</span>append(Chunk(
</span></span><span style="display:flex;"><span>                    content<span style="color:#f92672">=</span>sub<span style="color:#f92672">.</span>text,
</span></span><span style="display:flex;"><span>                    metadata<span style="color:#f92672">=</span>{<span style="color:#f92672">**</span>section<span style="color:#f92672">.</span>metadata, <span style="color:#e6db74">&#34;sub_position&#34;</span>: sub<span style="color:#f92672">.</span>index},
</span></span><span style="display:flex;"><span>                    id<span style="color:#f92672">=</span><span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>doc<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>section<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>sub<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                ))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> chunks
</span></span></code></pre></div><p>Two things matter here. First, the <code>id</code> is deterministic, derived from source and position, not random. This means re-ingesting the same content produces upserts, not duplicates. Second, metadata travels with every chunk. When retrieval returns a chunk, you know exactly where it came from, which version, and which section.</p>
<p>I can&rsquo;t overstate how many production RAG systems I&rsquo;ve reviewed where chunks had no stable ID. Every reindex created duplicates. Users got the same passage three times in their context window, and the model hallucinated a consensus that didn&rsquo;t exist.</p>
<h2 id="freshness-is-an-operational-problem">Freshness Is an Operational Problem</h2>
<p>Your pipeline isn&rsquo;t done when it runs once. Sources change, APIs update, and documents get deleted. If your index doesn&rsquo;t reflect reality, your AI lies with confidence.</p>
<p>Three rules I enforce on every pipeline:</p>
<ol>
<li>
<p><strong>Reindex on embedding model changes.</strong> If you swap or upgrade your  <a href="/blog/2023-07-10-embedding-models-deep-dive/"
   
   >embedding model</a>
, every existing vector is stale. This is a full reindex event. No exceptions. Pin your model version and log it.</p>
</li>
<li>
<p><strong>Purge on source deletion.</strong> If a document disappears from the source, its chunks must disappear from the index. Orphaned chunks are a retrieval poison pill.</p>
</li>
<li>
<p><strong>Alert on freshness drift.</strong> Every source has an expected update cadence. If your financial news feed hasn&rsquo;t updated in 6 hours, something is wrong. Don&rsquo;t wait for a user to notice.</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">freshness_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">sources</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">product_docs</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">24h</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">36h</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api_changelog</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">7d</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">10d</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">support_kb</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">48h</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">72h</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">on_embedding_change</span>: <span style="color:#ae81ff">full_reindex</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">on_source_delete</span>: <span style="color:#ae81ff">purge_chunks</span>
</span></span></code></pre></div><h2 id="the-mistakes-i-keep-seeing">The Mistakes I Keep Seeing</h2>
<p>After building AI infrastructure across telecom and fintech, the failure pattern is remarkably consistent:</p>
<p><strong>No stable IDs.</strong> Updates create duplicates. Retrieval returns the same content multiple times. The model treats repetition as emphasis and doubles down on whatever it found.</p>
<p><strong>Token-count-only chunking.</strong> A paragraph about authentication gets split mid-sentence. The first half lands in one chunk, the second half in another. Retrieval finds the first half. The model confidently gives half an answer.</p>
<p><strong>Ad-hoc reindexing.</strong> Someone runs a reindex on a Friday afternoon. Nobody knows what changed. Retrieval quality shifts. The team argues about whether it got better or worse. No one can prove either way because there&rsquo;s no baseline.</p>
<p><strong>Missing permission metadata.</strong> The chunks are indexed without access control data. A user with restricted access asks a question and gets an answer sourced from documents they shouldn&rsquo;t see. This is a compliance incident waiting to happen.</p>
<h2 id="what-matters">What matters</h2>
<p>AI pipelines are pipelines. The retrieval layer adds real complexity, but the solution isn&rsquo;t a new paradigm. It&rsquo;s the same discipline that has always worked: detect change early, preserve meaning when you split, keep identifiers stable, and make freshness an operational concern with clear owners and alerts.</p>
<p>Same fundamentals, new surface area. That&rsquo;s the whole story.</p>
]]></content:encoded></item><item><title>Agent Orchestration: Four Patterns, Honest Tradeoffs</title><link>https://lawzava.com/blog/2025-05-12-ai-agent-orchestration/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-05-12-ai-agent-orchestration/</guid><description>Multi-agent systems are distributed systems with the usual coordination headaches. The four patterns I&amp;amp;rsquo;ve seen work, and when each one falls apart.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>More agents doesn&rsquo;t mean better results. It means more coordination overhead and more failure modes. Start with a simple pipeline, add a verifier, and only go multi-agent when you can clearly define who owns each decision. If your agents don&rsquo;t have contracts, you don&rsquo;t have orchestration &ndash; you have chaos.</p>
<hr>
<p>I keep getting asked about  <a href="/blog/2024-10-28-advanced-agent-patterns/"
   
   >multi-agent architectures</a>
. Teams see the demos &ndash; agents collaborating, debating, building things together &ndash; and they want that. What they usually need is simpler.</p>
<p>The uncomfortable truth about agent orchestration is that it&rsquo;s just  <a href="/blog/2022-05-30-distributed-systems-patterns/"
   
   >distributed systems</a>
 with worse debugging tools. Every coordination problem you&rsquo;ve seen in  <a href="/blog/2016-01-15-why-microservices-arent-always-the-answer/"
   
   >microservices</a>
 shows up again: unclear ownership, implicit state,  <a href="/blog/2019-05-06-designing-for-failure/"
   
   >cascading failures</a>
, and the seductive illusion that more components mean more capability.</p>
<p>That said, there are real use cases where multiple agents outperform a single one. The key is choosing the right pattern and being honest about the tradeoffs.</p>
<h2 id="the-four-patterns">The four patterns</h2>
<p>After building and reviewing  <a href="/blog/2023-09-18-agent-architecture-patterns/"
   
   >agent systems in production</a>
, I&rsquo;ve landed on four patterns that cover most real-world use cases.</p>
<h3 id="1-sequential-pipeline">1. Sequential pipeline</h3>
<p>The simplest pattern. Agent A does research, passes results to Agent B for analysis, then Agent B passes to Agent C for writing. Each agent has a clear input and output contract.</p>
<p><strong>When it works:</strong> Tasks with a natural sequence of distinct steps. Content generation pipelines. Data processing workflows. Anything where each step builds on the previous one.</p>
<p><strong>When it breaks:</strong> Early agents produce weak output and later agents can&rsquo;t recover. Errors compound. The pipeline is only as good as its weakest step.</p>
<p><strong>My rule:</strong> Add explicit checkpoints between stages. If Agent B receives garbage from Agent A, it should reject and request a retry rather than trying to work with bad input. We learned this the hard way on a project &ndash; a research agent that returned vague summaries poisoned every downstream step.</p>
<h3 id="2-parallel-execution">2. Parallel execution</h3>
<p>Multiple agents work on the same problem independently, then results are merged. Think: three agents each review a PR from a different angle (logic, security, performance), and a synthesis step combines their findings.</p>
<p><strong>When it works:</strong> Tasks where multiple perspectives add value. Review workflows. Risk assessment. Brainstorming alternatives.</p>
<p><strong>When it breaks:</strong> The synthesis step. Merging conflicting agent outputs is hard. If your merge strategy is &ldquo;average the results&rdquo; or &ldquo;take the longest response,&rdquo; you&rsquo;re losing the benefit of parallel execution.</p>
<p><strong>My rule:</strong> Define merge rules explicitly. Conflicts get escalated to a human or resolved by a designated arbiter agent with clear criteria.</p>
<h3 id="3-hierarchical-orchestration">3. Hierarchical orchestration</h3>
<p>A coordinator agent breaks work into subtasks, delegates to specialist agents, and assembles the final result. This is the manager-worker pattern.</p>
<p><strong>When it works:</strong> Large, complex tasks that can be decomposed. Project planning. Multi-file code generation. Report compilation from multiple data sources.</p>
<p><strong>When it breaks:</strong> The coordinator overfits to its initial plan. If subtask results invalidate the plan, the coordinator needs to replan. Most implementations don&rsquo;t handle this well &ndash; the coordinator stubbornly follows the original decomposition even when evidence says it shouldn&rsquo;t.</p>
<p><strong>My rule:</strong> Give the coordinator explicit replanning triggers. If a subtask fails or returns unexpected results, the coordinator reassesses before continuing.</p>
<h3 id="4-debate-and-verification">4. Debate and verification</h3>
<p>Two or more agents argue opposing positions. A judge agent evaluates the arguments and makes a final call. This pattern surfaces assumptions and edge cases that a single agent misses.</p>
<p><strong>When it works:</strong> Decisions with genuine uncertainty. Code review where the tradeoffs are unclear. Risk assessment where different framings lead to different conclusions.</p>
<p><strong>When it breaks:</strong> Agents generate artificial disagreement to fill their roles. Or the judge defaults to the more verbose argument. The pattern needs real divergence to add value.</p>
<p><strong>My rule:</strong> Only use debate when the single-agent answer has measurable uncertainty. If the task has a clear correct answer, debate is overhead.</p>
<h2 id="pattern-comparison">Pattern comparison</h2>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Best for</th>
          <th>Failure mode</th>
          <th>Complexity</th>
          <th>Agent count</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sequential pipeline</td>
          <td>Step-by-step workflows</td>
          <td>Error compounding</td>
          <td>Low</td>
          <td>2-4</td>
      </tr>
      <tr>
          <td>Parallel execution</td>
          <td>Multi-perspective review</td>
          <td>Bad merge logic</td>
          <td>Medium</td>
          <td>3-5</td>
      </tr>
      <tr>
          <td>Hierarchical</td>
          <td>Large decomposable tasks</td>
          <td>Rigid planning</td>
          <td>High</td>
          <td>3-8</td>
      </tr>
      <tr>
          <td>Debate/verification</td>
          <td>Uncertain decisions</td>
          <td>Artificial disagreement</td>
          <td>Medium</td>
          <td>2-3</td>
      </tr>
  </tbody>
</table>
<h2 id="the-coordination-basics-nobody-talks-about">The coordination basics nobody talks about</h2>
<p>The pattern is the easy part. The hard part is the coordination contract between agents. Every agent needs:</p>
<ul>
<li><strong>Defined inputs and outputs.</strong> Not &ldquo;whatever seems relevant.&rdquo; A schema. Required fields. Validation at the boundary.</li>
<li><strong>Pass/retry/escalate criteria.</strong> What does the next agent do when it receives bad input? Accept it? Reject it? Ask for clarification? This must be explicit.</li>
<li><strong>Short, stable context.</strong> Don&rsquo;t pass the entire conversation history between agents. Pass a structured summary of what the previous agent decided and why. Long contexts lead to confusion and drift.</li>
<li><strong>Decision logging.</strong> Every agent decision gets logged with reasoning. When the final output is wrong, you need to trace which agent made the bad call and why.</li>
</ul>
<p>Without these, adding agents just multiplies failure modes. You get more components and less reliability. I&rsquo;ve seen teams build five-agent systems that performed worse than a single well-prompted model because coordination overhead drowned out the benefits.</p>
<h2 id="when-to-not-use-multi-agent">When to not use multi-agent</h2>
<p>Most of the time.</p>
<p>I&rsquo;m serious. A single agent with good tools, clear instructions, and a verification step handles 80% of use cases better than a multi-agent system. Multi-agent adds value when:</p>
<ul>
<li>The task genuinely requires different capabilities or perspectives</li>
<li>Verification needs to be independent from generation</li>
<li>The work can be parallelized for speed</li>
<li>No single prompt can hold all the necessary context</li>
</ul>
<p>If none of those apply, you&rsquo;re adding complexity for its own sake.</p>
<h2 id="how-i-start">How I start</h2>
<p>Two agents. One that does the work. One that checks the work. That&rsquo;s it. The generator-verifier pattern is the simplest multi-agent setup and the one with the highest reliability improvement per unit of added complexity.</p>
<p>Once the generator-verifier is stable and measured, you can consider whether splitting the generator into specialized sub-agents would help. Usually it doesn&rsquo;t. But when it does &ndash; when you have distinct expertise domains that benefit from isolation &ndash; the improvement is real.</p>
<p>Start simple. Add complexity only when you can measure the improvement. Orchestration isn&rsquo;t a goal. Reliability is.</p>
]]></content:encoded></item><item><title>AI Security: Same Principles, New Attack Surface</title><link>https://lawzava.com/blog/2025-04-28-ai-security-2025/</link><pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-04-28-ai-security-2025/</guid><description>AI systems are exposed APIs with real blast radius. The threats are injection, leakage, and tool misuse. The defenses are the ones we&amp;amp;rsquo;ve always needed.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Treat every AI endpoint like an exposed API that can be tricked into doing things you didn&rsquo;t intend. Separate trusted instructions from untrusted content. Constrain tool access. Filter outputs for leakage. Monitor like the system is adversarial, because someone will make it so. Security, stability, performance &ndash; in that order.</p>
<hr>
<p>During a national cyber-defense exercise a few years back, we ran a scenario where the opposing team compromised an automated decision support system. They didn&rsquo;t hack the system in the traditional sense. They fed it manipulated data that changed its recommendations. The system worked exactly as designed. It just made the wrong decisions because its inputs were poisoned.</p>
<p>That scenario has stayed in my head this year because it&rsquo;s exactly what  <a href="/blog/2023-10-30-llm-security-considerations/"
   
   >prompt injection</a>
 does to AI systems. The model works as designed. The inputs are manipulated. The outputs are wrong. And the system has no idea.</p>
<h2 id="the-threat-model-isnt-theoretical">The threat model isn&rsquo;t theoretical</h2>
<p>Every AI system I see in production combines three things that should make security engineers nervous:</p>
<ol>
<li><strong>Untrusted user input</strong> goes directly into the model context.</li>
<li><strong>Retrieved content</strong> from external sources is treated as context, not as untrusted data.</li>
<li><strong>Tool access</strong> allows the model to take actions with real consequences.</li>
</ol>
<p>Mix those three together and you get a system where a malicious string in a support ticket can, in the worst case, cause the model to call an internal API, exfiltrate data, or take an action that nobody authorized.</p>
<p>This isn&rsquo;t hypothetical. I&rsquo;ve seen prompt injection succeed against production systems. In one case, a user embedded instructions in a document that was retrieved during RAG. The model followed those instructions and included internal system prompt details in its response. The user got a screenshot and posted it on social media. Not a great day for that team.</p>
<h2 id="where-the-attacks-land">Where the attacks land</h2>
<p><strong>Prompt injection</strong> is the big one. Direct injection, where the user types instructions that override the system prompt, is the obvious case. Indirect injection is scarier: malicious instructions embedded in retrieved documents, emails, or web pages that the model processes. The model can&rsquo;t reliably distinguish &ldquo;instructions from the developer&rdquo; from &ldquo;instructions from an attacker hiding in the data.&rdquo;</p>
<p><strong>Data leakage</strong> is the second big one. Models will echo back their system prompts, retrieved context, or other users&rsquo; data if you ask the right way. Output filtering catches some of this. But the model is creative, and attackers are more creative. Assume that anything in the context window can potentially appear in the output.</p>
<p><strong>Tool misuse</strong> is the emerging threat. As AI systems gain access to tools &ndash; databases, APIs, file systems, deployment pipelines &ndash; the blast radius of a successful injection grows dramatically. A chatbot that can only generate text is annoying when compromised. A chatbot that can query your database and call your APIs is dangerous.</p>
<h2 id="defenses-that-actually-work">Defenses that actually work</h2>
<p>I apply the same layered defense approach I learned in the national cyber-defense context, adapted for AI systems.</p>
<h3 id="separate-trusted-from-untrusted">Separate trusted from untrusted</h3>
<p>The most important architectural decision is maintaining a clear hierarchy of instructions. System prompts are trusted. User input is untrusted. Retrieved content is untrusted. Tool outputs are semi-trusted. The model should have explicit markers for these boundaries, and the system should be designed so that untrusted content can&rsquo;t override trusted instructions.</p>
<p>This doesn&rsquo;t fully prevent injection, but it raises the bar. Label everything. Normalize inputs. Strip or escape known injection patterns before they enter the context.</p>
<h3 id="constrain-tool-access">Constrain tool access</h3>
<p>Every tool an AI system can access should follow  <a href="/blog/2021-08-23-zero-trust-architecture/"
   
   >least privilege</a>
. Read-only by default. Write operations require explicit confirmation. Destructive operations require human approval. Scope queries to the current user&rsquo;s data. Rate limit everything.</p>
<p>Our  <a href="/blog/2025-03-17-mcp-model-context-protocol/"
   
   >MCP tool servers</a>
 enforce permission checks at the tool level, not just at the connection level. A user might be allowed to query their own deployment status but not trigger a rollback. The model never gets to make that decision &ndash; the permission boundary does.</p>
<h3 id="filter-outputs-aggressively">Filter outputs aggressively</h3>
<p>Output filtering is your last line of defense. Check every response for:</p>
<ul>
<li>System prompt fragments or internal instructions</li>
<li>Personally identifiable information that shouldn&rsquo;t appear</li>
<li>Known attack patterns (encoded instructions, suspicious URLs)</li>
<li>Content that violates your safety policies</li>
</ul>
<p>This isn&rsquo;t foolproof. Models are remarkably good at paraphrasing things they shouldn&rsquo;t say. But filtering catches the low-hanging fruit and raises the cost of attack.</p>
<h3 id="monitor-for-the-weird">Monitor for the weird</h3>
<p>Traditional security monitoring looks for known attack patterns. AI security monitoring also needs to detect behavioral anomalies:</p>
<ul>
<li>Sudden changes in tool call patterns</li>
<li>Requests that are unusually long or contain encoded content</li>
<li>Responses that include fragments of system prompts</li>
<li>Spikes in refusal rates or cost</li>
<li>Users who systematically probe the model&rsquo;s boundaries</li>
</ul>
<p>On one project, we caught an attacker by noticing a user who submitted 200 requests in an hour, each slightly different, all testing variations of the same injection technique. Traditional rate limiting didn&rsquo;t flag it because the request volume was below the threshold. Behavioral analysis did.</p>
<h2 id="the-architecture-matters-more-than-the-detection">The architecture matters more than the detection</h2>
<p>Here&rsquo;s the uncomfortable truth: you can&rsquo;t fully prevent prompt injection with current techniques. The model is a general-purpose text processor that follows instructions, and there&rsquo;s no reliable way to make it distinguish between legitimate instructions and injected ones.</p>
<p>What you can do is limit the blast radius. Isolate AI services from core systems. Scope permissions narrowly. Put human approval gates on sensitive actions. Log everything. Make the system auditable.</p>
<p>This is the same defense-in-depth approach we apply to every exposed system. The fact that the attack vector is natural language instead of SQL or shellcode doesn&rsquo;t change the principles. It changes the surface.</p>
<h2 id="what-i-tell-every-team">What I tell every team</h2>
<p>Security, stability, performance &ndash; in that order. That&rsquo;s my priority stack for AI systems, same as any other system I build.</p>
<p>Start by assuming the model will be tricked. Design your system so that a successful trick does as little damage as possible. Then add detection. Then add  <a href="/blog/2019-07-15-security-incident-response/"
   
   >response playbooks</a>
. Then drill them.</p>
<p>The teams that treat their AI systems like exposed APIs with real blast radius will be fine. The teams that treat them like internal tools with trusted inputs will learn an expensive lesson. I&rsquo;d rather they learned from this post than from their first incident.</p>
]]></content:encoded></item><item><title>Testing AI Where It Actually Runs</title><link>https://lawzava.com/blog/2025-04-14-ai-testing-production/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-04-14-ai-testing-production/</guid><description>Offline evals are necessary but not sufficient. Here&amp;amp;rsquo;s how I test AI features in production with shadow mode, canaries, and rollback automation &amp;amp;ndash; with Go code.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your eval suite passes. Your staging environment looks good. Your AI feature will still break in production because real users do things your test set never imagined. Shadow it, canary it, measure it, and make every rollout reversible. Evidence before confidence.</p>
<hr>
<p>I wrote about  <a href="/blog/2019-06-03-testing-in-production/"
   
   >testing in production</a>
 back in 2019. The core thesis hasn&rsquo;t changed: staging lies to you. What has changed is that AI makes the lying worse.</p>
<p>Traditional software either works or it doesn&rsquo;t. The test passes or fails. The API returns the right data or throws an error. AI features exist in a gray zone where the output is almost always plausible, sometimes correct, and occasionally dangerous. Your test suite can&rsquo;t cover this space. Production can.</p>
<h2 id="why-offline-evals-arent-enough">Why offline evals aren&rsquo;t enough</h2>
<p>Every AI project should have an  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >eval suite</a>
. I&rsquo;ve been saying this for over a year. But evals test known scenarios. Production surfaces the unknown ones.</p>
<p>Real users send inputs your test set never imagined. They misspell things. They paste in multi-language text. They include personally identifiable information that triggers different model behavior. They ask questions that are ambiguous in ways your eval prompts aren&rsquo;t.</p>
<p>At one company, their AI support agent passed every eval with flying colors. In production, users started treating it like a search engine &ndash; pasting in order numbers and expecting it to look up status. The model happily hallucinated order details instead of saying &ldquo;I can&rsquo;t do that.&rdquo; The eval suite had no test case for &ldquo;user treats chatbot like a database query tool.&rdquo; Production found it in the first hour.</p>
<h2 id="shadow-mode-first">Shadow mode first</h2>
<p>Before any AI change touches a real user, shadow it. Run the new version in parallel with the current one, compare outputs, and log everything. The user only sees the current version.</p>
<p>Here&rsquo;s the pattern I use in Go:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ShadowRunner</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">current</span>   <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">candidate</span> <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">logger</span>    <span style="color:#f92672">*</span><span style="color:#a6e22e">ShadowLogger</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ShadowRunner</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Current model serves the user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Candidate runs in background -- never blocks the user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">candidateCtx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#ae81ff">30</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">candidateResp</span>, <span style="color:#a6e22e">candidateErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">candidate</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">candidateCtx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">LogComparison</span>(<span style="color:#a6e22e">ShadowResult</span>{
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">RequestID</span>:      <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">ID</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CurrentOutput</span>:  <span style="color:#a6e22e">resp</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CandidateOutput</span>: <span style="color:#a6e22e">candidateResp</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CandidateErr</span>:   <span style="color:#a6e22e">candidateErr</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Match</span>:          <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">compareOutputs</span>(<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">candidateResp</span>),
</span></span><span style="display:flex;"><span>		})
</span></span><span style="display:flex;"><span>	}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The shadow logger captures every comparison. I review divergences daily during the shadow period. If the candidate produces different outputs, I want to understand whether those differences are improvements, regressions, or neutral changes.</p>
<p>The shadow period should last at least a week. Longer for high-traffic services. The goal is to see enough real-world input diversity to have confidence in the change.</p>
<h2 id="canary-with-kill-switches">Canary with kill switches</h2>
<p>Once shadow results look good, move to a  <a href="/blog/2021-02-08-gitops-progressive-delivery/"
   
   >canary deployment</a>
. Route a small percentage of real traffic to the new version and  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >monitor closely</a>
.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CanaryRouter</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">current</span>     <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">candidate</span>   <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">percentage</span>  <span style="color:#a6e22e">atomic</span>.<span style="color:#a6e22e">Int32</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">qualityGate</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">QualityGate</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CanaryRouter</span>) <span style="color:#a6e22e">Route</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">shouldCanary</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">UserID</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">candidate</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">||</span> !<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">qualityGate</span>.<span style="color:#a6e22e">Check</span>(<span style="color:#a6e22e">resp</span>) {
</span></span><span style="display:flex;"><span>			<span style="color:#75715e">// Automatic fallback to current</span>
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CanaryRouter</span>) <span style="color:#a6e22e">shouldCanary</span>(<span style="color:#a6e22e">userID</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hash</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fnv</span>.<span style="color:#a6e22e">New32a</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#a6e22e">userID</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> int(<span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Sum32</span>()<span style="color:#f92672">%</span><span style="color:#ae81ff">100</span>) &lt; int(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">percentage</span>.<span style="color:#a6e22e">Load</span>())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>QualityGate</code> is the part most teams skip. It checks the candidate response against basic quality criteria before serving it. If the response fails the gate, the user gets the current version transparently. No harm done.</p>
<p>I start at 1%. Watch for a day. If quality signals hold, move to 5%. Then 25%. Then 100%. Each step gets at least a few hours of observation. If anything looks off at any step, roll back to the previous percentage. No drama.</p>
<p>The hash-based routing is important: the same user always gets the same version within a rollout step. This prevents confusing experiences where the same user gets different quality outputs on consecutive requests.</p>
<h2 id="what-to-measure-during-rollout">What to measure during rollout</h2>
<p>Three categories of signals, checked at every rollout step:</p>
<p><strong>Quality signals.</strong> Task success rate on your eval set. But also: user re-prompts (did they have to ask again?), abandonment rate (did they give up?), explicit negative feedback. These are the signals your eval suite can&rsquo;t give you.</p>
<p><strong>Safety signals.</strong> Refusal rate. Policy trigger count. Anything flagged by your content filters. If the candidate model refuses more or fewer requests than the current one, investigate before expanding.</p>
<p><strong>Operational signals.</strong> Latency p50 and p95 by workflow. Token usage. Cost per request. Error rates. A model change that improves quality but doubles cost might not be a net win. Make that trade-off explicit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RolloutMetrics</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Version</span>         <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">QualityScore</span>    <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">RefusalRate</span>     <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">P50Latency</span>      <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">P95Latency</span>      <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">CostPerRequest</span>  <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ErrorRate</span>       <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">UserRepromptRate</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">RolloutMetrics</span>) <span style="color:#a6e22e">PassesGate</span>(<span style="color:#a6e22e">baseline</span> <span style="color:#a6e22e">RolloutMetrics</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">QualityScore</span> &lt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">QualityScore</span><span style="color:#f92672">*</span><span style="color:#ae81ff">0.95</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// quality regression &gt; 5%</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">ErrorRate</span> &gt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">ErrorRate</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1.5</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// error rate increase &gt; 50%</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">P95Latency</span> &gt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">P95Latency</span><span style="color:#f92672">*</span><span style="color:#ae81ff">2</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// latency doubled</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>These thresholds aren&rsquo;t magic numbers. They&rsquo;re product decisions. A 5% quality regression might be acceptable if cost drops by 40%. A latency doubling might be fine for a background task but fatal for a chat interface. Define them before the rollout starts, not during.</p>
<h2 id="the-one-change-rule">The one-change rule</h2>
<p>Never change the model and the prompt at the same time. If quality drops, you won&rsquo;t know which change caused it. This sounds obvious. I&rsquo;ve watched four different teams make this mistake in the last three months.</p>
<p>Ship the prompt change. Measure. Ship the model change. Measure. If you must change both, do the prompt first because it&rsquo;s cheaper to roll back.</p>
<p>Same goes for retrieval changes, system message changes, and tool configuration changes. One variable at a time. Anything else is debugging in the dark.</p>
<h2 id="holdout-baselines">Holdout baselines</h2>
<p>Keep a small, stable slice of traffic permanently on a known-good version. This is your holdout. It tells you whether quality changes are due to your changes or due to shifts in user behavior, input distribution, or upstream data.</p>
<p>Without a holdout, slow regressions look like normal variance. You won&rsquo;t notice a 2% quality drop per week because no individual week looks bad. But your holdout will show the cumulative drift loud and clear.</p>
<h2 id="what-matters">What matters</h2>
<p>Testing AI in production isn&rsquo;t reckless. Shipping AI without testing it in production is reckless. Offline evals give you a baseline. Shadow mode gives you confidence. Canaries give you safety. Holdouts give you ground truth.</p>
<p>Every rollout should be reversible, measurable, and attributable to a single change. That isn&rsquo;t a testing philosophy. That&rsquo;s  <a href="/blog/2024-01-08-ai-engineering-discipline/"
   
   >engineering discipline</a>
 applied to a system that fails in ways your test suite can&rsquo;t anticipate.</p>
]]></content:encoded></item><item><title>Your AI System Looks Healthy. It Is Not.</title><link>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</guid><description>Traditional monitoring will tell you your AI service is up. It won&amp;amp;rsquo;t tell you it&amp;amp;rsquo;s returning confident garbage. Here&amp;amp;rsquo;s what observability actually looks like for AI.</description><content:encoded><![CDATA[<p>Here&rsquo;s a scenario I&rsquo;ve seen three times this year.</p>
<p>An AI-powered feature is in production. Uptime: 99.9%. Latency: nominal. Error rate: near zero. Dashboards are green. Everyone is happy.</p>
<p>Except the answers are wrong 15% of the time, and nobody knows because nothing is measuring answer quality. The system is healthy. The outputs are not.</p>
<p>This is the fundamental gap in  <a href="/blog/2023-08-21-llm-observability/"
   
   >AI observability</a>
.  <a href="/blog/2017-03-20-why-observability-matters-more-than-monitoring/"
   
   >Traditional monitoring</a>
 tells you whether the service is running. It does not tell you whether the service is useful.</p>
<h2 id="why-ai-systems-fail-silently">Why AI systems fail silently</h2>
<p>A classic API returns structured data. If the response is malformed, you get a parse error. If the logic is wrong, a test catches it. The failure modes are usually loud and obvious.</p>
<p>AI systems fail quietly. The model returns a perfectly formatted response with a confident tone and completely wrong content. The HTTP status is 200. The latency is fine. The JSON is valid. And the user just got told that their refund was processed when it wasn&rsquo;t.</p>
<p>At a fintech startup, we had a similar problem with our financial news summarization pipeline, long before the current AI wave. The summaries looked plausible but occasionally attributed quotes to the wrong CEO or mixed up fiscal quarters. The system was &ldquo;working&rdquo; by every operational metric. The outputs were unreliable. We caught it only because a user complained, not because monitoring flagged it.</p>
<p>The lesson stuck with me. You can&rsquo;t monitor AI like you monitor a REST API. You need different signals.</p>
<h2 id="the-signals-that-actually-matter">The signals that actually matter</h2>
<p>I use a simple framework with five categories. If you are not tracking all five, you have blind spots.</p>
<p><strong>Traceability.</strong> For every response, you need to know: which model, which prompt version, which retrieved context, which tool calls. If you can&rsquo;t reconstruct why the model said what it said, you can&rsquo;t debug a bad answer. You&rsquo;re just guessing. I store a trace object alongside every response that includes model ID, prompt hash, retrieval IDs, and tool call logs. When something goes wrong, the trace is the first thing I pull.</p>
<p><strong>Quality signals.</strong> This is the hard one. You need some measure of whether the output was good. Heuristic checks catch obvious failures: empty responses, responses that are too long or too short, and responses that contain known-bad patterns. Sampled evaluation catches the subtle failures: a human or a second model scores a random slice of outputs against a rubric. Neither is perfect. Together they cover enough ground.</p>
<p><strong>Cost per outcome.</strong> Not cost per request, cost per successful outcome. A system that gets it right on the first try costs less than one that needs three retries and a human escalation. Track the full cost of getting to a good answer, including retries, fallbacks, and human review. This number will surprise you.</p>
<p><strong>Safety and policy.</strong> Refusal rates, blocked content, policy trigger counts. If your refusal rate spikes, something changed &ndash; either the inputs or the model behavior. If it drops to zero, something might be wrong too. These are canary signals.</p>
<p><strong>Operational basics.</strong> Latency percentiles by workflow (not globally &ndash; global averages hide everything), error rates with reason codes, token usage trends. The same stuff you track for any API, but broken down by the AI-specific dimensions that matter.</p>
<h2 id="the-prompt-versioning-problem">The prompt versioning problem</h2>
<p>Here is something that bites almost every team. Someone changes a prompt. Quality drops. Nobody connects the two events because the prompt change was not tracked alongside the quality metrics.</p>
<p>Treat prompts as production code. Version them. Deploy them through your normal release process. Tag every response with the prompt version that produced it. When quality dips, the first question should be: what changed since the last known-good state?</p>
<p>I version prompts in the same repo as the service code. A prompt change gets a PR, a review, and a run against  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >the eval suite</a>
 before it hits production. It sounds like overkill until the first time it prevents a regression. Then it sounds obvious.</p>
<h2 id="keep-it-lean">Keep it lean</h2>
<p>The temptation is to build a dashboard for everything. Do not. Start with the minimum set of signals that lets you answer one question: &ldquo;A user reported a bad answer. Can I explain why it happened and prevent it from happening again?&rdquo;</p>
<p>If you can answer that question end-to-end, your observability is good enough. If you can&rsquo;t, no amount of dashboards will save you.</p>
<p>Log the trace. Track quality. Version your prompts. Measure cost per outcome, not cost per request. That&rsquo;s the baseline. Everything else is optimization.</p>
]]></content:encoded></item><item><title>MCP in Practice: Building Tool Servers in Go</title><link>https://lawzava.com/blog/2025-03-17-mcp-model-context-protocol/</link><pubDate>Mon, 17 Mar 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-03-17-mcp-model-context-protocol/</guid><description>Model Context Protocol promises to standardize how AI talks to tools. I built an MCP server in Go to see if the promise holds up. Here&amp;amp;rsquo;s what I found.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>MCP is a real protocol that solves a real problem: the N-times-M integration matrix between AI clients and tool servers. I built one in Go. The protocol layer is clean. The hard parts are still auth, permissions, and not handing the model a footgun. If you&rsquo;re building tool-heavy AI systems, MCP is worth investing in now.</p>
<hr>
<p>I&rsquo;ve been building  <a href="/blog/2024-07-08-function-calling-patterns/"
   
   >tool integrations for AI systems</a>
 since early 2024. Every project, the same pattern: custom connector, custom auth wrapper, custom request/response format, custom error handling. Multiply that by every tool and every AI provider and you get an integration matrix that grows quadratically. It&rsquo;s the microservices API sprawl problem all over again.</p>
<p>MCP &ndash; Model Context Protocol &ndash; is Anthropic&rsquo;s answer: a standard protocol for connecting AI models to external tools and data sources. Instead of N clients times M tools worth of custom integrations, you get N clients and M servers all speaking the same language.</p>
<p>I spent the last few weeks building an MCP server in Go to see whether the protocol lives up to the pitch. Here&rsquo;s what stood out.</p>
<h2 id="what-mcp-actually-is">What MCP actually is</h2>
<p>Strip away the marketing and MCP is a JSON-RPC-based protocol with three core concepts:</p>
<p><strong>Tools.</strong> Functions the model can call. Each tool has a name, a description, and a JSON Schema for its inputs. The model decides when to call a tool based on the description.</p>
<p><strong>Resources.</strong> Data the model can read. Think files, database records, API responses. Resources have URIs and can be listed or read by the client.</p>
<p><strong>Prompts.</strong> Reusable prompt templates that servers can expose. Less interesting for most production use cases, but useful for standardizing common interactions.</p>
<p>The transport layer is deliberately simple: stdio for local servers, HTTP with SSE for remote ones. The protocol handles capability negotiation, so a client can discover what a server offers at connection time.</p>
<h2 id="building-an-mcp-server-in-go">Building an MCP server in Go</h2>
<p>Here&rsquo;s a minimal MCP tool server that wraps a database query. This is roughly what I built for an internal tool in a recent project that lets the AI assistant query deployment status.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;log&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;github.com/mark3labs/mcp-go/mcp&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;github.com/mark3labs/mcp-go/server&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">DeploymentStatus</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Service</span>     <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;service&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Version</span>     <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;version&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Environment</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;environment&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Status</span>      <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;status&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">DeployedAt</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;deployed_at&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">NewMCPServer</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;deployment-status&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;1.0.0&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">WithToolCapabilities</span>(<span style="color:#66d9ef">true</span>),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">tool</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewTool</span>(<span style="color:#e6db74">&#34;get_deployment_status&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">WithDescription</span>(<span style="color:#e6db74">&#34;Get the current deployment status for a service in a given environment&#34;</span>),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">WithString</span>(<span style="color:#e6db74">&#34;service&#34;</span>, <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">Required</span>(), <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">Description</span>(<span style="color:#e6db74">&#34;Service name&#34;</span>)),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">WithString</span>(<span style="color:#e6db74">&#34;environment&#34;</span>, <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">Required</span>(), <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">Description</span>(<span style="color:#e6db74">&#34;Target environment: staging or production&#34;</span>)),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">AddTool</span>(<span style="color:#a6e22e">tool</span>, <span style="color:#a6e22e">handleGetDeploymentStatus</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">ServeStdio</span>(<span style="color:#a6e22e">s</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;server failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleGetDeploymentStatus</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolResult</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">service</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Params</span>.<span style="color:#a6e22e">Arguments</span>[<span style="color:#e6db74">&#34;service&#34;</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">env</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Params</span>.<span style="color:#a6e22e">Arguments</span>[<span style="color:#e6db74">&#34;environment&#34;</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">env</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;staging&#34;</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">env</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;production&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultError</span>(<span style="color:#e6db74">&#34;environment must be &#39;staging&#39; or &#39;production&#39;&#34;</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">status</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">queryDeploymentDB</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">service</span>, <span style="color:#a6e22e">env</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultError</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;query failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">status</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultText</span>(string(<span style="color:#a6e22e">data</span>)), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A few things to note. The tool definition includes a JSON Schema for inputs, which means the client can validate before calling. The handler returns structured results or errors. The server handles all the JSON-RPC plumbing &ndash; capability negotiation, method routing, error formatting. You just write the handler.</p>
<p>This is roughly 50 lines of actual logic. The equivalent custom integration I had before was about 200 lines, with its own HTTP server, auth middleware, and request parsing. That reduction matters when you have 15 tools to wrap.</p>
<h2 id="adding-auth-and-permissions">Adding auth and permissions</h2>
<p>The protocol itself doesn&rsquo;t define authentication. That&rsquo;s intentional &ndash; different deployments have different auth requirements. But it means you have to solve it yourself, and this is where most teams will spend their time.</p>
<p>Here&rsquo;s the pattern I use: a middleware wrapper that checks permissions before the tool handler runs.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">PermissionChecker</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">allowedTools</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>][]<span style="color:#66d9ef">string</span> <span style="color:#75715e">// tool -&gt; allowed roles</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">pc</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">PermissionChecker</span>) <span style="color:#a6e22e">Wrap</span>(<span style="color:#a6e22e">toolName</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">handler</span> <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">ToolHandlerFunc</span>) <span style="color:#a6e22e">server</span>.<span style="color:#a6e22e">ToolHandlerFunc</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolResult</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">user</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">userFromContext</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">user</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultError</span>(<span style="color:#e6db74">&#34;authentication required&#34;</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">allowed</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pc</span>.<span style="color:#a6e22e">allowedTools</span>[<span style="color:#a6e22e">toolName</span>]
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">hasAnyRole</span>(<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">allowed</span>) {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;DENIED: user=%s tool=%s roles=%v&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">toolName</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">Roles</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultError</span>(<span style="color:#e6db74">&#34;permission denied&#34;</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;ALLOWED: user=%s tool=%s&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">toolName</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every tool call gets logged with the user identity, whether it was allowed or denied, and the arguments (redacted where necessary). This isn&rsquo;t optional. If an AI system can call tools that read your database or modify your infrastructure, you need an audit trail.</p>
<p>For remote MCP servers over HTTP, I add standard bearer token auth at the transport layer. For local stdio servers, the auth context comes from the parent process. Either way, the permission check happens at the tool level, not just at the connection level. A user might be allowed to read deployment status but not trigger a rollback.</p>
<h2 id="the-security-conversation">The security conversation</h2>
<p>This is the part that keeps me up at night. MCP makes it easy to give an AI model access to tools. Maybe too easy. The protocol doesn&rsquo;t enforce:</p>
<ul>
<li><strong>Read vs. write separation.</strong> A tool that reads data and a tool that deletes data look the same to the protocol. You have to enforce the distinction.</li>
<li><strong>Rate limiting.</strong> Nothing stops the model from calling a tool a thousand times in a loop. Build your own limits.</li>
<li><strong>Input sanitization.</strong> The model generates the tool arguments. If those arguments end up in a SQL query or a shell command, you&rsquo;re one  <a href="/blog/2023-10-30-llm-security-considerations/"
   
   >prompt injection</a>
 away from a bad day.</li>
<li><strong>Blast radius.</strong> A tool that queries one record is different from a tool that dumps an entire table. Scope your tools narrowly.</li>
</ul>
<p>I enforce a simple rule: every tool that can write or modify gets a confirmation step that goes back to the user. The model can propose the action, but a human approves it. For read-only tools, I still scope the query to the current user&rsquo;s data and add rate limits.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleTriggerRollback</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">CallToolResult</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">service</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Params</span>.<span style="color:#a6e22e">Arguments</span>[<span style="color:#e6db74">&#34;service&#34;</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">env</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Params</span>.<span style="color:#a6e22e">Arguments</span>[<span style="color:#e6db74">&#34;environment&#34;</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Never auto-execute destructive actions</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mcp</span>.<span style="color:#a6e22e">NewToolResultText</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;CONFIRMATION REQUIRED: Roll back %s in %s to previous version? &#34;</span><span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>			<span style="color:#e6db74">&#34;This action requires human approval.&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">service</span>, <span style="color:#a6e22e">env</span>,
</span></span><span style="display:flex;"><span>	)), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is the same principle from my national cyber-defense days:  <a href="/blog/2021-08-23-zero-trust-architecture/"
   
   >least privilege, explicit authorization, and comprehensive auditing</a>
. The fact that the agent is an AI model doesn&rsquo;t change the security model. If anything, it makes it more important, because the model can be manipulated through prompt injection in ways a human user can&rsquo;t.</p>
<h2 id="where-mcp-shines">Where MCP shines</h2>
<p><strong>Tool portability.</strong> I built the deployment status server once. It works with Claude, with our internal assistant, and with any future client that speaks MCP. That&rsquo;s the whole pitch, and it delivers.</p>
<p><strong>Discovery.</strong> A client can connect to a server and ask &ldquo;what can you do?&rdquo; The response is machine-readable and includes schemas. This means the AI model gets accurate tool descriptions automatically instead of relying on hardcoded prompts.</p>
<p><strong>Composability.</strong> An AI client can connect to multiple MCP servers simultaneously. One for deployments, one for monitoring, one for documentation. Each server is independently deployable and testable. This is  <a href="/blog/2016-01-15-why-microservices-arent-always-the-answer/"
   
   >the microservices pattern</a>
 applied to AI tool access, with the same benefits and the same risks.</p>
<h2 id="where-it-doesnt">Where it doesn&rsquo;t</h2>
<p><strong>No standard auth.</strong> Every deployment rolls its own. This will improve, but right now it&rsquo;s extra work.</p>
<p><strong>Ecosystem maturity.</strong> The Go ecosystem is solid thanks to <code>mcp-go</code>, but tooling for testing, debugging, and monitoring MCP interactions is still young. I wrote my own trace logger.</p>
<p><strong>Complexity budget.</strong> MCP is one more protocol layer to understand, debug, and operate. For a team with two tools, the overhead might not be worth it. For a team with ten tools across multiple AI clients, it pays for itself quickly.</p>
<h2 id="should-you-adopt-it-now">Should you adopt it now</h2>
<p>If you&rsquo;re building AI systems that call tools &ndash; and increasingly, every AI system does &ndash; start with one server. Pick your simplest, most-used tool. Wrap it in MCP. Test it against a real client. Measure the integration effort against your current custom approach.</p>
<p>From what I&rsquo;ve seen, MCP cut tool integration time roughly in half and made our tools testable in isolation for the first time. The security work is the same either way &ndash; you have to solve auth and permissions regardless of protocol. MCP just standardizes everything else.</p>
<p>The protocol is real. The ecosystem is growing. The hard problems are still hard. But the easy problems &ndash; discovery, invocation, transport &ndash; are solved. That&rsquo;s enough to make it worth building on.</p>
]]></content:encoded></item><item><title>AI Governance That Does Not Suck</title><link>https://lawzava.com/blog/2025-03-03-ai-governance-practice/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-03-03-ai-governance-practice/</guid><description>Governance that blocks delivery is broken. Governance that makes &amp;amp;lsquo;yes&amp;amp;rsquo; safe and fast is a competitive advantage. Here&amp;amp;rsquo;s how to build the second kind.</description><content:encoded><![CDATA[<p>Nearly every enterprise has an AI governance document. Most of them are useless.</p>
<p>Not because the content is wrong. Because nobody reads it. Because it was written by a committee that has never shipped an AI feature. Because it treats governance as a gate instead of a guardrail, and engineers respond to gates the way water responds to dams &ndash; they find a way around.</p>
<p>I&rsquo;ve watched teams at large telcos spend six weeks in governance review for an internal summarization tool that touches no customer data. Meanwhile, a different team ships a customer-facing chatbot with no review at all because nobody told them they were supposed to ask. That&rsquo;s what governance failure looks like: not the absence of rules, but the absence of practical, enforceable, proportional rules.</p>
<h2 id="what-governance-should-actually-do">What governance should actually do</h2>
<p>Three things. That&rsquo;s it.</p>
<ol>
<li>
<p><strong>Define what&rsquo;s allowed, with conditions.</strong> Not a blanket &ldquo;AI is approved.&rdquo; Not a blanket &ldquo;AI requires review.&rdquo; A clear mapping from risk level to requirements.</p>
</li>
<li>
<p><strong>Match oversight to risk.</strong> An internal tool that summarizes meeting notes doesn&rsquo;t need the same review as a system that makes lending decisions. If your governance process can&rsquo;t tell the difference, it&rsquo;s broken.</p>
</li>
<li>
<p><strong>Provide evidence that controls work.</strong> Not a signed-off PDF from six months ago. Living evidence: monitoring dashboards, automated checks, audit trails.</p>
</li>
</ol>
<p>Anything beyond those three outcomes is compliance theater.</p>
<h2 id="risk-tiers-are-the-whole-game">Risk tiers are the whole game</h2>
<p>The simplest model that works:</p>
<p><strong>Low risk:</strong> Internal tools, no customer data, no decisions with real consequences. Team-level approval. One-page system card. Basic monitoring. Ship it.</p>
<p><strong>Medium risk:</strong> Customer-facing features, data processing, content generation. Formal review. Testing against  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >an eval set</a>
. Documented safeguards. Scheduled re-checks.</p>
<p><strong>High risk:</strong> Systems that make decisions affecting people&rsquo;s money, health, access, or rights. Executive visibility. Human oversight. Continuous monitoring. No exceptions.</p>
<p>The tier matters less than the discipline of routing every AI deployment through the right path every time. At one company, we built a simple intake form &ndash; five questions, two minutes &ndash; that automatically assigned a risk tier and told teams exactly what they needed before shipping. Governance review time dropped from weeks to days. Compliance improved because teams actually followed the process.</p>
<h2 id="the-system-card">The system card</h2>
<p>Every AI deployment gets a one-page system card. It should answer:</p>
<ul>
<li>What is this system allowed to do? What is it explicitly not allowed to do?</li>
<li>What data does it touch and how is that data protected?</li>
<li>What safeguards exist and how are they tested?</li>
<li>Who owns this system when something goes wrong?</li>
</ul>
<p>That last question is the most important. If nobody has clear ownership, your incident response becomes a group chat full of confusion. I&rsquo;ve seen that play out too many times.</p>
<h2 id="governance-isnt-a-one-time-event">Governance isn&rsquo;t a one-time event</h2>
<p>Models change. Data drifts. Usage expands beyond the original scope. A governance review from January is stale by March. Build automated checks: version tracking, usage monitoring, and alerts when behavior changes. Treat governance the way you treat infrastructure &ndash; continuously, not ceremonially.</p>
<p>The organizations that get AI governance right will move faster than the ones that skip it. Not because rules are fun, but because clear rules eliminate the ambiguity that slows everything down.</p>
]]></content:encoded></item><item><title>Video Understanding AI: What Actually Works</title><link>https://lawzava.com/blog/2025-02-17-video-understanding-ai/</link><pubDate>Mon, 17 Feb 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-02-17-video-understanding-ai/</guid><description>I pointed a video understanding pipeline at 200 hours of meeting recordings. The results taught me more about pipeline design than about meetings.</description><content:encoded><![CDATA[<p>Last month, a team asked me to evaluate whether AI could replace their manual video review process. They had four people watching customer support call recordings, tagging issues, and writing summaries for eight hours a day. I said yes and built a prototype.</p>
<p>The prototype worked beautifully on the first three test clips. Then I ran it against their actual library and it confidently told me a customer was &ldquo;demonstrating frustration through aggressive keyboard usage.&rdquo; The customer was typing their account number. The model was hallucinating emotional context from audio artifacts.</p>
<p>That experience captures video AI right now. It&rsquo;s genuinely capable. It&rsquo;s also confidently wrong in ways that are hard to predict and even harder to catch at scale.</p>
<h2 id="video-isnt-just-lots-of-images">Video isn&rsquo;t just &ldquo;lots of images&rdquo;</h2>
<p>The fundamental challenge with video understanding is time. An image model looks at a single moment. A video model has to track what happened, in what order, and how things changed. That temporal reasoning is where models still struggle.</p>
<p>The practical failure modes I&rsquo;ve seen:</p>
<ul>
<li><strong>Temporal confusion.</strong> The model describes events out of order or merges two separate moments into one. This is especially bad with longer clips.</li>
<li><strong>Missing key moments.</strong> The model summarizes the overall vibe of a clip but misses the specific 10-second window where the important thing happened.</li>
<li><strong>Overconfidence.</strong> The model narrates with authority even when it&rsquo;s guessing. No hedging. No &ldquo;I&rsquo;m not sure.&rdquo; Just wrong with conviction.</li>
</ul>
<h2 id="the-pipeline-that-actually-works">The pipeline that actually works</h2>
<p>Forget single-prompt video understanding. It doesn&rsquo;t scale. What works is a pipeline that breaks the problem into stages you can debug independently.</p>
<p>Here&rsquo;s the architecture I landed on:</p>
<p><strong>Step 1: Extract audio and transcribe.</strong> If the video has spoken content, the transcript is your primary signal. Audio transcription is a solved problem, and the output is reliable. Start here.</p>
<p><strong>Step 2: Sample frames intelligently.</strong> Not every N seconds. Use scene detection to identify transitions, then sample the first frame of each scene plus any frame with significant visual change. This reduces the frame count by 60-80% without losing meaningful content.</p>
<p><strong>Step 3: Analyze frames with context.</strong> Feed each frame to a vision model along with the surrounding transcript text. The transcript grounds the visual analysis and prevents the model from inventing narratives that don&rsquo;t match what was said.</p>
<p><strong>Step 4: Synthesize with timestamps.</strong> Merge the transcript-grounded visual analysis into a structured timeline. Every claim in the summary must reference a specific timestamp. If the model can&rsquo;t cite when something happened, it probably didn&rsquo;t happen.</p>
<p>The key insight: audio-first, video-second. The transcript is your source of truth. The video adds context. Not the other way around.</p>
<h2 id="where-its-actually-useful">Where it&rsquo;s actually useful</h2>
<p>After the initial disaster and a week of pipeline tuning, I found the sweet spots:</p>
<p><strong>Meeting summaries with action items.</strong> Transcribe, extract decisions and action items, tag them with speaker and timestamp. This works well because the transcript carries most of the signal and the visual component (slides, screen shares) adds structure.</p>
<p><strong>Content moderation.</strong> Checking video against a specific policy with concrete criteria. &ldquo;Does this clip contain product logos?&rdquo; &ldquo;Is the speaker reading from a teleprompter?&rdquo; Questions with binary answers that the model can ground in visual evidence.</p>
<p><strong>Search and retrieval.</strong> &ldquo;Find the part of this recording where they discuss pricing.&rdquo; Natural language search over video libraries works surprisingly well when you have good transcripts and frame-level annotations.</p>
<p><strong>Compliance review.</strong> Structured checks against a rubric. Did the agent identify themselves? Did they read the required disclosure? Was the customer&rsquo;s consent recorded? This works because the criteria are specific and verifiable.</p>
<h2 id="where-it-isnt-ready">Where it isn&rsquo;t ready</h2>
<p>Long-form video without speech. Surveillance-style footage. Anything where the important signal is subtle body language or spatial relationships. Anything where the model needs to count reliably or track specific objects across many frames.</p>
<p>Also, anything where a false positive has real consequences. If your video review pipeline flags a customer interaction as &ldquo;hostile&rdquo; and that triggers an HR process, you had better have a human in the loop.</p>
<h2 id="starting-without-overbuilding">Starting without overbuilding</h2>
<p>Pick one use case. Keep clips under 10 minutes. Fix your output format before you start &ndash; structured JSON, not free-form prose. Build a gold set of 20-30 annotated clips and run every pipeline change against it.</p>
<p> <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >The evaluation loop</a>
 is everything. Without it, you&rsquo;re optimizing by vibes, and vibes don&rsquo;t catch temporal hallucinations.</p>
<p>Video AI is real and useful for the right problems. Just don&rsquo;t let the first impressive demo convince you it&rsquo;s ready for the hard ones.</p>
]]></content:encoded></item><item><title>AI Code Review Is Mostly Noise</title><link>https://lawzava.com/blog/2025-02-03-ai-code-review/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-02-03-ai-code-review/</guid><description>I&amp;amp;rsquo;ve been running AI code review on real PRs for months. It catches some real bugs. It also generates a staggering amount of useless commentary.</description><content:encoded><![CDATA[<p>I&rsquo;m going to say something that will annoy AI tooling vendors: most AI code review output is garbage.</p>
<p>Not all of it. Maybe 15-20% is genuinely useful. But the other 80% is vague, style-obsessed, context-free commentary that would get a human reviewer told to try harder. &ldquo;Consider adding error handling here.&rdquo; Thanks. I hadn&rsquo;t considered that. In Go. Where every third line is error handling.</p>
<p>I&rsquo;ve been running AI review on PRs across production codebases for months. I wanted it to work. I really did. A tireless reviewer that catches logic bugs and security issues while humans  <a href="/blog/2018-10-01-effective-code-reviews/"
   
   >focus on architecture and design</a>
? Sign me up. The reality is more complicated.</p>
<h2 id="what-it-actually-catches">What it actually catches</h2>
<p>When AI code review works, it works well. The wins are real:</p>
<p><strong>Logic errors on changed paths.</strong> The model is good at spotting off-by-one errors, nil pointer risks, and missing edge cases in the specific lines that changed. It caught a race condition in a  <a href="/blog/2022-08-22-golang-concurrency-patterns/"
   
   >Go channel handler</a>
 that three human reviewers missed. That alone justified the experiment.</p>
<p><strong>Security surface area.</strong> SQL injection in a new endpoint. Hardcoded credentials in a test file that was about to be committed. An overly permissive CORS config. These are pattern-matching tasks, and models are decent at pattern matching.</p>
<p><strong>Copy-paste bugs.</strong> Someone copies a function, changes three of four parameters, and forgets the fourth. The model catches this reliably. Humans miss it because we read what we expect to see.</p>
<h2 id="where-it-falls-apart">Where it falls apart</h2>
<p><strong>Business context.</strong> The model doesn&rsquo;t know why your checkout flow has that weird retry logic. It doesn&rsquo;t know that the &ldquo;redundant&rdquo; nil check exists because a specific vendor API lies about its response types. It doesn&rsquo;t know your system&rsquo;s history. So it flags things that aren&rsquo;t problems and misses things that are.</p>
<p><strong>Large diffs.</strong> Anything over a few hundred lines and the model loses the thread. It starts making generic observations instead of specific findings. &ldquo;This function is complex and could benefit from refactoring.&rdquo; Really helpful on a 2,000-line migration PR.</p>
<p><strong>Style opinions nobody asked for.</strong> &ldquo;Consider using a more descriptive variable name.&rdquo; &ldquo;This comment could be more detailed.&rdquo; &ldquo;Consider extracting this into a separate function.&rdquo; If I wanted a style cop, I&rsquo;d configure a linter. AI review should find bugs, not police style.</p>
<h2 id="how-i-actually-use-it">How I actually use it</h2>
<p>After months of tuning, here&rsquo;s what works.</p>
<p><strong>Scope it to the diff.</strong> Don&rsquo;t let the model browse the entire repo. Give it the changed lines and maybe the immediate surrounding context. The more you feed it, the more generic the output gets.</p>
<p><strong>Demand specifics.</strong> My review prompt is aggressive about this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Review this diff. For each finding:
</span></span><span style="display:flex;"><span>- Exact line number
</span></span><span style="display:flex;"><span>- Severity: critical / warning / info
</span></span><span style="display:flex;"><span>- What could fail at runtime
</span></span><span style="display:flex;"><span>- A concrete fix
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Skip style suggestions. Skip anything a linter would catch.
</span></span><span style="display:flex;"><span>If nothing is wrong, say nothing.
</span></span></code></pre></div><p>That last line matters. Without it, the model will always find something to say because it&rsquo;s trained to be helpful. Sometimes the most helpful thing is silence.</p>
<p><strong>Track the hit rate.</strong> I log every AI review comment and whether the human reviewer accepted, dismissed, or ignored it. Our current acceptance rate is about 22%. That means 78% of AI review output is noise. Not great. But the 22% that lands includes some of the highest-severity findings in our review history.</p>
<p><strong>Never gate merges on it.</strong> AI review is advisory. A comment. A suggestion. The human reviewer decides. The moment you make AI review a merge blocker, you&rsquo;ve handed authority to a system that&rsquo;s wrong four times out of five. Don&rsquo;t do this.</p>
<h2 id="the-uncomfortable-math">The uncomfortable math</h2>
<p>AI code review costs money. Token costs, API calls, latency in your CI pipeline. At our current volume, it adds about 15-30 seconds per PR and a few dollars per day. That&rsquo;s cheap for the bugs it catches. But if you aren&rsquo;t measuring hit rate, you have no idea whether it&rsquo;s worth it.</p>
<p>Most teams set up AI review, get excited about the first few catches, and then never look at the numbers again. Six months later, developers have learned to ignore the comments entirely because most of them are noise. The tool becomes furniture.</p>
<h2 id="what-i-actually-want">What I actually want</h2>
<p>I want AI code review that knows when to shut up. That understands the system well enough to distinguish a real bug from an intentional design choice. That can read a PR description and connect the changes to the stated intent.</p>
<p>We aren&rsquo;t there yet. But the foundation is real. Scope it tight, demand specifics, measure ruthlessly, and never trust it to make decisions. It&rsquo;s a second pair of eyes, not a senior engineer.</p>
]]></content:encoded></item><item><title>Reasoning Models in Production: A Practical Guide</title><link>https://lawzava.com/blog/2025-01-20-reasoning-models-production/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-01-20-reasoning-models-production/</guid><description>Reasoning models are powerful but expensive and slow. Here&amp;amp;rsquo;s how I integrate them in Go services with routing, async patterns, and cost controls that actually work.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Don&rsquo;t make reasoning models your default path. Route by complexity, run expensive calls async, set per-request budgets, and  <a href="/blog/2024-03-25-prompt-caching-strategies/"
   
   >cache aggressively</a>
. The model is the easy part. The routing and cost control are where you earn your keep.</p>
<hr>
<p>I spent the last month integrating reasoning models into a production service. The short version: they&rsquo;re genuinely better at complex analysis tasks. The long version: they&rsquo;ll wreck your UX and budget if you treat them like a drop-in replacement for fast models.</p>
<p>This post covers the architecture I landed on, with real Go code. When I started this work, most posts I found were hand-wavy &ldquo;use async patterns&rdquo; advice with zero implementation detail.</p>
<h2 id="the-problem-concretely">The problem, concretely</h2>
<p>Standard LLM calls in our pipeline take 1-3 seconds. Reasoning model calls take 8-45 seconds. That&rsquo;s not a rounding error. It&rsquo;s a completely different product experience.</p>
<p>Cost scales the same way. A reasoning call can burn 10-50x the tokens of a standard call for the same input because the model does internal chain-of-thought before producing output. On a high-traffic endpoint, that adds up fast.</p>
<p>At one company, someone enabled a reasoning model as the default for their support chatbot. The monthly API bill went from $2,000 to $34,000 in three weeks. Most of those calls were &ldquo;what are your business hours?&rdquo; Not exactly a problem that requires deep reasoning.</p>
<h2 id="when-reasoning-models-actually-help">When reasoning models actually help</h2>
<p>I&rsquo;ve found three categories where the latency and cost trade-off is worth it:</p>
<p><strong>Multi-step analysis.</strong> Reviewing a contract clause, debugging a complex data pipeline, synthesizing information from multiple sources. Tasks where a wrong answer costs more than a slow answer.</p>
<p><strong>Code review and debugging.</strong> Reasoning models catch logic errors and subtle bugs that fast models miss entirely. I use them in our CI pipeline for reviewing diffs on critical paths. Nobody cares if that takes 30 seconds.</p>
<p><strong>Planning and decomposition.</strong> Breaking a complex task into subtasks, reasoning about dependencies, identifying risks. The model needs to hold a lot of context and think through implications.</p>
<p>Where they&rsquo;re a waste: simple Q&amp;A, classification, extraction, and anything high-volume or latency-sensitive. Route those to fast models and save money.</p>
<h2 id="the-routing-layer">The routing layer</h2>
<p>The core insight is simple:  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >not every request deserves the same model</a>
. Here&rsquo;s the router I use in Go:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ComplexityLevel</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ComplexityLow</span> <span style="color:#a6e22e">ComplexityLevel</span> = <span style="color:#66d9ef">iota</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ComplexityMedium</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ComplexityHigh</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Router</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fastModel</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">reasoningModel</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">classifier</span>     <span style="color:#f92672">*</span><span style="color:#a6e22e">ComplexityClassifier</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Router</span>) <span style="color:#a6e22e">Route</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">level</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">classifier</span>.<span style="color:#a6e22e">Assess</span>(<span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">level</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ComplexityLow</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">fastModel</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">defaultBudget</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ComplexityMedium</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">fastModel</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">defaultBudget</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Confidence</span> &lt; <span style="color:#ae81ff">0.7</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">reasoningModel</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">premiumBudget</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ComplexityHigh</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">reasoningModel</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">premiumBudget</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">fastModel</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">defaultBudget</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The complexity classifier doesn&rsquo;t need to be fancy. Ours uses a combination of input length, certain keywords (like &ldquo;analyze&rdquo;, &ldquo;compare&rdquo;, &ldquo;debug&rdquo;), and whether the request references multiple documents. A simple heuristic gets you 80% of the way there.</p>
<p>The medium-complexity path is where this gets interesting. Try the fast model first. If confidence is low, escalate to reasoning. This keeps costs down for tasks that turn out to be simpler than they look.</p>
<h2 id="async-execution-for-expensive-calls">Async execution for expensive calls</h2>
<p>Any reasoning model call that might take more than a few seconds shouldn&rsquo;t block your HTTP handler. Here&rsquo;s the pattern I use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Job</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ID</span>        <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Status</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Request</span>   <span style="color:#a6e22e">Request</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Response</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">AsyncExecutor</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jobs</span>   <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Map</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">router</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Router</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">notify</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">jobID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">resp</span> <span style="color:#a6e22e">Response</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">e</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">AsyncExecutor</span>) <span style="color:#a6e22e">Submit</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Job</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ID</span>:        <span style="color:#a6e22e">generateID</span>(),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Status</span>:    <span style="color:#e6db74">&#34;pending&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Request</span>:   <span style="color:#a6e22e">req</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">CreatedAt</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">jobs</span>.<span style="color:#a6e22e">Store</span>(<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">job</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">Route</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Status</span> = <span style="color:#e6db74">&#34;failed&#34;</span>
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Response</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">resp</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">Status</span> = <span style="color:#e6db74">&#34;completed&#34;</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">notify</span>(<span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">resp</span>)
</span></span><span style="display:flex;"><span>	}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">job</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">e</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">AsyncExecutor</span>) <span style="color:#a6e22e">Poll</span>(<span style="color:#a6e22e">jobID</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>, <span style="color:#66d9ef">bool</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">jobs</span>.<span style="color:#a6e22e">Load</span>(<span style="color:#a6e22e">jobID</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>.(<span style="color:#f92672">*</span><span style="color:#a6e22e">Job</span>), <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The caller gets a job ID back immediately. They can poll for status, or we can push a notification when it&rsquo;s done. The UX team shows a &ldquo;thinking deeply about this&hellip;&rdquo; indicator. Users are surprisingly tolerant of waiting when you tell them why.</p>
<p>In production, you want a proper job queue (we use Redis) and persistence. But the pattern is the same.</p>
<h2 id="per-request-cost-budgets">Per-request cost budgets</h2>
<p>This is the piece most teams skip, and it&rsquo;s what prevents surprise bills. Every model call gets a token budget:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Budget</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxInputTokens</span>  <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxOutputTokens</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxCostCents</span>    <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">TimeoutSeconds</span>  <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">defaultBudget</span> = <span style="color:#a6e22e">Budget</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxInputTokens</span>:  <span style="color:#ae81ff">4000</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxOutputTokens</span>: <span style="color:#ae81ff">1000</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxCostCents</span>:    <span style="color:#ae81ff">5</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">TimeoutSeconds</span>:  <span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">premiumBudget</span> = <span style="color:#a6e22e">Budget</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxInputTokens</span>:  <span style="color:#ae81ff">16000</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxOutputTokens</span>: <span style="color:#ae81ff">4000</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">MaxCostCents</span>:    <span style="color:#ae81ff">50</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">TimeoutSeconds</span>:  <span style="color:#ae81ff">60</span>,
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Router</span>) <span style="color:#a6e22e">callModel</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">model</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>, <span style="color:#a6e22e">budget</span> <span style="color:#a6e22e">Budget</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">TimeoutSeconds</span>)<span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">EstimatedInputTokens</span>() &gt; <span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">MaxInputTokens</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;input exceeds budget: %d &gt; %d tokens&#34;</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">EstimatedInputTokens</span>(), <span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">MaxInputTokens</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">model</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">ToPrompt</span>(),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">WithMaxTokens</span>(<span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">MaxOutputTokens</span>),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Response</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;model call failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">costCents</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">estimateCost</span>(<span style="color:#a6e22e">model</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Usage</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">costCents</span> &gt; <span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">MaxCostCents</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;WARN: call exceeded cost budget: %d &gt; %d cents&#34;</span>, <span style="color:#a6e22e">costCents</span>, <span style="color:#a6e22e">budget</span>.<span style="color:#a6e22e">MaxCostCents</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">parseResponse</span>(<span style="color:#a6e22e">resp</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The budget is enforced before and during the call. Context timeouts prevent runaway reasoning. Token limits prevent ballooning inputs. Cost estimation after the call feeds monitoring and alerting.</p>
<p>At one company, we added a daily cost ceiling per endpoint. If the endpoint hits 80% of its daily budget by noon, it automatically downgrades all calls to the fast model for the rest of the day. Crude but effective.</p>
<h2 id="caching-reasoning-results">Caching reasoning results</h2>
<p>Reasoning model outputs are expensive to produce but often reusable. Same contract clause reviewed twice? Same code pattern analyzed in different PRs? Cache it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ResultCache</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">store</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">redis</span>.<span style="color:#a6e22e">Client</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ttl</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ResultCache</span>) <span style="color:#a6e22e">GetOrCompute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">compute</span> <span style="color:#66d9ef">func</span>() (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>)) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">cached</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>).<span style="color:#a6e22e">Result</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">resp</span> <span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">cached</span>), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">resp</span>) <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">FromCache</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">compute</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">resp</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ttl</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The cache key is a hash of the input and model version. When the model changes, the cache invalidates naturally. We use a 24-hour TTL for most analysis tasks and a 1-hour TTL for anything time-sensitive.</p>
<p>This alone cut our reasoning model costs by about 40% on the code review pipeline, because many PRs touch similar patterns.</p>
<h2 id="what-i-got-wrong-the-first-time">What I got wrong the first time</h2>
<p>I initially tried to hide latency entirely. Bad idea. Users thought the system was broken. The moment we switched to explicit &ldquo;this needs deeper analysis, checking now&hellip;&rdquo; messaging, complaints dropped to zero. People understand that some questions take longer to answer well. Respect that.</p>
<p>I also over-routed to reasoning models early on. The classifier was too generous with &ldquo;high complexity&rdquo; ratings. We added a feedback loop: if a reasoning model call produces essentially the same output as a fast model would have (measured by comparing on a sample), downgrade the classification for that pattern. Within two weeks, our routing accuracy improved significantly.</p>
<h2 id="the-architecture-summarized">The architecture, summarized</h2>
<pre tabindex="0"><code>Request → Complexity Classifier → Router
                                    ├── Low → Fast Model (sync)
                                    ├── Medium → Fast Model → check confidence → maybe Reasoning Model
                                    └── High → Async Executor → Reasoning Model → Notify

All paths → Budget Enforcement → Cache Check → Model Call → Response
</code></pre><p>Treat reasoning models as a premium tier. Route intelligently. Execute async when latency matters. Budget every call. Cache reusable results. The model does the thinking. Your job is to make sure it only thinks when it needs to.</p>
]]></content:encoded></item><item><title>AI in 2025: The Year Discipline Wins</title><link>https://lawzava.com/blog/2025-01-06-ai-trends-2025/</link><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-01-06-ai-trends-2025/</guid><description>The AI hype cycle is over. 2025 is about the teams who can make this stuff actually work in production &amp;amp;ndash; repeatably, measurably, and without burning money.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop chasing model announcements. The teams that win in 2025 are the ones building evals, monitoring quality, and treating AI like infrastructure instead of magic. Discipline over heroics.</p>
<hr>
<p>Every January, someone publishes a breathless AI predictions post. &ldquo;This will be the year of AGI.&rdquo; &ldquo;Agents will replace developers.&rdquo; &ldquo;Multimodal everything.&rdquo;</p>
<p>I&rsquo;m not going to do that.</p>
<p>What I can tell you is what I see working with teams that are actually shipping AI to production. The pattern is clear: 2024 was the year everyone built demos. 2025 is the year those demos have to work.</p>
<h2 id="the-demo-hangover">The demo hangover</h2>
<p>Here&rsquo;s what happened to most AI projects last year. Someone built a prototype in a weekend. It was impressive. Leadership got excited. Budget appeared. Then the prototype hit real users, real data, and real edge cases, and everything got complicated.</p>
<p>I watched this play out at three different companies. Same story every time. The model was fine. The engineering around the model wasn&rsquo;t.</p>
<p>Missing evaluation suites. No fallback paths. Prompts that drifted every time someone tweaked them. Cost tracking that amounted to &ldquo;we&rsquo;ll figure it out later.&rdquo; The model was the easy part. Operating discipline was the hard part.</p>
<p>That&rsquo;s the real trend for 2025. Not a new model. A new level of engineering rigor around models.</p>
<h2 id="reasoning-gets-interesting">Reasoning gets interesting</h2>
<p>Models that think before they answer are genuinely useful for a specific class of problems. Multi-step analysis. Code review. Debugging. Anything where you would rather wait 30 seconds for a correct answer than get a fast wrong one.</p>
<p>The trap is treating reasoning models as the default. They&rsquo;re slower, more expensive, and overkill for 80% of requests. The smart move is routing: fast model for simple tasks, reasoning model for complex ones. I&rsquo;ll write more about this in a couple of weeks.</p>
<h2 id="multimodal-is-real-but-boring">Multimodal is real but boring</h2>
<p>Image, audio, and text working together is no longer a research demo. It&rsquo;s a feature. Internal tools are the clearest win &ndash; think document-processing pipelines that can read scanned forms, or support systems that understand screenshots.</p>
<p>The value isn&rsquo;t in any single modality being amazing. It&rsquo;s in combining them so the system has richer context. Boring. Useful. Exactly the kind of thing that makes money.</p>
<h2 id="evaluation-first-development">Evaluation-first development</h2>
<p>The single biggest shift I keep pushing is simple:  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >define success before you write the first prompt</a>
.</p>
<p>This sounds obvious. Almost nobody does it. Teams will spend weeks tuning prompts and then measure success by vibes. &ldquo;It feels better.&rdquo; &ldquo;The CEO liked the demo.&rdquo; That isn&rsquo;t engineering. That&rsquo;s hope.</p>
<p>What works: a fixed eval set, tested on every change, with clear pass/fail criteria. Treat prompts like code. Version them. Review them. Test them. I won&rsquo;t ship a prompt change without running it against the eval suite. Period.</p>
<h2 id="governance-stops-being-optional">Governance stops being optional</h2>
<p>Regulation is firming up. The EU AI Act is real. Enterprise clients are asking for audit trails, documentation, and risk tiers before they&rsquo;ll sign contracts. If your AI system can&rsquo;t explain what it does, what data it touches, and who&rsquo;s responsible when it goes wrong, you&rsquo;re in for a bad year.</p>
<p>This isn&rsquo;t bureaucracy for its own sake. Good governance actually accelerates adoption because it turns &ldquo;can we use AI for this?&rdquo; from a six-week debate into a checklist. Risk tier low? Ship it. Risk tier high? Here&rsquo;s exactly what you need before you ship.</p>
<p>Governance that blocks delivery is broken governance. Governance that makes yes safe and fast is a competitive advantage.</p>
<h2 id="agents-promising-overhyped">Agents: promising, overhyped</h2>
<p>Agents that can execute multi-step tasks are improving fast. They&rsquo;re also still brittle. Context changes break them. Domain boundaries confuse them. The failure modes are subtle and hard to detect.</p>
<p>The near-term play is constrained agents with explicit checkpoints. Not open-ended autonomy. Not &ldquo;let the agent figure it out.&rdquo; Clear scope, clear permissions, clear rollback. We learned this lesson with microservices a decade ago: autonomy without contracts is chaos.</p>
<h2 id="what-im-ignoring">What I&rsquo;m ignoring</h2>
<ul>
<li>Any roadmap built on vendor keynote slides instead of product outcomes.</li>
<li>Prompt engineering tricks that can&rsquo;t be tested, versioned, or reproduced.</li>
<li>&ldquo;Autonomous&rdquo; systems with no permission model, no audit trail, and no kill switch.</li>
<li>Anyone who says &ldquo;just add AI&rdquo; without specifying what success looks like.</li>
</ul>
<h2 id="what-matters">What matters</h2>
<p>The capabilities are real. The models will keep getting better. But the gap between &ldquo;this works in a demo&rdquo; and &ldquo;this works in production at 3am on a Saturday&rdquo; is where careers and companies are made.</p>
<p>Ruthless focus on the boring stuff. Evals. Monitoring. Cost tracking. Fallback paths. Governance. That&rsquo;s the 2025 playbook.</p>
<p>The teams that  <a href="/blog/2024-12-09-ai-infrastructure-scale/"
   
   >treat AI like infrastructure</a>
 &ndash; with the same rigor they bring to databases and deployment pipelines &ndash; will win. Everyone else will keep rebuilding demos.</p>
]]></content:encoded></item><item><title>2025 Will Reward the Boring Teams</title><link>https://lawzava.com/blog/2024-12-23-preparing-for-2025/</link><pubDate>Mon, 23 Dec 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-12-23-preparing-for-2025/</guid><description>The AI advantage in 2025 goes to teams that ship measurable workflows, not teams that chase capabilities. The gap is discipline, not technology.</description><content:encoded><![CDATA[<p>The prediction game is easy. Models get better. Context windows get longer. Multimodal improves. Agents get more capable. Legal and compliance teams get more involved. None of this is surprising.</p>
<p>The harder question: what should you actually do differently?</p>
<p>Here’s my short answer, based on a year of working on AI across multiple organizations and watching the gap between teams that shipped and teams that stalled.</p>
<h2 id="stop-experimenting-start-measuring">Stop Experimenting. Start Measuring.</h2>
<p>If you&rsquo;ve been running AI &ldquo;experiments&rdquo; for more than a quarter without a clear evaluation framework, you aren&rsquo;t experimenting. You&rsquo;re procrastinating. Experiments have hypotheses, metrics, and endpoints. Pilots have owners, success criteria, and deadlines.</p>
<p>Pick two or three use cases closest to production. Define success in numbers, not narratives. Build an evaluation set. Ship to real users with monitoring. Learn from data, not opinions.</p>
<p>This isn&rsquo;t glamorous. It&rsquo;s effective.</p>
<h2 id="build-the-operational-foundation">Build the Operational Foundation</h2>
<p>The teams that will move fastest in 2025 are the ones building the plumbing now. Not new models. Not new frameworks. Plumbing.</p>
<ul>
<li>An evaluation loop that runs regularly, not when someone remembers</li>
<li>Cost tracking with per-feature attribution so you know where money goes</li>
<li>Security controls for model access and data handling that satisfy your legal team</li>
<li>Model-agnostic interfaces so you can swap providers without rewriting your stack</li>
</ul>
<p>Every one of these is boring. Every one of these is a prerequisite for scaling anything in 2025. Through Q4, I&rsquo;ve been helping teams set up exactly this kind of infrastructure, and the teams that have it in place are already iterating faster than teams that built flashy demos without it.</p>
<h2 id="governance-isnt-the-enemy">Governance Isn&rsquo;t the Enemy</h2>
<p>AI governance has a reputation problem. Engineers hear &ldquo;governance&rdquo; and think &ldquo;bureaucracy that slows us down.&rdquo; That framing is wrong.</p>
<p>Lightweight governance &ndash; clear ownership for use case intake, a simple review path for legal and security risks, a cadence for measuring value and retiring weak experiments &ndash; actually accelerates shipping. It removes the ambiguity that causes teams to stall waiting for implicit approval.</p>
<p>The companies that move fastest all have some version of this. Not a committee. Not a 50-page policy document. A clear owner, a simple process, and a regular review. That&rsquo;s it.</p>
<h2 id="what-im-betting-on">What I&rsquo;m Betting On</h2>
<p>Personally, I&rsquo;m betting that 2025 is the year AI stops being a separate initiative and becomes part of how software gets built. Not a team. Not a project. A capability that lives inside existing workflows, owned by existing teams, measured by existing standards.</p>
<p>The companies that treat AI as special will keep producing expensive demos. The companies that treat it as normal &ndash; same code review, same evaluation, same cost accountability, same ownership &ndash; will ship things that last.</p>
<p>Discipline over heroics. Same as always.</p>
]]></content:encoded></item><item><title>2024: The Year AI Got Boring (In a Good Way)</title><link>https://lawzava.com/blog/2024-12-16-year-in-review-2024/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-12-16-year-in-review-2024/</guid><description>2024 was the year AI stopped being exciting and started being useful. The demo phase ended. The production phase began. Discipline won.</description><content:encoded><![CDATA[<p>Looking back at 2024, the word that keeps coming to mind is &ldquo;normalization.&rdquo; AI stopped being the shiny thing leadership wanted to announce and became the thing teams had to maintain. That shift changed everything about how I spent my year.</p>
<h2 id="the-work">The Work</h2>
<p>Most of my 2024 was hands-on. Telecom, food delivery, real-time communications, fintech &ndash; different industries and scales, but the same fundamental questions. How do we go from demo to production? How do we control costs? How do we measure whether this actually works?</p>
<p>The conversations changed dramatically between January and December. Early in the year, the question was what AI could do. By mid-year, it was what AI should do &ndash; which tasks justified the cost, the complexity, and the risk. By Q4, the conversations were about operations: monitoring, evaluation cadence, cost attribution, team structure.</p>
<p>That progression felt right, like an industry growing up.</p>
<h2 id="what-held-up">What Held Up</h2>
<p>A few things I believed in January that held up through December:</p>
<p><strong>Narrow scope wins.</strong> Every successful deployment I saw this year started with a tightly scoped use case. &ldquo;Classify these support tickets into five categories&rdquo; beats &ldquo;build an AI assistant for customer service&rdquo; every time. The narrow scope forces clear success criteria, which forces real evaluation, which forces real accountability.</p>
<p><strong>Evaluation is the product.</strong> Teams that built evaluation harnesses early shipped faster and with more confidence. Teams that skipped evaluation shipped demos that never became products. I&rsquo;ll keep saying it.</p>
<p><strong>Retrieval quality determines answer quality.</strong> I built multiple RAG systems this year. In every single case, the initial complaint was &ldquo;the model hallucinates&rdquo; and the actual fix was improving retrieval. Better chunking. Hybrid search. Reranking. The model was fine. The evidence was bad.</p>
<p><strong>Cost control is a day-one concern.</strong> I watched one team&rsquo;s AI bill go from manageable to alarming in six weeks because nobody was tracking per-feature attribution. By the time they noticed, the organizational habit of ignoring cost was already baked in. Much harder to fix after the fact.</p>
<h2 id="what-surprised-me">What Surprised Me</h2>
<p><strong>Claude 3.5 Sonnet changed my default recommendation.</strong> For most of the year I was recommending different models for different tasks with complex routing logic. By late 2024, Claude 3.5 Sonnet had become my default &ldquo;just start here&rdquo; answer for a wide range of production tasks. The quality-to-cost ratio was hard to beat. I still recommend routing for cost optimization, but the bar for when routing matters got higher.</p>
<p><strong>Open models got good enough to matter.</strong> Llama 3 and Mistral variants crossed a threshold this year. Not for everything &ndash; frontier tasks still need frontier models. But for classification, extraction, and structured output, open models running on modest hardware became a real option. I helped two teams set up self-hosted deployments where the economics made sense.</p>
<p><strong>Teams overbuilt.</strong> This one surprised me less than it should have. Multiple teams built multi-agent orchestration systems for tasks that should have been a single prompt with a good system message. The complexity wasn&rsquo;t justified by the task. It was justified by enthusiasm. I spent a fair amount of Q3 and Q4 helping teams simplify.</p>
<h2 id="what-stayed-hard">What Stayed Hard</h2>
<p>Evaluation is hard. I keep preaching it, and I keep watching teams struggle with it. Building a good eval set requires domain expertise, clear criteria, and the willingness to maintain it over time. Most teams get the first version right, then let it rot. Evaluation sets need the same care as test suites.</p>
<p>Multi-step workflows remained fragile. Agents that need to plan, execute, observe, and adapt are architecturally interesting and operationally painful. The tooling improved this year but the fundamental challenge &ndash; maintaining coherence over many steps &ndash; is still unsolved. The teams that succeeded constrained the number of steps aggressively.</p>
<p>Hiring remained weird. The &ldquo;AI engineer&rdquo; role is still not well-defined. Every company means something different by it. The best hires I saw were strong software engineers who learned the AI-specific parts on the job, not ML researchers who struggled with production engineering.</p>
<h2 id="the-personal-angle">The Personal Angle</h2>
<p>I&rsquo;m still contributing to Go. Still building tools. The work is rewarding but I miss building full-time sometimes. There&rsquo;s a different satisfaction in shipping code versus reviewing architecture diagrams.</p>
<p>The problem space &ndash; helping teams build faster and ship reliably &ndash; feels increasingly important as AI lowers the barrier to starting projects but does nothing to lower the barrier to finishing them. Starting is easy. Shipping is hard. That gap is where I keep ending up.</p>
<h2 id="the-takeaway">The Takeaway</h2>
<p>2024 was the year AI got boring. I mean that as the highest compliment. Boring means production-ready. Boring means maintainable. Boring means teams can build on top of it without wondering if the foundation will shift next month.</p>
<p>The demo phase is over. The real work is underway. And the teams that win from here are the ones that treat AI for what it is: another production system that needs discipline, measurement, and ownership.</p>
<p>Same as everything else.</p>
]]></content:encoded></item><item><title>Your AI Infrastructure Is Not Special</title><link>https://lawzava.com/blog/2024-12-09-ai-infrastructure-scale/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-12-09-ai-infrastructure-scale/</guid><description>AI infrastructure at scale is just infrastructure. The same boring patterns &amp;amp;ndash; gateways, caching, circuit breakers, budgets &amp;amp;ndash; solve the same boring problems.</description><content:encoded><![CDATA[<p>I&rsquo;m tired of seeing AI infrastructure treated as if it needs a whole new discipline.</p>
<p>It doesn&rsquo;t. It&rsquo;s the same infrastructure engineering we&rsquo;ve been doing for decades, applied to a workload that happens to involve model inference. The latency problems are the same. The cost problems are the same. The reliability problems are the same. And the solutions are the same.</p>
<p>And yet every week I review a team&rsquo;s architecture and find they&rsquo;ve reinvented service meshes, badly, because they assumed AI needed something different.</p>
<h2 id="the-demo-to-production-gap-is-infrastructure">The Demo-to-Production Gap Is Infrastructure</h2>
<p>Here&rsquo;s what happens: a team builds a demo. It works great at one request per minute. Then real traffic arrives and everything falls apart. Latency spikes. Costs explode. The system goes down when the provider rate-limits them.</p>
<p>None of these are AI problems. They&rsquo;re infrastructure problems that we solved years ago in every other context. The teams that scale AI successfully are the ones that apply those solutions without reinventing them.</p>
<h2 id="put-a-gateway-in-front-please">Put a Gateway in Front. Please.</h2>
<p>I&rsquo;m genuinely baffled by how many production AI systems I see where every service calls the model provider directly. No centralized routing. No rate limiting. No budget enforcement. No observability.</p>
<p>This is like building a web application in 2024 without a load balancer. Nobody would do that. But somehow AI gets a pass.</p>
<p>A gateway &ndash; call it whatever you want, broker, proxy, control plane &ndash; does the boring work:</p>
<ul>
<li>Routes requests to the right model based on task type</li>
<li>Enforces rate limits and budgets per user, per feature, per environment</li>
<li>Caches deterministic responses</li>
<li>Provides a single point for observability and tracing</li>
<li>Handles provider failover when one API goes down</li>
</ul>
<p>You can build a basic version in a day: a YAML config and a reverse proxy. It doesn&rsquo;t need to be fancy. It needs to exist.</p>
<h2 id="separate-your-workloads">Separate Your Workloads</h2>
<p>Interactive requests and batch processing shouldn&rsquo;t share the same execution path. I keep saying this, and teams keep ignoring it until interactive latency tanks because a batch job saturated the rate limit.</p>
<p>Interactive work gets tight latency budgets and priority access. Batch work gets queued and retried patiently. The split is trivial to implement and painful to retrofit after the fact.</p>
<h2 id="cache-everything-deterministic">Cache. Everything. Deterministic.</h2>
<p>If you&rsquo;re sending the same prompt with the same inputs to the same model and not caching the response, you&rsquo;re burning money. Literally.</p>
<p>Exact-match caching for deterministic requests is table stakes. Similarity-based caching for near-duplicate requests is a bonus. Even a simple TTL-based cache with invalidation on prompt updates can cut costs significantly.</p>
<p>One team was spending $40k/month on model inference. After adding exact-match caching for their classification pipeline, it dropped to $15k. Same outputs. Same quality. Less waste.</p>
<h2 id="cost-controls-arent-optional">Cost Controls Aren&rsquo;t Optional</h2>
<p>&ldquo;We&rsquo;ll optimize costs later&rdquo; is the AI equivalent of &ldquo;we&rsquo;ll add tests later.&rdquo; You won&rsquo;t. And when the bill arrives, it becomes an emergency.</p>
<p>Budget enforcement belongs in the gateway. Hard caps with clear error messages. Soft limits that degrade to cheaper models or slower paths. Per-user and per-feature attribution so you know where the money goes.</p>
<p>I&rsquo;ve seen teams discover that a single feature was responsible for 70% of their AI spend because nobody was tracking attribution. The feature wasn&rsquo;t even high-value. It was just chatty.</p>
<h2 id="reliability-isnt-heroics">Reliability Isn&rsquo;t Heroics</h2>
<p>Retry with backoff. Circuit breakers. Graceful degradation. Provider failover.</p>
<p>These aren&rsquo;t advanced patterns. They&rsquo;re baseline production engineering. If your AI system doesn&rsquo;t have them, it isn&rsquo;t production-ready. It&rsquo;s a demo with a billing account.</p>
<p>Graceful degradation is a product decision, not an ops feature. If the full response is unavailable, a simpler response or a cached response or even a &ldquo;try again in a moment&rdquo; is better than an error page. Design for this upfront. Don&rsquo;t bolt it on during an incident.</p>
<h2 id="the-unsexy-truth">The Unsexy Truth</h2>
<p>AI infrastructure at scale is boring. That&rsquo;s the point. Boring means predictable. Predictable means reliable. Reliable means you can actually build products on top of it.</p>
<p>The gateway, the cache, budget enforcement, workload separation, circuit breakers: none of it is novel. All of it is necessary. The teams that treat AI infrastructure like regular infrastructure, applying patterns that already exist, are the ones that scale without drama.</p>
<p>Stop reinventing. Start reusing. Your SRE team already knows how to do this. Let them.</p>
]]></content:encoded></item><item><title>Your AI Team Problem Is Not Technical</title><link>https://lawzava.com/blog/2024-12-02-building-ai-teams/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-12-02-building-ai-teams/</guid><description>Most AI team failures come from unclear ownership and weak evaluation, not missing talent. Structure and discipline beat hiring sprees.</description><content:encoded><![CDATA[<p>I&rsquo;ve been in or around AI teams since 2018 &ndash; from a startup accelerator to enterprise teams, with roots going back to my first startup. One lesson keeps repeating: teams rarely fail at AI because they lack talent. They fail because nobody owns the outcome.</p>
<p>That sounds harsh. It&rsquo;s also true.</p>
<h2 id="the-ownership-gap">The Ownership Gap</h2>
<p>Here&rsquo;s how it usually goes. A company decides to &ldquo;do AI.&rdquo; They hire an ML engineer, maybe two. Those engineers build a demo. Leadership is impressed. Then someone asks, &ldquo;Who owns this in production?&rdquo; and the room goes quiet.</p>
<p>The ML engineer built the model. The product team didn&rsquo;t spec the success criteria. The data engineer wasn&rsquo;t involved. The designer has no idea what happens when the model gets it wrong. And nobody defined what &ldquo;getting it wrong&rdquo; even means.</p>
<p>I&rsquo;ve seen this exact pattern at large enterprises and small startups. The blocker isn&rsquo;t technology. It&rsquo;s structure.</p>
<h2 id="three-models-that-work">Three Models That Work</h2>
<p>Every successful team I&rsquo;ve seen fits one of three structures, and the core tradeoff has not changed.</p>
<p><strong>Embedded.</strong> AI engineers sit inside product teams. They ship features directly, own the evaluation, and live with the consequences of their choices. This works when AI is a feature, not a platform. The downside: practices drift across teams because there&rsquo;s no central coordination.</p>
<p><strong>Platform.</strong> A central team builds shared infrastructure &ndash; model serving, evaluation harnesses, prompt management, observability. Product teams consume that platform. This works when multiple products need AI. The downside: the platform team gets pulled in every direction and loses focus on any single product.</p>
<p><strong>Hybrid.</strong> A platform team builds the core. Embedded engineers in product teams customize it. This is the most common pattern at companies that have scaled this successfully. It also requires the most coordination. Without clear ownership boundaries, it degenerates into blame-passing between platform and product.</p>
<p>Pick the model that matches your current scale, not the one you hope to need in two years.</p>
<h2 id="who-to-hire">Who to Hire</h2>
<p>The best AI engineers I&rsquo;ve worked with share a few traits that don&rsquo;t show up on resumes.</p>
<p>They can explain how their system fails. Not just how it works, but how it breaks and what happens when it does. This is the best interview signal I&rsquo;ve found.</p>
<p>They think in systems, not models. The model is one component. The retrieval layer, validation step, fallback path, and monitoring are just as important. A candidate who talks only about model architecture is missing the point.</p>
<p>They build evaluations before they build features. If you can&rsquo;t measure whether the thing works, you&rsquo;re guessing. The best engineers treat eval sets like test suites. They version them, maintain them, and refuse to ship without them.</p>
<p>They&rsquo;ve shipped something to real users. Not a notebook. Not a demo. Something people used, complained about, and forced them to iterate on. Production experience changes how you think about every design choice.</p>
<h2 id="the-operating-loop">The Operating Loop</h2>
<p>Fancy process frameworks aren&rsquo;t necessary. A tight loop between four phases covers it:</p>
<p><strong>Discovery.</strong> Define success in measurable terms. What does &ldquo;good&rdquo; look like? What are the edge cases? Is the data available? A clear definition of success is worth more than a long list of ideas.</p>
<p><strong>Prototyping.</strong> Run small experiments with real examples. Document the failures, not just the successes. Bring domain experts in early &ndash; they know the edge cases you&rsquo;ll miss.</p>
<p><strong>Development.</strong> Build the evaluation suite first. Version prompts and retrieval logic as code. Test against known failure cases whenever models or data change.</p>
<p><strong>Production.</strong> Roll out gradually. Monitor quality and cost in the same dashboard. Treat regressions as product issues with named owners, not vague &ldquo;the model changed&rdquo; explanations.</p>
<h2 id="what-actually-goes-wrong">What Actually Goes Wrong</h2>
<p>The problems I see most often aren&rsquo;t technical:</p>
<ul>
<li>Nobody owns evaluation for a specific feature. There&rsquo;s a shared checklist but no named person.</li>
<li>Success criteria are undefined, so feedback becomes opinion. &ldquo;This doesn&rsquo;t feel right&rdquo; isn&rsquo;t actionable.</li>
<li>The pipeline is too complex for the use case. Someone built a multi-agent system for what should have been a single prompt.</li>
<li>Knowledge stays in people&rsquo;s heads. When someone leaves, the team loses context that took months to build.</li>
</ul>
<p>Fix these four problems and you&rsquo;re ahead of most AI teams. No new tools required. No new hires. Just clarity about who owns what and how you know it&rsquo;s working.</p>
<p>That&rsquo;s the whole secret: clear ownership, reliable evaluation, and the discipline to maintain both. Everything else is detail.</p>
]]></content:encoded></item><item><title>Picking an AI Model for Production (Late 2024)</title><link>https://lawzava.com/blog/2024-11-25-ai-model-comparison-2024/</link><pubDate>Mon, 25 Nov 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-11-25-ai-model-comparison-2024/</guid><description>There&amp;amp;rsquo;s no best model. There&amp;amp;rsquo;s the model that fits your workload, latency budget, cost constraint, and ops tolerance. Here&amp;amp;rsquo;s how to compare them.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Benchmarks mislead. The right model depends on your tasks, latency requirements, cost tolerance, and how much ops overhead you can absorb. Run a bake-off on your actual workload. Route between models. Stop looking for a universal winner.</p>
<hr>
<p>I get asked &ldquo;which model should we use?&rdquo; at least once a week. The answer is always the same: it depends. That answer always disappoints, so let me make it useful.</p>
<p>The late-2024 model landscape is competitive enough that the gap between top-tier providers on general tasks is small. The differences that matter most are operational, not intellectual. Here&rsquo;s how I think about model selection for production systems.</p>
<h2 id="the-landscape-at-a-glance">The Landscape at a Glance</h2>
<p>Two tracks dominate. Hosted APIs from Anthropic, OpenAI, and Google iterate fast and are easiest to ship with. Open-weight models from Meta (Llama), Mistral, and others give you more control but come with infrastructure baggage.</p>
<table>
  <thead>
      <tr>
          <th>Track</th>
          <th>Strengths</th>
          <th>Weaknesses</th>
          <th>Best For</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hosted API (frontier)</td>
          <td>Latest capability, zero ops, fast iteration</td>
          <td>Cost at scale, vendor dependency, data leaves your infra</td>
          <td>Most teams starting out, complex reasoning tasks</td>
      </tr>
      <tr>
          <td>Hosted API (mid-tier)</td>
          <td>Good cost/quality ratio, same deployment simplicity</td>
          <td>Weaker on complex tasks, less controllable</td>
          <td>High-volume simple tasks, routing targets</td>
      </tr>
      <tr>
          <td>Open-weight (large)</td>
          <td>Data control, no per-token cost at scale, fine-tunable</td>
          <td>GPU costs, ops burden, slower model updates</td>
          <td>High volume, data residency, offline</td>
      </tr>
      <tr>
          <td>Open-weight (small)</td>
          <td>Fast inference, cheap, embeddable</td>
          <td>Limited capability, more prompt engineering</td>
          <td>Classification, extraction, edge deployment</td>
      </tr>
  </tbody>
</table>
<h2 id="what-to-actually-compare">What to Actually Compare</h2>
<p>Forget leaderboards. They&rsquo;re narrow, gameable, and rarely match your workload. Here are the dimensions that matter in production:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>What to Measure</th>
          <th>Why It Matters</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Task fit</td>
          <td>Success rate on your actual prompts</td>
          <td>A model that aces coding benchmarks might fail your extraction tasks</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>p50 and p95 with realistic prompt sizes</td>
          <td>Average latency hides tail problems that users feel</td>
      </tr>
      <tr>
          <td>Cost per success</td>
          <td>Total spend per completed task, including retries</td>
          <td>Cheap per-token doesn&rsquo;t mean cheap per-task</td>
      </tr>
      <tr>
          <td>Structured output</td>
          <td>JSON/schema compliance rate</td>
          <td>Critical if downstream code parses the response</td>
      </tr>
      <tr>
          <td>Tool use</td>
          <td>Accuracy of function calling and parameter extraction</td>
          <td>Bad tool calls are worse than no tool calls</td>
      </tr>
      <tr>
          <td>Safety/controllability</td>
          <td>Refusal rates, policy adherence, output consistency</td>
          <td>Too permissive or too restrictive both cause problems</td>
      </tr>
      <tr>
          <td>Context handling</td>
          <td>Quality at 8k, 32k, 128k+ tokens</td>
          <td>Long context support isn&rsquo;t the same as long context quality</td>
      </tr>
  </tbody>
</table>
<p>I&rsquo;ve run these comparisons for teams I&rsquo;ve worked with. The results consistently surprise people. The &ldquo;best&rdquo; model on paper is rarely the best model for their specific tasks.</p>
<h2 id="how-to-run-a-bake-off">How to Run a Bake-Off</h2>
<p>Don&rsquo;t spend a month on this. A focused bake-off should take a few days:</p>
<ol>
<li>Pick 30-50 representative inputs from your actual workload. Cover the common cases and the hard cases.</li>
<li>Define success criteria for each one. Not vibes, specific, checkable criteria.</li>
<li>Run each model against the same inputs with the same system prompt.</li>
<li>Score each model by task success rate, latency, and cost.</li>
<li>Check structured output compliance if you depend on it.</li>
</ol>
<p>The results won&rsquo;t be close on every dimension. One model will be cheaper. Another will be more accurate on complex tasks. A third will have better latency. That&rsquo;s the point &ndash; you&rsquo;re mapping the tradeoff space, not finding a winner.</p>
<h2 id="the-router-pattern">The Router Pattern</h2>
<p>Once you have bake-off data, the next step is obvious: route different task types to different models.</p>
<table>
  <thead>
      <tr>
          <th>Task Type</th>
          <th>Route To</th>
          <th>Rationale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Simple classification / extraction</td>
          <td>Small or mid-tier model</td>
          <td>High volume, accuracy is sufficient, saves 60-80%</td>
      </tr>
      <tr>
          <td>Complex reasoning / generation</td>
          <td>Frontier model</td>
          <td>Quality matters, volume is lower</td>
      </tr>
      <tr>
          <td>Structured data extraction</td>
          <td>Model with best schema compliance</td>
          <td>Parsing reliability is non-negotiable</td>
      </tr>
      <tr>
          <td>Latency-critical</td>
          <td>Fastest model that meets quality bar</td>
          <td>User experience trumps marginal quality</td>
      </tr>
      <tr>
          <td>Fallback</td>
          <td>Second provider</td>
          <td>Availability protection</td>
      </tr>
  </tbody>
</table>
<p>A routing layer adds complexity, but not much. An <code>if</code> statement or a config-driven switch is enough to start. You don&rsquo;t need an ML-based router. You need a decision tree grounded in your bake-off results.</p>
<p>One team I worked with went from a single frontier model to a two-model router and cut monthly spend by 60% with no measurable quality regression. The hard part was running the bake-off. The router itself was 50 lines of Go.</p>
<h2 id="open-models-when-and-when-not">Open Models: When and When Not</h2>
<p>Self-hosting is a real option now. Llama 3 and Mistral variants are genuinely capable. But the question isn&rsquo;t &ldquo;can it do the task?&rdquo; It&rsquo;s &ldquo;do we want to own the infrastructure?&rdquo;</p>
<p>Self-host when:</p>
<ul>
<li>Data must not leave your network (regulatory, contractual)</li>
<li>Volume is high and predictable enough that fixed GPU costs beat per-token pricing</li>
<li>You need fine-tuning that hosted APIs don&rsquo;t support</li>
<li>You need offline or air-gapped operation</li>
</ul>
<p>Don&rsquo;t self-host when:</p>
<ul>
<li>Volume is bursty or growing unpredictably</li>
<li>You need frontier capability that open models haven&rsquo;t matched yet</li>
<li>Your team doesn&rsquo;t have GPU ops experience</li>
<li>You want to iterate model versions quickly</li>
</ul>
<p>I&rsquo;ve talked a few teams out of self-hosting after running the numbers. The GPU costs plus ops burden plus slower iteration cycle made the total cost higher than the hosted API they were trying to replace. Self-hosting is a capability decision as much as a cost decision.</p>
<h2 id="contracts-and-pricing-check-the-fine-print">Contracts and Pricing: Check the Fine Print</h2>
<p>Pricing shifts fast. What I can tell you as of late 2024:</p>
<ul>
<li>The spread between frontier and mid-tier models is 10-30x on a per-token basis</li>
<li>Total cost is dominated by usage patterns (retries, context size, output length), not headline price</li>
<li>Enterprise agreements often include committed-use discounts that change the math significantly</li>
<li>Rate limits and quotas vary by tier and can cap throughput during peak usage</li>
</ul>
<p>Verify current rates directly with providers before locking in. A pricing comparison that&rsquo;s two months old is already stale.</p>
<h2 id="the-only-advice-that-ages-well">The Only Advice That Ages Well</h2>
<p>There&rsquo;s no universal winner. Run a focused bake-off on your tasks, build a simple router, monitor everything, and re-evaluate quarterly. The model landscape moves fast. Your selection process should be fast too.</p>
<p>Treat vendor claims and public benchmarks as starting points, not decisions. The evaluation set built from your actual prompts, reviewed by your team, is the only benchmark that matters.</p>
]]></content:encoded></item><item><title>AI Safety Is Just Production Engineering</title><link>https://lawzava.com/blog/2024-11-11-ai-safety-production/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-11-11-ai-safety-production/</guid><description>AI safety in production isn&amp;amp;rsquo;t a research problem. It&amp;amp;rsquo;s defense in depth, the same way cyber defense works &amp;amp;ndash; layered controls, assumed breach, observable boundaries.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Treat AI safety like you treat security: assume breach, layer your defenses, and make every boundary observable. A single filter will fail. A layered system with clear escalation paths won&rsquo;t.</p>
<hr>
<p>My time working with national cyber-defense taught me one lesson that transfers directly to AI safety: if your security model depends on a single control working perfectly, you don&rsquo;t have a security model. You have hope.</p>
<p>Most AI safety implementations I review look like this: one content filter, one system prompt instruction, maybe a regex check on output. Then comes surprise when someone finds a bypass in production.</p>
<p>AI safety isn&rsquo;t a research frontier. It&rsquo;s production engineering. The same defense-in-depth thinking that protects networks also protects AI systems. The mental model is the same.</p>
<h2 id="assume-your-controls-will-be-tested">Assume Your Controls Will Be Tested</h2>
<p>The moment you deploy an AI system to users, it becomes a target. Not always from malicious actors &ndash; though those exist &ndash; but from curious users, edge cases you never imagined, and the simple reality that models do unexpected things with novel inputs.</p>
<p>In cyber defense, you plan for this. You assume the perimeter will be breached and design the interior to limit damage. AI safety is the same. Assume:</p>
<ul>
<li>Someone will try prompt injection. They&rsquo;ll try hard.</li>
<li>The model will occasionally produce harmful or inappropriate output. No filter catches everything.</li>
<li>Data will leak through outputs or logs if you don&rsquo;t explicitly prevent it.</li>
<li>Users will find ways to use capabilities you didn&rsquo;t intend to expose.</li>
</ul>
<p>This isn&rsquo;t pessimism. It&rsquo;s operational realism. Plan for it.</p>
<h2 id="input-treat-it-as-untrusted">Input: Treat It as Untrusted</h2>
<p>Every input to your AI system is untrusted. Full stop. This isn&rsquo;t different from web security &ndash; you wouldn&rsquo;t pass raw user input to a SQL query. Don&rsquo;t pass raw user input to a model without validation.</p>
<p>Practical input controls:</p>
<ul>
<li>Separate user content from system instructions at the architecture level, not just the prompt level</li>
<li>Length and format limits for every input field</li>
<li>Explicit allowlists for supported content types and languages</li>
<li>PII detection with consent-aware handling</li>
<li>Pattern checks for known injection techniques</li>
</ul>
<p>Keep these simple. Complex input policies are hard to test, hard to maintain, and easy to bypass. A few robust checks beat a hundred brittle ones.</p>
<h2 id="output-the-last-boundary">Output: The Last Boundary</h2>
<p>Output is the final safety layer before the user sees a response. In my national cyber-defense work, we called this the &ldquo;last line of defense&rdquo; principle: design it assuming everything upstream has already failed.</p>
<p>Output controls:</p>
<ul>
<li>Content filtering to block or redact unsafe responses</li>
<li>Leakage checks for system prompts, internal data, or PII</li>
<li>Schema validation when the response must follow a defined format</li>
<li>Safe fallback behavior when a response fails any check</li>
</ul>
<p>Fallback behavior matters more than people think. A system that returns &ldquo;I can&rsquo;t help with that&rdquo; when unsure is vastly safer than one that guesses and serves a plausible-looking wrong answer. Refusal is a feature.</p>
<h2 id="system-level-controls">System-Level Controls</h2>
<p>Safety doesn&rsquo;t live in the model layer alone. It belongs in the surrounding system. This is where the cyber defense analogy is strongest: you don&rsquo;t just firewall the endpoint, you design the entire network for containment.</p>
<p><strong>Rate limits and quotas</strong> reduce abuse surface and cost spikes. If someone is hammering your system with injection attempts, rate limiting slows them down before any content filter needs to fire.</p>
<p><strong>Scoped tool access</strong> with clear permissions limits blast radius. If your agent can call APIs, those APIs should have the minimum permissions required. Not admin. Not read-write when read-only suffices.</p>
<p><strong>Sandboxed execution</strong> for anything that touches external systems. If your agent generates code or makes API calls, run those in a sandbox. No exceptions.</p>
<p><strong>Configurable policy modes</strong> so you can tighten safety quickly during an incident. A kill switch isn&rsquo;t elegant but it&rsquo;s necessary.</p>
<h2 id="monitoring-safety-is-operational">Monitoring: Safety Is Operational</h2>
<p>In cyber defense, detection matters as much as prevention. You need to know when your controls are failing. The same applies to AI safety.</p>
<p>Treat safety incidents like reliability incidents:</p>
<ul>
<li>Define thresholds for unsafe output rates, injection attempt rates, and escalation volumes</li>
<li>Set up clear escalation paths &ndash; who gets paged, what gets rolled back, what needs a review</li>
<li>Feed production signals back into model prompts, filters, and product design</li>
<li>Run regular reviews. Not quarterly. Weekly at minimum during early deployment.</li>
</ul>
<p>The teams that catch problems early treat safety as an operational concern. The teams that catch problems late treat it as a PR crisis.</p>
<h2 id="defense-in-depth">Defense in Depth</h2>
<p>A single safeguard will fail. I can&rsquo;t say this enough. Every content filter has bypasses. Every system prompt can be manipulated under the right conditions. Every validation check has edge cases.</p>
<p>The defense-in-depth approach layers controls so that any single failure doesn&rsquo;t become an incident:</p>
<ol>
<li>Input validation catches obvious abuse</li>
<li>System prompt discipline limits the model&rsquo;s scope</li>
<li>Output filtering catches problematic responses</li>
<li>System controls (rate limits, permissions, sandboxing) limit blast radius</li>
<li>Monitoring detects when any layer is failing</li>
</ol>
<p>Each layer is simple. The combination is robust. This isn&rsquo;t a new idea &ndash; it&rsquo;s how every mature security program works. AI safety should be no different.</p>
<h2 id="where-to-start">Where to Start</h2>
<p>If you&rsquo;re deploying AI to production and haven&rsquo;t built safety controls yet, start small:</p>
<ul>
<li>Define the allowed inputs and outputs for your first use case. Write them down.</li>
<li>Implement input validation and output filtering with clear failure behavior</li>
<li>Add rate limiting and logging</li>
<li>Set up a simple review queue for flagged interactions</li>
<li>Iterate based on what you see in production</li>
</ul>
<p>Don&rsquo;t try to build a perfect safety system before shipping. Build a functional one, instrument it, and improve it continuously. Teams that wait for perfection ship nothing. Teams that ship with layered, observable safety controls learn fast and get better.</p>
<p>Safe systems and reliable systems are built the same way. Clear boundaries, observable behavior, steady iteration. The discipline transfers.</p>
]]></content:encoded></item><item><title>Agent Patterns That Survive Production</title><link>https://lawzava.com/blog/2024-10-28-advanced-agent-patterns/</link><pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-10-28-advanced-agent-patterns/</guid><description>Single-prompt agents break on real tasks. Plan-execute-replan, orchestrated specialists, structured memory, and explicit recovery are what survive &amp;amp;ndash; in Go.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Agents need structure, not longer prompts. Plan-execute-replan, specialist orchestration, compact memory management, and explicit recovery paths are the patterns that hold up. This post walks through each one with Go implementations.</p>
<hr>
<p>I&rsquo;ve been building and reviewing agent systems most of this year. The pattern is always the same: someone builds a single-prompt agent, it works beautifully on the happy path, and then it meets a real task and falls apart.</p>
<p>The fix is never &ldquo;make the prompt better.&rdquo; It&rsquo;s always &ldquo;add structure around the model.&rdquo; Here are the patterns that actually survive production, with Go code you can adapt.</p>
<h2 id="when-simple-agents-break">When Simple Agents Break</h2>
<p>Simple agents &ndash; one prompt, one model call, maybe a tool &ndash; fail predictably once tasks get real:</p>
<ul>
<li>More steps than fit in one context window</li>
<li>Tool calls that return errors or ambiguous results</li>
<li>Multiple valid paths with unknown payoff</li>
<li>Dependencies between sub-tasks that require ordering</li>
</ul>
<p>If your task has any of these properties, you need patterns. Not hope.</p>
<h2 id="plan-execute-replan">Plan, Execute, Replan</h2>
<p>The most useful pattern is also the simplest. Break the task into a plan, execute steps sequentially, and replan when reality diverges from the plan.</p>
<p>The plan is a draft, not a contract.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Plan represents a sequence of steps the agent intends to execute.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Steps can be updated mid-execution when results diverge.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Plan</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Goal</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Steps</span>     []<span style="color:#a6e22e">Step</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Completed</span> []<span style="color:#a6e22e">StepResult</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Step</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ID</span>          <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Description</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ToolName</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Input</span>       <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">StepResult</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">StepID</span>  <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Output</span>  <span style="color:#66d9ef">any</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Err</span>     <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Blocked</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Execute runs through the plan, replanning when a step is blocked</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// or produces unexpected results.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Plan</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Plan</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> len(<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Steps</span>) &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">step</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Steps</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Steps</span> = <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Steps</span>[<span style="color:#ae81ff">1</span>:]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">runStep</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">step</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Completed</span> = append(<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Completed</span>, <span style="color:#a6e22e">result</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">Blocked</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">Err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">revised</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">replan</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">p</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;replan failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">p</span> = <span style="color:#a6e22e">revised</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// replan asks the model to revise remaining steps given what has</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// happened so far. The completed results provide context.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>) <span style="color:#a6e22e">replan</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Plan</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Plan</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;Goal: %s\nCompleted: %s\nRevise the remaining steps.&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Goal</span>, <span style="color:#a6e22e">formatResults</span>(<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Completed</span>),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">llm</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">prompt</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">Steps</span> = <span style="color:#a6e22e">parseSteps</span>(<span style="color:#a6e22e">resp</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The key design choice is to replan on failure, not on every step. Replanning is expensive &ndash; it costs a model call and risks plan instability. Only trigger it when the current plan is provably broken.</p>
<p>I&rsquo;ve seen teams replan after every step &ldquo;for safety.&rdquo; The result is an agent that never commits to anything and burns tokens oscillating between plans. Pick a plan, execute, and adjust on failure, not anxiety.</p>
<h2 id="orchestrator-specialist-pattern">Orchestrator-Specialist Pattern</h2>
<p>When tasks naturally split into parallel or specialized work, a single agent doing everything is the wrong abstraction. Use an orchestrator that breaks the task down and dispatches to specialists.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Orchestrator decomposes a task and dispatches sub-tasks to</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// specialist agents. It synthesizes their results.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Orchestrator</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">planner</span>     <span style="color:#a6e22e">LLM</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">specialists</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#f92672">*</span><span style="color:#a6e22e">Specialist</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Specialist</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Name</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Agent</span>   <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Domain</span>  <span style="color:#66d9ef">string</span> <span style="color:#75715e">// e.g. &#34;research&#34;, &#34;code-generation&#34;, &#34;validation&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SubTask</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ID</span>          <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Description</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Specialist</span>  <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Input</span>       <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">DependsOn</span>   []<span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Run decomposes the task, executes sub-tasks respecting dependencies,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// and synthesizes results.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">o</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Orchestrator</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">task</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">subtasks</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">decompose</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;decompose: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">results</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">batch</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">topologicalBatches</span>(<span style="color:#a6e22e">subtasks</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">g</span>, <span style="color:#a6e22e">gCtx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">errgroup</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">st</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">batch</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">st</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">st</span>
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">spec</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">specialists</span>[<span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Specialist</span>]
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;unknown specialist: %s&#34;</span>, <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Specialist</span>)
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Go</span>(<span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#75715e">// Inject dependency results into the sub-task input.</span>
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">dep</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">DependsOn</span> {
</span></span><span style="display:flex;"><span>					<span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Input</span>[<span style="color:#a6e22e">dep</span>] = <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">dep</span>]
</span></span><span style="display:flex;"><span>				}
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">res</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">spec</span>.<span style="color:#a6e22e">Agent</span>.<span style="color:#a6e22e">RunTask</span>(<span style="color:#a6e22e">gCtx</span>, <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Description</span>, <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Input</span>)
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>					<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;specialist %s: %w&#34;</span>, <span style="color:#a6e22e">spec</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>				}
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">ID</span>] = <span style="color:#a6e22e">res</span>
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>			})
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">g</span>.<span style="color:#a6e22e">Wait</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">synthesize</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>, <span style="color:#a6e22e">results</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The topological batching is important. Sub-tasks without dependencies run in parallel. Sub-tasks that depend on earlier results wait. This gives you concurrency where it&rsquo;s safe and ordering where it&rsquo;s required.</p>
<p>Go&rsquo;s <code>errgroup</code> is perfect for this. I&rsquo;ve tried this pattern in Python with asyncio, and the error handling is significantly worse. Go&rsquo;s explicit error returns make failure paths clear.</p>
<h2 id="structured-working-memory">Structured Working Memory</h2>
<p>Context windows are finite and expensive. You can&rsquo;t dump every intermediate result into the prompt and hope for the best. Working memory needs structure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Memory manages the agent&#39;s working context with size limits</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// and periodic compression.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Memory</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">mu</span>       <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Mutex</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">facts</span>    []<span style="color:#a6e22e">Fact</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">maxFacts</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">llm</span>      <span style="color:#a6e22e">LLM</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Fact</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Key</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Value</span>     <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Source</span>    <span style="color:#66d9ef">string</span> <span style="color:#75715e">// which step produced this</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Priority</span>  <span style="color:#66d9ef">int</span>    <span style="color:#75715e">// higher = keep longer</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Add inserts a fact, compressing if the memory is full.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Memory</span>) <span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">f</span> <span style="color:#a6e22e">Fact</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span> = append(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>, <span style="color:#a6e22e">f</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>) &gt; <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">maxFacts</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">compress</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// compress asks the model to summarize low-priority facts into</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// fewer entries, keeping high-priority facts intact.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Memory</span>) <span style="color:#a6e22e">compress</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sort</span>.<span style="color:#a6e22e">Slice</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">Priority</span> &gt; <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>[<span style="color:#a6e22e">j</span>].<span style="color:#a6e22e">Priority</span>
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Keep top half as-is, compress bottom half.</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">keep</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>[:<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">maxFacts</span><span style="color:#f92672">/</span><span style="color:#ae81ff">2</span>]
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">toCompress</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>[<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">maxFacts</span><span style="color:#f92672">/</span><span style="color:#ae81ff">2</span>:]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">summary</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">llm</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;Summarize these facts into 2-3 key points:\n%s&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">formatFacts</span>(<span style="color:#a6e22e">toCompress</span>),
</span></span><span style="display:flex;"><span>	))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#75715e">// On failure, just drop the lowest priority facts.</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span> = <span style="color:#a6e22e">keep</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span> = append(<span style="color:#a6e22e">keep</span>, <span style="color:#a6e22e">Fact</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Key</span>:       <span style="color:#e6db74">&#34;compressed_context&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Value</span>:     <span style="color:#a6e22e">summary</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Priority</span>:  <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">CreatedAt</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ForPrompt renders the current memory as a string for inclusion</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// in a prompt.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Memory</span>) <span style="color:#a6e22e">ForPrompt</span>() <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">formatFacts</span>(<span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">facts</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The compression strategy matters. High-priority facts (decisions, constraints, key results) stay intact. Low-priority facts (intermediate outputs, exploration notes) get summarized. If compression fails, drop the least important items rather than crashing.</p>
<p>I keep raw tool outputs entirely outside the prompt. They go into a side store the agent can query if needed. Only extracted facts enter working memory.</p>
<h2 id="explicit-recovery">Explicit Recovery</h2>
<p>This is the pattern most teams skip, and it&rsquo;s the one that matters most in production. Agents will encounter tool failures, stale plans, missing inputs, and model refusals. Without explicit recovery, those become silent failures or infinite loops.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// RecoveryStrategy defines how the agent handles a specific failure type.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RecoveryStrategy</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Name</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxRetries</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Backoff</span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Handler</span>    <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) (<span style="color:#a6e22e">Action</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Action</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Retry</span>        <span style="color:#a6e22e">Action</span> = <span style="color:#66d9ef">iota</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Decompose</span>           <span style="color:#75715e">// break the failed step into smaller steps</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Skip</span>                <span style="color:#75715e">// mark step as skipped, continue</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Escalate</span>            <span style="color:#75715e">// pause for human input</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Abort</span>               <span style="color:#75715e">// stop the agent</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Recover selects and applies the appropriate recovery strategy.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>) <span style="color:#a6e22e">Recover</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">step</span> <span style="color:#a6e22e">Step</span>, <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) (<span style="color:#a6e22e">Action</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">strategy</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">selectStrategy</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">attempt</span> &lt; <span style="color:#a6e22e">strategy</span>.<span style="color:#a6e22e">MaxRetries</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">retryErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strategy</span>.<span style="color:#a6e22e">Handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">retryErr</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">action</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">strategy</span>.<span style="color:#a6e22e">Backoff</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">attempt</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>))
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// All retries exhausted. Escalate.</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Escalate</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;recovery exhausted for step %s after %d attempts: %w&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">step</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">strategy</span>.<span style="color:#a6e22e">MaxRetries</span>, <span style="color:#a6e22e">err</span>,
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The key insight: recovery actions are an enum, not free-form decisions. The agent picks from a fixed set of responses. Retry, decompose, skip, escalate, or abort. No improvisation. This keeps the failure paths testable and predictable.</p>
<p>The escalation path &ndash; pausing for human input &ndash; isn&rsquo;t a failure. It&rsquo;s a feature. An agent that knows when to ask for help is more reliable than one that guesses and gets it wrong.</p>
<h2 id="putting-it-together">Putting It Together</h2>
<p>A production agent combines these patterns in layers:</p>
<ol>
<li><strong>Plan-execute-replan</strong> as the outer loop</li>
<li><strong>Orchestrator-specialist</strong> for sub-task parallelism</li>
<li><strong>Structured memory</strong> to manage context within budget</li>
<li><strong>Explicit recovery</strong> at every step boundary</li>
</ol>
<p>Each layer is independently testable. You can unit test recovery strategies, benchmark memory compression, and integration test the orchestrator without running the full agent.</p>
<p>Start with plan-execute-replan and explicit recovery. Those two patterns alone will take you from &ldquo;works on demos&rdquo; to &ldquo;works on real tasks.&rdquo; Add orchestration and structured memory when your tasks demand it.</p>
<p>The agents that survive production aren&rsquo;t clever. They&rsquo;re disciplined.</p>
]]></content:encoded></item><item><title>AI Cost Benchmarking: What Your Bill Actually Tells You</title><link>https://lawzava.com/blog/2024-10-14-ai-cost-benchmarking/</link><pubDate>Mon, 14 Oct 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-10-14-ai-cost-benchmarking/</guid><description>Price-per-token is the least useful number on your AI bill. Real cost benchmarking starts with your workload, not a provider&amp;amp;rsquo;s pricing page.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your AI cost isn&rsquo;t what the pricing page says. It&rsquo;s tokens times retries times fallbacks times human review &ndash; all shaped by your specific prompts and workload. Benchmark against your actual tasks or you&rsquo;re optimizing fiction.</p>
<hr>
<p>Every few weeks someone sends me a spreadsheet comparing AI provider pricing and asks &ldquo;which one should we use?&rdquo; The spreadsheet always compares cost per million tokens. It&rsquo;s always useless.</p>
<p>After working on AI cost optimization since early 2024, I can tell you the gap between headline pricing and actual production cost is consistently 3-10x. Providers with the cheapest tokens sometimes end up being the most expensive per completed task. Here&rsquo;s why and how to benchmark properly.</p>
<h2 id="the-real-cost-stack">The Real Cost Stack</h2>
<p>Token price is one line item. Production cost includes everything the system does to deliver a reliable result.</p>
<table>
  <thead>
      <tr>
          <th>Cost Layer</th>
          <th>What It Includes</th>
          <th>Typical Share</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Model inference</td>
          <td>Input + output tokens</td>
          <td>30-50%</td>
      </tr>
      <tr>
          <td>Retries &amp; fallbacks</td>
          <td>Failed attempts, quality retries, provider failover</td>
          <td>10-25%</td>
      </tr>
      <tr>
          <td>Retrieval &amp; preprocessing</td>
          <td>Embedding, search, context assembly</td>
          <td>10-20%</td>
      </tr>
      <tr>
          <td>Human review</td>
          <td>Escalation, QA sampling, edge case handling</td>
          <td>10-30%</td>
      </tr>
      <tr>
          <td>Infrastructure</td>
          <td>Caching, logging, orchestration</td>
          <td>5-10%</td>
      </tr>
  </tbody>
</table>
<p>Teams that track only model inference are missing half their spend. I learned this the hard way on a document processing pipeline: the retry rate on complex documents was 40%, effectively doubling model cost. The pricing spreadsheet didn&rsquo;t mention that.</p>
<h2 id="benchmark-your-tasks-not-generic-prompts">Benchmark Your Tasks, Not Generic Prompts</h2>
<p>A useful benchmark mirrors your actual workload. Generic &ldquo;summarize this article&rdquo; tests tell you nothing about how a model handles your prompts, error rates, and latency requirements.</p>
<p>Build a benchmark set that covers:</p>
<table>
  <thead>
      <tr>
          <th>Task Category</th>
          <th>Why It Matters</th>
          <th>What to Measure</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>High-volume simple tasks</td>
          <td>Dominates token count</td>
          <td>Cost per success, latency p50</td>
      </tr>
      <tr>
          <td>Complex multi-step tasks</td>
          <td>Dominates per-task spend</td>
          <td>Total cost including retries, success rate</td>
      </tr>
      <tr>
          <td>Edge cases / policy triggers</td>
          <td>Drives fallback and review cost</td>
          <td>Escalation rate, human time per case</td>
      </tr>
      <tr>
          <td>Retrieval-heavy tasks</td>
          <td>Preprocessing is a big chunk of cost</td>
          <td>End-to-end cost, retrieval overhead ratio</td>
      </tr>
  </tbody>
</table>
<p>Keep this set stable. If benchmark inputs change every week, you can&rsquo;t tell whether cost shifts came from system changes or test changes.</p>
<h2 id="compare-approaches-not-providers">Compare Approaches, Not Providers</h2>
<p>Provider names and model versions change quarterly. A benchmark built around &ldquo;GPT-4 vs Claude 3.5&rdquo; has a shelf life of weeks. Instead, compare the architectural choices you control:</p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>Cost Profile</th>
          <th>When It Wins</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Large model, single pass</td>
          <td>High per-call, low retry</td>
          <td>Simple tasks, tight latency budgets</td>
      </tr>
      <tr>
          <td>Small model + reranker</td>
          <td>Lower per-call, extra step</td>
          <td>High volume, tolerance for pipeline complexity</td>
      </tr>
      <tr>
          <td>Router: small for easy, large for hard</td>
          <td>Variable, needs routing logic</td>
          <td>Mixed workloads with clear difficulty signals</td>
      </tr>
      <tr>
          <td>Self-hosted open model</td>
          <td>Fixed infra cost, zero per-token</td>
          <td>High volume, data residency, offline needs</td>
      </tr>
  </tbody>
</table>
<p>The router pattern is where I&rsquo;ve seen the biggest wins. One team cut monthly spend by 60% by routing straightforward classification tasks to a small model and reserving the large model for generation. Classification accuracy from the small model was identical. They were paying frontier prices for commodity work.</p>
<h2 id="the-drivers-that-actually-move-your-bill">The Drivers That Actually Move Your Bill</h2>
<p>Forget micro-optimizing prompts. These four factors determine 80% of your cost trajectory:</p>
<p><strong>Response length drift.</strong> Prompts evolve over time. Engineers add instructions, examples, formatting requirements. Output gets longer. Nobody notices until the bill does. Track average output tokens per task type weekly.</p>
<p><strong>Retry rates.</strong> Every retry is a full cost event. If your validation rejects 20% of responses and retries, your effective cost is 1.25x the base. If it retries twice on failure, it&rsquo;s worse. Measure retry rate by task type and fix the root cause.</p>
<p><strong>Retrieval bloat.</strong> Context windows keep growing, so teams stuff more chunks in. More context means more input tokens. But past a point, more context doesn&rsquo;t improve answers &ndash; it just costs more. Measure answer quality versus context size and find the plateau.</p>
<p><strong>Routing waste.</strong> Sending everything to the most capable model is the default because it&rsquo;s easy. It&rsquo;s also the most expensive default. Any task where a smaller model achieves the same success rate is money burned on the large model.</p>
<h2 id="self-hosting-when-the-math-works">Self-Hosting: When the Math Works</h2>
<p>Self-hosting isn&rsquo;t a cost optimization for most teams. It works for teams with specific constraints:</p>
<ul>
<li>Predictable, high-volume workloads where the per-token savings exceed infra costs</li>
<li>Strict data residency or air-gapped environments</li>
<li>Fine-tuned models that don&rsquo;t exist as hosted APIs</li>
</ul>
<p>For bursty workloads or teams that need frequent model upgrades, operational overhead eats the savings. I&rsquo;ve talked a few teams out of self-hosting after we modeled actual GPU costs, ops burden, and iteration-speed penalties. The math didn&rsquo;t work for them. It might for you. Run the numbers on your workload, not someone else&rsquo;s blog post.</p>
<h2 id="set-up-monitoring-before-you-need-it">Set Up Monitoring Before You Need It</h2>
<p>A benchmark is a snapshot. Production spend is a moving target. Set up cost monitoring from day one:</p>
<ul>
<li>Track cost per successful task, not cost per API call</li>
<li>Break it down by feature and user tier</li>
<li>Alert on spend spikes and retry rate increases</li>
<li>Review monthly with someone who owns the budget</li>
</ul>
<p>The teams that catch cost problems early treat them like performance regressions. The teams that catch them late treat them like budget emergencies.</p>
<p>Boring systems, predictable bills.</p>
]]></content:encoded></item><item><title>RAG Retrieval That Actually Works</title><link>https://lawzava.com/blog/2024-09-30-retrieval-strategies-rag/</link><pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-09-30-retrieval-strategies-rag/</guid><description>Most RAG failures are retrieval failures. Hybrid search, smarter chunking, query expansion, and reranking &amp;amp;ndash; measured separately from generation.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop blaming the LLM. If your RAG system gives bad answers, the retrieval is almost certainly the bottleneck. Hybrid search, proper chunking, query expansion, and reranking &ndash; measured separately from generation &ndash; will do more for answer quality than any prompt engineering trick.</p>
<hr>
<p>I&rsquo;ve built three different  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >RAG systems</a>
 this year, and each time the first complaint was &ldquo;the model hallucinates.&rdquo; Each time, the real problem was retrieval feeding garbage into context. The model was doing its best with bad evidence.</p>
<p>Basic RAG &ndash; embed the query, grab the top-k chunks, stuff them into the prompt &ndash; is a fragile baseline. It works in demos. It breaks on real data. Here&rsquo;s why, and what to do about it.</p>
<h2 id="why-basic-retrieval-fails">Why Basic Retrieval Fails</h2>
<p>The failure modes are predictable. I see the same ones everywhere:</p>
<p><strong>Vocabulary mismatch.</strong> The user asks about &ldquo;cancellation policy&rdquo; but the source document says &ldquo;termination terms.&rdquo; Pure  <a href="/blog/2023-06-26-semantic-search-implementation/"
   
   >semantic search</a>
 sometimes bridges this gap. Sometimes it doesn&rsquo;t.</p>
<p><strong>Context fragmentation.</strong> A paragraph that answers the question gets split across two chunks. Neither chunk scores high enough on its own. The answer exists in your corpus but the retrieval never finds it.</p>
<p><strong>Wrong granularity.</strong> Your chunks are 512 tokens. The user asks a question that needs a 50-token fact buried in the middle. The surrounding noise tanks the relevance score.</p>
<p><strong>Temporal confusion.</strong> The 2022 policy and the 2024 policy both match the query. The retrieval returns whichever embeds closer, not whichever is current.</p>
<p><strong>Multi-hop requirements.</strong> The answer requires combining facts from two different documents. Single-query retrieval will find one, maybe. Not both.</p>
<h2 id="hybrid-search-combine-signals">Hybrid Search: Combine Signals</h2>
<p>Pure  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector search</a>
 misses exact terms. Pure lexical search misses paraphrases. Combine them.</p>
<p>The implementation is straightforward. Run both searches, normalize the scores, and fuse the rankings. Reciprocal Rank Fusion (RRF) is the simplest approach that works:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">search</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// RRFMerge combines results from multiple search backends using</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Reciprocal Rank Fusion. k controls how much rank position</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// matters -- 60 is a common default.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">RRFMerge</span>(<span style="color:#a6e22e">results</span> [][]<span style="color:#a6e22e">Result</span>, <span style="color:#a6e22e">k</span> <span style="color:#66d9ef">float64</span>) []<span style="color:#a6e22e">Result</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">scores</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">float64</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">docs</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">Result</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">ranked</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">results</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">rank</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">ranked</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">scores</span>[<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">ID</span>] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1.0</span> <span style="color:#f92672">/</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">+</span> float64(<span style="color:#a6e22e">rank</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>))
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">docs</span>[<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">ID</span>] = <span style="color:#a6e22e">r</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">merged</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">Result</span>, <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">scores</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">id</span>, <span style="color:#a6e22e">score</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">scores</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">doc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">docs</span>[<span style="color:#a6e22e">id</span>]
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">doc</span>.<span style="color:#a6e22e">Score</span> = <span style="color:#a6e22e">score</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">merged</span> = append(<span style="color:#a6e22e">merged</span>, <span style="color:#a6e22e">doc</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sort</span>.<span style="color:#a6e22e">Slice</span>(<span style="color:#a6e22e">merged</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">merged</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">Score</span> &gt; <span style="color:#a6e22e">merged</span>[<span style="color:#a6e22e">j</span>].<span style="color:#a6e22e">Score</span>
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">merged</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>From what I&rsquo;ve seen, hybrid search with RRF improves recall by 15-30% over pure vector search on real corpora. Not synthetic benchmarks &ndash; real production data with messy, inconsistent documents.</p>
<h2 id="chunking-isnt-a-formatting-detail">Chunking Isn&rsquo;t a Formatting Detail</h2>
<p>Most teams treat chunking as a config parameter. Set <code>chunk_size=512</code>, done. This is wrong.</p>
<p>Good chunking preserves the structure of the source material. If your documents have headings, keep them. If a section is self-contained, chunk it as a unit. If a chunk can&rsquo;t be understood without its parent heading, prepend a breadcrumb.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Chunk represents a document fragment with enough context</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// to be understood when retrieved independently.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Chunk</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ID</span>         <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Content</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Breadcrumb</span> <span style="color:#66d9ef">string</span> <span style="color:#75715e">// e.g. &#34;Policy Manual &gt; Section 4 &gt; Termination&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Source</span>     <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">UpdatedAt</span>  <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Tokens</span>     <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ChunkWithContext prepends the breadcrumb to the content so the</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// chunk is self-contained when injected into a prompt.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#a6e22e">Chunk</span>) <span style="color:#a6e22e">ChunkWithContext</span>() <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Breadcrumb</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Content</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;[%s]\n\n%s&#34;</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Breadcrumb</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Content</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The breadcrumb costs a few tokens per chunk. It pays for itself by making the model understand what it&rsquo;s reading. Without it, the model gets a floating paragraph with no context about where it came from.</p>
<h2 id="query-expansion">Query Expansion</h2>
<p>Single-shot queries are narrow. The user types one phrasing, but the relevant document uses different words. You miss.</p>
<p>Query expansion generates alternative phrasings and retrieves against all of them. The simplest version that works: ask the LLM to generate 2-3 reformulations, then run all queries and merge results.</p>
<p>A more interesting approach is HyDE (Hypothetical Document Embeddings). Instead of expanding the query, generate a hypothetical answer and embed that. The intuition is that a hypothetical answer is closer in  <a href="/blog/2023-07-10-embedding-models-deep-dive/"
   
   >embedding space</a>
 to the actual answer than the question is.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// ExpandQuery generates alternative phrasings for retrieval.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Returns the original query plus expansions.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ExpandQuery</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">llm</span> <span style="color:#a6e22e">LLM</span>, <span style="color:#a6e22e">query</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">n</span> <span style="color:#66d9ef">int</span>) ([]<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;Generate %d alternative phrasings of this search query. &#34;</span><span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>			<span style="color:#e6db74">&#34;Return only the queries, one per line.\n\nQuery: %s&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">n</span>, <span style="color:#a6e22e">query</span>,
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">llm</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">prompt</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#75715e">// Fallback: just use the original query.</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> []<span style="color:#66d9ef">string</span>{<span style="color:#a6e22e">query</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">queries</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">string</span>{<span style="color:#a6e22e">query</span>}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">line</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">resp</span>, <span style="color:#e6db74">&#34;\n&#34;</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">line</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">line</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">line</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">queries</span> = append(<span style="color:#a6e22e">queries</span>, <span style="color:#a6e22e">line</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">queries</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Note the error handling: if expansion fails, fall back to the original query. Don&rsquo;t let a retrieval enhancement become a retrieval blocker.</p>
<p>Expansion increases recall, but it also brings in noise. That&rsquo;s fine, because the next step handles it.</p>
<h2 id="reranking-the-cleanup-step">Reranking: The Cleanup Step</h2>
<p>After gathering candidates from hybrid search across expanded queries, you have a broad set. Most of it is relevant. Some isn&rsquo;t. A reranker fixes the ordering.</p>
<p>A cross-encoder reranker compares the full query against the full chunk text. It&rsquo;s slower than embedding similarity but significantly more accurate for the final ranking. Run it on your top 20-50 candidates, not your entire corpus.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Rerank takes candidate chunks and reorders them by relevance</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// using a cross-encoder model. Keep topN results.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Rerank</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">model</span> <span style="color:#a6e22e">Reranker</span>, <span style="color:#a6e22e">query</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">candidates</span> []<span style="color:#a6e22e">Chunk</span>, <span style="color:#a6e22e">topN</span> <span style="color:#66d9ef">int</span>) ([]<span style="color:#a6e22e">Chunk</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">type</span> <span style="color:#a6e22e">scored</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">chunk</span> <span style="color:#a6e22e">Chunk</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">score</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">pairs</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">QueryDocPair</span>, len(<span style="color:#a6e22e">candidates</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">candidates</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">pairs</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">QueryDocPair</span>{<span style="color:#a6e22e">Query</span>: <span style="color:#a6e22e">query</span>, <span style="color:#a6e22e">Document</span>: <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ChunkWithContext</span>()}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">scores</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">model</span>.<span style="color:#a6e22e">Score</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">pairs</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">candidates</span>[:<span style="color:#a6e22e">topN</span>], <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// degrade gracefully</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ranked</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">scored</span>, len(<span style="color:#a6e22e">candidates</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">candidates</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ranked</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">scored</span>{<span style="color:#a6e22e">chunk</span>: <span style="color:#a6e22e">candidates</span>[<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">score</span>: <span style="color:#a6e22e">scores</span>[<span style="color:#a6e22e">i</span>]}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sort</span>.<span style="color:#a6e22e">Slice</span>(<span style="color:#a6e22e">ranked</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">j</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ranked</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">score</span> &gt; <span style="color:#a6e22e">ranked</span>[<span style="color:#a6e22e">j</span>].<span style="color:#a6e22e">score</span>
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">Chunk</span>, <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">topN</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">topN</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">ranked</span>); <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">result</span> = append(<span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">ranked</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">chunk</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Again, graceful degradation. If the reranker fails, return the original order truncated to topN. The system should always return something useful.</p>
<h2 id="multi-representation-indexing">Multi-Representation Indexing</h2>
<p>One embedding per document is leaving retrieval quality on the table. For important documents, index multiple representations:</p>
<ul>
<li>The full text (for detail queries)</li>
<li>A concise summary (for broad queries)</li>
<li>Question-like phrasings that the text answers (for direct questions)</li>
</ul>
<p>This widens the retrieval surface without changing the source documents. It&rsquo;s extra indexing work, but the recall improvement on multi-hop queries is substantial. I&rsquo;ve seen it close the gap on questions that basic retrieval missed entirely.</p>
<h2 id="measure-retrieval-separately">Measure Retrieval Separately</h2>
<p>This is the part most teams skip, and it&rsquo;s the most important.</p>
<p>If you only measure end-to-end answer quality, you can&rsquo;t tell whether a bad answer came from bad retrieval or bad generation. You need retrieval-specific metrics:</p>
<ul>
<li><strong>Recall@k</strong>: Did the relevant chunk appear in the top k results?</li>
<li><strong>Precision@k</strong>: What fraction of the top k results were actually relevant?</li>
<li><strong>MRR (Mean Reciprocal Rank)</strong>: How high did the first relevant result rank?</li>
<li><strong>nDCG</strong>: How well-ordered is the full ranking?</li>
</ul>
<p>Build a small eval set &ndash; 50 to 100 query-document pairs where you know which chunks should be retrieved. Run it after every change to chunking, embedding, or search logic. This is the single highest-leverage investment in a RAG system.</p>
<p>I keep these eval sets in the repo alongside the retrieval code. They&rsquo;re as important as unit tests. Maybe more important.</p>
<h2 id="the-full-pipeline">The Full Pipeline</h2>
<p>Putting it all together, the retrieval pipeline for a production RAG system looks like:</p>
<ol>
<li>Expand the query (2-3 reformulations)</li>
<li>Run hybrid search (vector + lexical) for each query variant</li>
<li>Merge results with RRF</li>
<li>Rerank the merged candidates</li>
<li>Return top-k chunks with breadcrumbs</li>
</ol>
<p>Each step is independently testable and independently measurable. When something breaks, you know where to look.</p>
<p>The generation step is almost an afterthought once retrieval is solid. A decent model with the right evidence in context will give you a good answer. A frontier model with the wrong evidence will confidently give you a wrong one.</p>
<p>Fix retrieval first. Everything else follows.</p>
]]></content:encoded></item><item><title>Let AI Write Your First Draft, Not Your Docs</title><link>https://lawzava.com/blog/2024-09-16-technical-documentation-ai/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-09-16-technical-documentation-ai/</guid><description>AI is a decent drafting assistant for technical docs. It&amp;amp;rsquo;s a terrible replacement for ownership.</description><content:encoded><![CDATA[<p>Technical documentation is one of the most undervalued forms of engineering communication. Everyone agrees it matters. Almost nobody prioritizes it. I&rsquo;ve watched this pattern repeat at every company I&rsquo;ve worked with, and the failure mode is always the same: docs rot because nobody owns them.</p>
<p>AI won&rsquo;t fix that problem. But it can remove the excuse.</p>
<h2 id="the-drafting-problem">The Drafting Problem</h2>
<p>The hardest part of writing docs is getting started. A blank page plus a busy engineer usually means no documentation. AI is genuinely good at solving this specific problem. Feed it the code structure, recent PRs, and changelogs, and you can get a usable first draft in minutes instead of hours.</p>
<p>That draft will be wrong in places. It will miss context. It will occasionally hallucinate an API parameter that doesn&rsquo;t exist. That&rsquo;s fine. A wrong draft you can edit is still faster than a correct document nobody writes.</p>
<h2 id="where-it-falls-apart">Where It Falls Apart</h2>
<p>The moment you treat AI output as finished documentation, you&rsquo;ve created something worse than no documentation at all. Wrong docs train people to distrust all docs. I&rsquo;ve seen this happen: a team auto-generates reference pages, skips review, and six months later nobody believes anything in the docs. They go straight to the source code. The docs become decoration.</p>
<p>The fix is dead simple: AI drafts, humans review, same PR as the code change. No separate workflow. No &ldquo;we&rsquo;ll update the docs later.&rdquo; If the docs don&rsquo;t land in the same review cycle as the code, they&rsquo;ll drift. This isn&rsquo;t a tooling problem. It&rsquo;s a discipline problem.</p>
<h2 id="the-search-use-case">The Search Use Case</h2>
<p>The other place AI helps is doc search. A retrieval-backed answer system that points users to the right section &ndash; with citations &ndash; is genuinely useful. The key constraint: it should refuse to answer when it can&rsquo;t find supporting material. &ldquo;I don&rsquo;t know, but here&rsquo;s the closest section&rdquo; is a better answer than a confident fabrication.</p>
<p>I&rsquo;ve been setting this up across a few projects and the pattern holds. Grounded search with citations works. Generative answers without grounding don&rsquo;t.</p>
<h2 id="what-i-would-actually-do">What I Would Actually Do</h2>
<p>If I were starting a docs workflow today:</p>
<ul>
<li>Generate first drafts from code context. Edit for accuracy and tone before merging.</li>
<li>Block releases when critical docs are stale. Make it a CI check if you have to.</li>
<li>Keep docs in the repo. Same review, same merge, same ownership.</li>
<li>Add retrieval-backed search with citation links. Refuse when unsupported.</li>
</ul>
<p>None of this is complicated. The tooling exists. The gap is always ownership and review discipline, not technology. AI makes the drafting faster. It doesn&rsquo;t make the caring automatic.</p>
]]></content:encoded></item><item><title>AI-Assisted Code Migration: What Actually Works</title><link>https://lawzava.com/blog/2024-09-02-ai-code-migration/</link><pubDate>Mon, 02 Sep 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-09-02-ai-code-migration/</guid><description>I used LLMs to help migrate a 200K-line Go codebase. The mechanical parts went fast. Everything else was still hard.</description><content:encoded><![CDATA[<p>Last quarter I helped a team migrate a large Go codebase from an internal HTTP framework to standard library patterns: around 200K lines across 40+ services. It was the kind of project where you know the end state, you know the transformation rules, and the work is 90% mechanical and 10% judgment calls that keep you up at night.</p>
<p>We used LLMs to handle the mechanical 90%. It worked. But &ldquo;it worked&rdquo; comes with enough caveats that it&rsquo;s worth being honest about what actually happened.</p>
<h2 id="what-the-ai-was-good-at">What the AI was good at</h2>
<p>Pattern matching and consistent transformation are the sweet spot. We had about 15 distinct patterns that needed to change: custom route handlers to standard ones, middleware signatures, and error response formats. For each pattern, we wrote a clear transformation rule with before/after examples.</p>
<p>The LLM could take a file, identify which patterns were present, and produce a transformed version. For straightforward cases, it was faster than any human and more consistent. It didn&rsquo;t get bored on file 200. It didn&rsquo;t introduce typos. It applied the same transformation rule the same way every time.</p>
<p>We processed about 300 files in two days that would have taken two engineers a couple of weeks. The mechanical savings were real.</p>
<h2 id="what-the-ai-was-bad-at">What the AI was bad at</h2>
<p>Judgment. The 10% of cases that didn&rsquo;t fit neatly into the transformation rules required understanding intent, not just pattern matching: a handler that looked standard but had a subtle side effect; a middleware chained in an unusual order for a specific reason; error handling intentionally different from the standard pattern because of a business rule documented nowhere except a Slack thread from 2021.</p>
<p>The LLM would happily transform these cases using the standard rules. The output would compile. The tests would pass. And the behavior would be subtly wrong in ways that only surfaced under specific conditions.</p>
<p>This is the dangerous part. AI-generated code that&rsquo;s almost right is harder to catch than code that&rsquo;s obviously wrong. It passes automated checks and casual review. Then you find the bug three weeks later when a customer reports something weird.</p>
<h2 id="the-workflow-that-worked">The workflow that worked</h2>
<p>Here&rsquo;s what we settled on after the first batch of surprises:</p>
<p><strong>Step 1: Scope with samples.</strong> Don&rsquo;t start with &ldquo;migrate everything.&rdquo; Pick 10 representative files that cover the range of patterns. Run them through the LLM. Review the output manually. This reveals the transformation rules you need and the edge cases you&rsquo;ll need to handle differently.</p>
<p><strong>Step 2: One rule per pattern.</strong> Write each transformation rule explicitly. Not &ldquo;update the HTTP handlers,&rdquo; but &ldquo;replace <code>framework.Handler(func(ctx *Ctx) error {...})</code> with <code>http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {...})</code> and move error handling to&hellip;&rdquo; The more specific the rule, the better the LLM follows it.</p>
<p><strong>Step 3: Small batches, continuous validation.</strong> We processed 10-20 files at a time. After each batch: run the build, run the tests, run the linter, and do a quick diff review. If something broke, fix it and update the transformation rule before continuing. Don&rsquo;t accumulate 200 files of changes and then try to debug a test failure.</p>
<p><strong>Step 4: Flag the hard ones.</strong> When the LLM produced a transformation that looked different from the standard pattern, we flagged it for human review instead of forcing it through. About 15% of files got flagged. Those were the ones where the AI saved us no time at all &ndash; but catching them early saved us from a lot of pain later.</p>
<h2 id="treat-ai-output-as-draft-code">Treat AI output as draft code</h2>
<p>This is the principle that made the whole process work. Every AI-generated change went through the same review process as a human-written change. Same CI checks. Same code review. Same approval workflow.</p>
<p>The temptation is to trust the AI more because it&rsquo;s consistent and fast. Resist that temptation. The AI is a junior engineer who types incredibly fast and never pushes back on your instructions. That&rsquo;s useful. It isn&rsquo;t the same as reliable.</p>
<h2 id="what-id-do-differently">What I&rsquo;d do differently</h2>
<p>I&rsquo;d build the evaluation harness first. We started the migration, then realized we didn&rsquo;t have a good way to verify that migrated services behaved identically to the originals. We retrofitted integration tests, but it would have been faster to invest that time upfront.</p>
<p>I&rsquo;d also version the transformation rules alongside the code. We iterated on the rules as we discovered edge cases, but we didn&rsquo;t track which version of the rules produced which batch of changes. When we found a bug, tracing it back to the specific rule version that caused it was harder than it should have been.</p>
<h2 id="the-honest-summary">The honest summary</h2>
<p>AI made a two-month migration take three weeks. That&rsquo;s a genuine win. But it didn&rsquo;t change the nature of the hard parts. Scoping, validation, edge case handling, and human judgment on ambiguous cases &ndash; those are still the bottleneck. The AI accelerated the parts that were already straightforward.</p>
<p>Use AI for migrations. Just don&rsquo;t pretend it replaces the discipline that makes migrations safe.</p>
]]></content:encoded></item><item><title>How I Actually Test LLM Features</title><link>https://lawzava.com/blog/2024-08-19-llm-testing-strategies/</link><pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-08-19-llm-testing-strategies/</guid><description>LLM outputs are non-deterministic. That doesn&amp;amp;rsquo;t mean you can&amp;amp;rsquo;t test them rigorously. Here&amp;amp;rsquo;s the layered testing approach I use in production.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Test LLM features in layers: deterministic checks for everything around the model (parsing, validation, prompt rendering), property-based checks for model outputs (format, required fields, safety), and a curated golden set for regression detection. Don&rsquo;t test exact string matches. Test the properties that matter to users.</p>
<hr>
<p>The first time I shipped an LLM feature without a proper test suite, we spent three weeks arguing about whether the quality had regressed after a prompt change. Nobody had baseline numbers. Nobody had a definition of &ldquo;good.&rdquo; We were debugging by vibes.</p>
<p>Never again.</p>
<p>LLM testing is different from traditional software testing, but it isn&rsquo;t impossible. It requires accepting that you&rsquo;re testing probabilistic behavior and building your strategy around that reality instead of fighting it.</p>
<h2 id="the-problem-with-llm-outputs">The problem with LLM outputs</h2>
<p>Three things make LLM testing hard:</p>
<ol>
<li><strong>Non-determinism.</strong> The same input can produce different outputs across runs, even with temperature set to zero (some providers still have variance).</li>
<li><strong>Multiple valid answers.</strong> For most tasks, there isn&rsquo;t one correct answer. There&rsquo;s a space of acceptable answers.</li>
<li><strong>Invisible regressions.</strong> A prompt change or model update can shift behavior without any code change. Your CI pipeline sees green. Your users see worse outputs.</li>
</ol>
<p>The instinct is to throw up your hands and say &ldquo;we can&rsquo;t test this.&rdquo; That&rsquo;s wrong. You can test this. You just can&rsquo;t use <code>assertEqual</code>.</p>
<h2 id="layer-1-deterministic-tests-for-everything-around-the-model">Layer 1: deterministic tests for everything around the model</h2>
<p>The code around the LLM &ndash; prompt rendering, response parsing, validation, error handling &ndash; is deterministic. Test it like normal software.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestPromptRendering</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tmpl</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewSupportPrompt</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tmpl</span>.<span style="color:#a6e22e">Render</span>(<span style="color:#a6e22e">PromptInput</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CustomerName</span>: <span style="color:#e6db74">&#34;Alice&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Issue</span>:        <span style="color:#e6db74">&#34;billing dispute&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">History</span>:      []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;previous contact on 2024-07-15&#34;</span>},
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;render failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">result</span>, <span style="color:#e6db74">&#34;Alice&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;prompt should contain customer name&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">result</span>, <span style="color:#e6db74">&#34;billing dispute&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;prompt should contain issue description&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">result</span>, <span style="color:#e6db74">&#34;2024-07-15&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;prompt should contain interaction history&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestResponseParsing</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">raw</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">`{&#34;action&#34;: &#34;escalate&#34;, &#34;reason&#34;: &#34;billing dispute over $500&#34;, &#34;priority&#34;: &#34;high&#34;}`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ParseSupportResponse</span>([]byte(<span style="color:#a6e22e">raw</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;parse failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Action</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;escalate&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;expected action=escalate, got %s&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Action</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Priority</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;high&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;expected priority=high, got %s&#34;</span>, <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Priority</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>These tests are fast, stable, and catch a surprising number of regressions. I&rsquo;ve seen parsing bugs slip through because teams only tested the happy path, then the model started returning JSON with trailing commas.</p>
<p>Also test mocked LLM responses to verify error handling and orchestration logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestHandlesModelTimeout</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">MockLLMClient</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Response</span>: <span style="color:#66d9ef">nil</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Err</span>:      <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">DeadlineExceeded</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">handler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewSupportHandler</span>(<span style="color:#a6e22e">client</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handler</span>.<span style="color:#a6e22e">Handle</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;test query&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatal</span>(<span style="color:#e6db74">&#34;handler should not propagate model timeout as error&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">Fallback</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">true</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;should trigger fallback on timeout&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="layer-2-property-based-checks-for-model-outputs">Layer 2: property-based checks for model outputs</h2>
<p>You can&rsquo;t check that the model said &ldquo;I apologize for the inconvenience.&rdquo; You can check that the response acknowledges the issue, avoids profanity, and stays under 200 words.</p>
<p>Define a rubric. Keep it simple.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EvalCriteria</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Check</span>   <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">output</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">supportResponseCriteria</span> = []<span style="color:#a6e22e">EvalCriteria</span>{
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;acknowledges_issue&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Check</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">output</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">lower</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">ToLower</span>(<span style="color:#a6e22e">output</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;sorry&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;understand&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;apologize&#34;</span>)
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;includes_next_steps&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Check</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">output</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">lower</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">ToLower</span>(<span style="color:#a6e22e">output</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;will&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;next&#34;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Contains</span>(<span style="color:#a6e22e">lower</span>, <span style="color:#e6db74">&#34;follow up&#34;</span>)
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;reasonable_length&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Check</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">output</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">words</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">output</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> len(<span style="color:#a6e22e">words</span>) <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">20</span> <span style="color:#f92672">&amp;&amp;</span> len(<span style="color:#a6e22e">words</span>) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>These aren&rsquo;t perfect. The string matching is crude. But they catch common failure modes: responses that ignore the user&rsquo;s problem, responses that are empty or absurdly long, and responses that miss expected elements.</p>
<p>For more nuanced checks &ndash; tone, factual accuracy, coherence &ndash; I use model-based evaluation. Have a separate evaluator model score the output against the rubric. It isn&rsquo;t free, but it&rsquo;s cheaper than human review on every test case and usually more reliable than regex.</p>
<h2 id="layer-3-the-golden-set">Layer 3: the golden set</h2>
<p>A golden set is a curated collection of representative inputs with expected properties. Not expected outputs, expected properties.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GoldenCase</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ID</span>       <span style="color:#66d9ef">string</span>            <span style="color:#e6db74">`json:&#34;id&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Input</span>    <span style="color:#66d9ef">string</span>            <span style="color:#e6db74">`json:&#34;input&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Expected</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;expected&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Example golden case</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// {</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#34;id&#34;: &#34;billing_complaint_042&#34;,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#34;input&#34;: &#34;I was charged twice for my subscription last month&#34;,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#34;expected&#34;: {</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//     &#34;tone&#34;: &#34;empathetic&#34;,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//     &#34;mentions&#34;: &#34;refund OR credit OR billing&#34;,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//     &#34;format&#34;: &#34;paragraph under 150 words&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">//   }</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// }</span>
</span></span></code></pre></div><p>I maintain 30-50 golden cases per feature. They cover common paths, known edge cases, and a few adversarial inputs. I run them weekly and after every prompt or model change.</p>
<p>The golden set is your regression detector. When a prompt change causes three previously passing golden cases to fail, you get a concrete signal that something shifted. No vibes. No arguments. Data.</p>
<h2 id="the-evaluation-cadence-that-works">The evaluation cadence that works</h2>
<p>After trying several approaches, here&rsquo;s what I&rsquo;ve settled on:</p>
<ul>
<li><strong>Every commit:</strong> Run deterministic tests (layer 1). These are in CI and they block merges. Fast, stable, non-negotiable.</li>
<li><strong>Every prompt/model change:</strong> Run the golden set (layer 3) and compare to the previous baseline. If pass rate drops, the change needs review.</li>
<li><strong>Weekly:</strong> Run the full evaluation suite (layers 2 + 3) and track trends. Output a simple report: pass rate by criteria, any new failures, average response length.</li>
<li><strong>After major updates:</strong> Human review of a random sample (~20 cases). Sanity check that the automated evaluation isn&rsquo;t missing something.</li>
</ul>
<p>This takes about two hours a week of human time. That&rsquo;s a small investment for the confidence it provides.</p>
<h2 id="what-i-wish-more-teams-did">What I wish more teams did</h2>
<p><strong>Version your prompts.</strong> Every prompt change should be a tracked commit with a diff. When quality regresses, you need to know which prompt version caused it. I keep prompts in version-controlled files, not in application code.</p>
<p><strong>Track quality over time.</strong> A single evaluation run is a snapshot. A time series of evaluation results shows trends. Is quality gradually degrading? Did a model provider update cause a step change? You can&rsquo;t answer these without historical data.</p>
<p><strong>Test adversarial inputs.</strong> Your golden set should include attempts to jailbreak, confuse, or extract system prompts. These aren&rsquo;t hypothetical attacks. They&rsquo;re things real users will try.</p>
<p>LLM testing isn&rsquo;t about proving the model is correct. It&rsquo;s about building enough evidence that the system behaves acceptably across the inputs that matter. Layers, properties, golden sets, and a consistent cadence. That&rsquo;s the strategy.</p>
]]></content:encoded></item><item><title>The Best Model Is the Smallest One That Works</title><link>https://lawzava.com/blog/2024-08-05-small-models-big-impact/</link><pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-08-05-small-models-big-impact/</guid><description>Everyone reaches for GPT-4 by default. Most production tasks don&amp;amp;rsquo;t need it. Small models are faster, cheaper, and often better when the task is well-defined.</description><content:encoded><![CDATA[<p>The default instinct when building with LLMs is to reach for the biggest model available. I get it. When you don&rsquo;t know exactly what you need, the biggest model feels like the safest bet. But &ldquo;safest bet&rdquo; and &ldquo;right choice&rdquo; are not the same thing.</p>
<p>Most production LLM tasks I see are classification, extraction, formatting, and short generation. Intent routing for a support bot. Extracting structured data from emails. Labeling inbound requests. These don&rsquo;t need GPT-4 or Claude Opus. They need a model that&rsquo;s fast, cheap, and predictable.</p>
<p>A small model running a well-scoped task will beat a large model running a vague one. Every time.</p>
<h2 id="where-small-wins">Where small wins</h2>
<p>Small models shine when the output space is narrow and the success criteria are clear. If you can describe the correct answer format in one sentence, a small model can probably handle it: classification with a fixed label set, entity extraction with a defined schema, or reformatting text from one structure to another.</p>
<p>The advantages are not marginal. A Haiku-class model might respond in 200ms at a fraction of a cent per request. The same task on a frontier model might take 2 seconds and cost 10x more. At scale, that difference is the gap between a sustainable product and one that burns through runway.</p>
<p>I switched an intent router from GPT-4 to a small model last month. Accuracy stayed within 1%. Latency dropped 80%. Monthly inference cost dropped from $12K to under $2K. The engineering effort was two days of prompt tuning and evaluation.</p>
<h2 id="where-small-fails">Where small fails</h2>
<p>Small models fall apart when the task requires multi-step reasoning, nuanced judgment, or long-form coherence. If you need a model to read a 10-page contract and identify three specific risks, it will miss things. If you need it to write a persuasive email that matches a specific executive&rsquo;s tone, it will usually produce something generic.</p>
<p>The failure mode is subtle. Small models don&rsquo;t refuse &ndash; they confidently produce mediocre output. You won&rsquo;t see errors. You&rsquo;ll see output that&rsquo;s 80% right and 20% subtly wrong in ways that are hard to catch without careful evaluation.</p>
<h2 id="the-routing-pattern">The routing pattern</h2>
<p>The most cost-effective architecture I&rsquo;ve built is a two-tier system. Small model handles the 90% of requests that are well-scoped and predictable. Large model handles the 10% that need depth.</p>
<p>Route by complexity, not by topic. A billing question that maps to one of five categories goes to the small model. A billing dispute that requires reading context and making a judgment call goes to the large model. The router itself can be a small model &ndash; it&rsquo;s just a classification task.</p>
<p>This is not novel. It is the same pattern as having junior engineers handle tickets and escalating to seniors. The model is the same. The economics are the same. Route smart, spend less.</p>
<h2 id="pick-the-smallest-model-that-clears-the-bar">Pick the smallest model that clears the bar</h2>
<p>Don&rsquo;t start with the biggest model and optimize later. Start with the smallest model and prove it&rsquo;s insufficient before upgrading. You&rsquo;ll be surprised how often &ldquo;insufficient&rdquo; never arrives.</p>
<p>The best model isn&rsquo;t the smartest one. It&rsquo;s the smallest one that meets your quality bar, at a cost and latency you can sustain.</p>
]]></content:encoded></item><item><title>Stop Stuffing Your Context Window</title><link>https://lawzava.com/blog/2024-07-22-context-window-strategies/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-07-22-context-window-strategies/</guid><description>Bigger context windows aren&amp;amp;rsquo;t an excuse to stop thinking about what goes into them. Most teams are paying for irrelevant tokens and wondering why quality degrades.</description><content:encoded><![CDATA[<p>I&rsquo;m tired of seeing teams dump entire documents into a context window because &ldquo;it supports 128K tokens now,&rdquo; then wonder why the model ignores their instructions. A bigger window isn&rsquo;t a bigger brain. It&rsquo;s a bigger inbox. And like any inbox, when you fill it with noise, important things get lost.</p>
<p>This is a rant. But it&rsquo;s a rant with actionable advice.</p>
<h2 id="the-just-throw-it-all-in-fallacy">The &ldquo;just throw it all in&rdquo; fallacy</h2>
<p>Here&rsquo;s what I keep seeing: a team builds a RAG pipeline that retrieves 20 document chunks for every query. They concatenate everything into the prompt because &ldquo;more context is better.&rdquo; The model now has 80K tokens of input, 60K of them irrelevant. The response is slower, more expensive, and, this is the part that kills me, lower quality than if they had sent 5K tokens of relevant context.</p>
<p>Retrieval isn&rsquo;t free just because the window is big enough to hold it. Every irrelevant token dilutes the signal. The model has to figure out which parts of the context actually matter, and it isn&rsquo;t always good at that, especially when the relevant information is sandwiched between walls of noise.</p>
<p>I reviewed a system where they were spending $400/day on inference. We cut their context budget by 70%, and quality went up. Not down. Up. The model could finally see the signal instead of drowning in noise.</p>
<h2 id="budget-your-context-like-you-budget-your-infrastructure">Budget your context like you budget your infrastructure</h2>
<p>You wouldn&rsquo;t provision 10x the compute you need and call it a day. Don&rsquo;t do it with context either.</p>
<p>Set a hard budget per request. Something like:</p>
<ul>
<li>System prompt: 1-2K tokens (this should be stable and tight)</li>
<li>Retrieved context: 3-5K tokens max (be aggressive about relevance filtering)</li>
<li>Conversation history: 2-4K tokens (recent turns verbatim, older turns summarized)</li>
<li>Reserve: 1K tokens (for the model&rsquo;s response and any overhead)</li>
</ul>
<p>That&rsquo;s 7-12K tokens for most requests. Not 128K. Not even close. And for 90% of production use cases, that&rsquo;s more than enough.</p>
<p>Teams using 128K tokens per request are either doing something genuinely complex (document analysis, long-form generation) or being lazy. Mostly the latter.</p>
<h2 id="anchors-the-stuff-that-must-never-fall-out">Anchors: the stuff that must never fall out</h2>
<p>Some information is non-negotiable. The user&rsquo;s permissions. The current task definition. Key constraints. Explicit decisions made earlier in the conversation. I call these &ldquo;anchors.&rdquo;</p>
<p>Anchors go at the top of the context, every time. They don&rsquo;t get summarized. They don&rsquo;t get rotated out. They&rsquo;re the ground truth that the model needs to respect regardless of how long the conversation gets.</p>
<p>I&rsquo;ve debugged conversations where the model contradicted an earlier decision because the decision was in a turn that got summarized away. The summary said &ldquo;the user chose option A&rdquo; but the model treated it as a suggestion, not a commitment. Anchors prevent this.</p>
<h2 id="summaries-need-maintenance">Summaries need maintenance</h2>
<p>Speaking of summaries: if you&rsquo;re compressing conversation history into summaries, you need to refresh them. A summary generated 20 turns ago may be inaccurate or incomplete relative to the current state of the conversation.</p>
<p>The pattern I use is simple: keep the last 3-5 turns verbatim. Everything before that gets summarized. Refresh the summary every 10 turns or whenever a significant decision changes. It&rsquo;s a small amount of extra work, and it prevents a category of bugs that&rsquo;s extremely difficult to diagnose.</p>
<h2 id="retrieval-is-a-precision-problem-not-a-recall-problem">Retrieval is a precision problem, not a recall problem</h2>
<p>Most RAG implementations err on the side of including too much. The logic goes: &ldquo;better to include something irrelevant than to miss something important.&rdquo; That sounds reasonable until you look at the actual failure modes.</p>
<p>From what I&rsquo;ve seen, the most common production failure isn&rsquo;t &ldquo;the model didn&rsquo;t have enough context.&rdquo; It&rsquo;s &ldquo;the model had too much context and picked the wrong information.&rdquo; Over-retrieval causes the model to confidently cite irrelevant passages while ignoring the one paragraph that actually answers the question.</p>
<p>Retrieve less. Filter aggressively. If you aren&rsquo;t sure a chunk is relevant, leave it out. The model can ask follow-up questions. It can&rsquo;t unsee irrelevant context.</p>
<h2 id="the-real-problem-is-that-nobody-measures-this">The real problem is that nobody measures this</h2>
<p>Most teams have no idea how their context utilization looks in production. They don&rsquo;t track average context size, the ratio of relevant to irrelevant tokens, or the correlation between context size and output quality. They just set a max limit and hope for the best.</p>
<p>Instrument your context pipeline. Log the size of each section (system prompt, retrieved context, history, anchors). Track output quality as a function of context size. You&rsquo;ll almost certainly discover that your sweet spot is much smaller than your current usage.</p>
<p>Bigger windows are a genuine improvement. They let you handle tasks that were impossible before. But for most production workloads, the discipline of managing context well matters more than the ability to stuff more into it.</p>
]]></content:encoded></item><item><title>Function Calling Patterns That Survive Production</title><link>https://lawzava.com/blog/2024-07-08-function-calling-patterns/</link><pubDate>Mon, 08 Jul 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-07-08-function-calling-patterns/</guid><description>Function calling is how LLMs touch real systems. Treat tools like APIs, arguments like untrusted input, and permissions like the model is an intern with root access.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Function calling works in production when you treat it like boring infrastructure: strict schemas, validation at every boundary, explicit permissions, and structured errors. The model isn&rsquo;t trusted code. It&rsquo;s an external caller that happens to speak JSON. Build accordingly.</p>
<hr>
<p>Function calling turned LLMs from text generators into system operators. That&rsquo;s the opportunity and the risk. A model that can create tickets, query databases, and trigger deployments is powerful. A model that does those things with unvalidated arguments and no permission checks is a security incident waiting to happen.</p>
<p>I&rsquo;ve built function calling integrations in past projects &ndash; mostly in Go &ndash; and the patterns that survive production are boring. That&rsquo;s the point. Here&rsquo;s what I&rsquo;ve learned.</p>
<h2 id="the-mental-model">The mental model</h2>
<p>Think of function calling as an API gateway where the caller is an LLM instead of a user. The model sees a list of available tools with schemas, picks one, and returns arguments as JSON. Your backend validates, executes, and returns results. The model then uses the results to continue the conversation.</p>
<pre tabindex="0"><code>User prompt + tool definitions
        |
        v
  Model selects tool + arguments (JSON)
        |
        v
  Backend validates arguments
        |
        v
  Backend executes tool (with permissions)
        |
        v
  Structured result returned to model
        |
        v
  Model generates final response
</code></pre><p>Simple in theory. In practice, the complexity is in validation, permissions, and error handling. That&rsquo;s where most teams cut corners, and where most production incidents start.</p>
<h2 id="tool-definitions-treat-them-like-api-contracts">Tool definitions: treat them like API contracts</h2>
<p>A tool definition is a contract. The model&rsquo;s behavior is only as good as the schema you provide. Vague descriptions produce vague arguments. Loose types produce invalid inputs.</p>
<p>In Go, I define tools as structs with explicit JSON Schema generation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// ToolDef represents a callable tool exposed to the LLM.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ToolDef</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>        <span style="color:#66d9ef">string</span>      <span style="color:#e6db74">`json:&#34;name&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Description</span> <span style="color:#66d9ef">string</span>      <span style="color:#e6db74">`json:&#34;description&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Parameters</span>  <span style="color:#a6e22e">JSONSchema</span>  <span style="color:#e6db74">`json:&#34;parameters&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Handler</span>     <span style="color:#a6e22e">ToolHandler</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Permission</span>  <span style="color:#a6e22e">Permission</span>  <span style="color:#e6db74">`json:&#34;-&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">JSONSchema</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Type</span>       <span style="color:#66d9ef">string</span>                <span style="color:#e6db74">`json:&#34;type&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Properties</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">Property</span>   <span style="color:#e6db74">`json:&#34;properties&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Required</span>   []<span style="color:#66d9ef">string</span>              <span style="color:#e6db74">`json:&#34;required&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Property</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Type</span>        <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`json:&#34;type&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Description</span> <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`json:&#34;description,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Enum</span>        []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;enum,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Default</span>     <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`json:&#34;default,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ToolHandler</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">ToolResult</span>, <span style="color:#66d9ef">error</span>)
</span></span></code></pre></div><p>A concrete example &ndash; a ticket creation tool:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">createTicketTool</span> = <span style="color:#a6e22e">ToolDef</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>:        <span style="color:#e6db74">&#34;create_ticket&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Description</span>: <span style="color:#e6db74">&#34;Create a support ticket. Requires a verified user session.&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Parameters</span>: <span style="color:#a6e22e">JSONSchema</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Type</span>: <span style="color:#e6db74">&#34;object&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Properties</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">Property</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;subject&#34;</span>:  {<span style="color:#a6e22e">Type</span>: <span style="color:#e6db74">&#34;string&#34;</span>, <span style="color:#a6e22e">Description</span>: <span style="color:#e6db74">&#34;Short summary of the issue&#34;</span>},
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;category&#34;</span>: {<span style="color:#a6e22e">Type</span>: <span style="color:#e6db74">&#34;string&#34;</span>, <span style="color:#a6e22e">Enum</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;billing&#34;</span>, <span style="color:#e6db74">&#34;bug&#34;</span>, <span style="color:#e6db74">&#34;account&#34;</span>, <span style="color:#e6db74">&#34;other&#34;</span>}},
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;priority&#34;</span>: {<span style="color:#a6e22e">Type</span>: <span style="color:#e6db74">&#34;string&#34;</span>, <span style="color:#a6e22e">Enum</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;low&#34;</span>, <span style="color:#e6db74">&#34;normal&#34;</span>, <span style="color:#e6db74">&#34;high&#34;</span>}, <span style="color:#a6e22e">Default</span>: <span style="color:#e6db74">&#34;normal&#34;</span>},
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Required</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">&#34;subject&#34;</span>, <span style="color:#e6db74">&#34;category&#34;</span>},
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Handler</span>:    <span style="color:#a6e22e">handleCreateTicket</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Permission</span>: <span style="color:#a6e22e">PermWriteApproval</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notice the pattern: enums on every field with a bounded set of values, a clear description that tells the model when to use the tool, and required fields marked explicitly. The model doesn&rsquo;t guess. It follows the contract.</p>
<h2 id="the-tool-registry">The tool registry</h2>
<p>Centralize tool registration. Don&rsquo;t scatter tool definitions across your codebase. A single registry makes it easy to generate schemas for the model, enforce permissions, and audit what&rsquo;s available.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Registry</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">mu</span>    <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">RWMutex</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">ToolDef</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewRegistry</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Registry</span>{<span style="color:#a6e22e">tools</span>: make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">ToolDef</span>)}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span>) <span style="color:#a6e22e">Register</span>(<span style="color:#a6e22e">tool</span> <span style="color:#a6e22e">ToolDef</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">tools</span>[<span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Name</span>] = <span style="color:#a6e22e">tool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span>) <span style="color:#a6e22e">Schema</span>() []<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">out</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>, <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">tools</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">t</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">tools</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">out</span> = append(<span style="color:#a6e22e">out</span>, <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;function&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;function&#34;</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">any</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;name&#34;</span>:        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Name</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;description&#34;</span>: <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Description</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;parameters&#34;</span>:  <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Parameters</span>,
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">out</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">name</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">ToolResult</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tool</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">tools</span>[<span style="color:#a6e22e">name</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ToolResult</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Success</span>:   <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">ErrorCode</span>: <span style="color:#e6db74">&#34;unknown_tool&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Message</span>:   <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;tool %q not found&#34;</span>, <span style="color:#a6e22e">name</span>),
</span></span><span style="display:flex;"><span>        }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">args</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>Execute</code> method is intentionally minimal. Validation and permission checks happen in the layers around it, not inside the registry itself. Separation of concerns matters here because you&rsquo;ll want to add middleware later without rewriting the registry.</p>
<h2 id="validation-the-model-isnt-trusted">Validation: the model isn&rsquo;t trusted</h2>
<p>This is the hill I&rsquo;ll die on: model-generated arguments are untrusted input. Always. Even with a tight schema, the model can produce unexpected values &ndash; empty strings, null where you expect a value, or fields that technically match the type but are nonsensical.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateTicketArgs</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Subject</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;subject&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Category</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;category&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Priority</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;priority&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">validateCreateTicketArgs</span>(<span style="color:#a6e22e">raw</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">CreateTicketArgs</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">CreateTicketArgs</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>(<span style="color:#a6e22e">raw</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">args</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid JSON: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Subject</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Subject</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Subject</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;subject must be non-empty&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Subject</span>) &gt; <span style="color:#ae81ff">200</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;subject exceeds 200 characters&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">validCategories</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">bool</span>{<span style="color:#e6db74">&#34;billing&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">&#34;bug&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">&#34;account&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">&#34;other&#34;</span>: <span style="color:#66d9ef">true</span>}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">validCategories</span>[<span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Category</span>] {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid category: %q&#34;</span>, <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Category</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Priority</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Priority</span> = <span style="color:#e6db74">&#34;normal&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">validPriorities</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">bool</span>{<span style="color:#e6db74">&#34;low&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">&#34;normal&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">&#34;high&#34;</span>: <span style="color:#66d9ef">true</span>}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">validPriorities</span>[<span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Priority</span>] {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid priority: %q&#34;</span>, <span style="color:#a6e22e">args</span>.<span style="color:#a6e22e">Priority</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">args</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Yes, this is verbose. That&rsquo;s deliberate. I don&rsquo;t want clever one-liners here. I want code that a new team member can read at 3 AM during an incident and immediately understand what it checks and why.</p>
<h2 id="structured-errors-that-the-model-can-recover-from">Structured errors that the model can recover from</h2>
<p>When validation fails, return a structured error the model can act on. Not a stack trace. Not a generic &ldquo;bad request.&rdquo; A clear envelope:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ToolResult</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Success</span>   <span style="color:#66d9ef">bool</span>   <span style="color:#e6db74">`json:&#34;success&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ErrorCode</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;error_code,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Message</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;message,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Data</span>      <span style="color:#66d9ef">any</span>    <span style="color:#e6db74">`json:&#34;data,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The model sees this and can retry with corrected arguments, ask the user for clarification, or explain the failure. Unstructured errors produce unstructured recovery attempts. I&rsquo;ve seen models apologize to users for &ldquo;server errors&rdquo; when the actual problem was a missing required field.</p>
<h2 id="permission-scoping">Permission scoping</h2>
<p>Every tool gets a permission level. Every request carries user context. The execution layer checks permissions before calling the handler. No exceptions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Permission</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">PermReadOnly</span> <span style="color:#a6e22e">Permission</span> = <span style="color:#66d9ef">iota</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">PermWriteApproval</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">PermAdminOnly</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ExecContext</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">UserID</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Role</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SessionID</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span>) <span style="color:#a6e22e">ExecuteWithAuth</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">ec</span> <span style="color:#a6e22e">ExecContext</span>, <span style="color:#a6e22e">name</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">ToolResult</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tool</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">tools</span>[<span style="color:#a6e22e">name</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ToolResult</span>{<span style="color:#a6e22e">Success</span>: <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">ErrorCode</span>: <span style="color:#e6db74">&#34;unknown_tool&#34;</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">hasPermission</span>(<span style="color:#a6e22e">ec</span>.<span style="color:#a6e22e">Role</span>, <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Permission</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ToolResult</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Success</span>:   <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">ErrorCode</span>: <span style="color:#e6db74">&#34;permission_denied&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Message</span>:   <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;role %q cannot execute %q&#34;</span>, <span style="color:#a6e22e">ec</span>.<span style="color:#a6e22e">Role</span>, <span style="color:#a6e22e">name</span>),
</span></span><span style="display:flex;"><span>        }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">args</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">hasPermission</span>(<span style="color:#a6e22e">role</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">required</span> <span style="color:#a6e22e">Permission</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">required</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">PermReadOnly</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">PermWriteApproval</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">role</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;user&#34;</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">role</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;admin&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">PermAdminOnly</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">role</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;admin&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The model doesn&rsquo;t decide permissions. The backend does. This isn&rsquo;t negotiable. I&rsquo;ve seen demos where the model is told &ldquo;you have admin access&rdquo; in the system prompt. That isn&rsquo;t a permission system. That&rsquo;s a suggestion.</p>
<h2 id="parallel-execution-with-guardrails">Parallel execution with guardrails</h2>
<p>Some models support parallel tool calls. This can cut latency significantly when tools are independent, but you still need timeouts and isolation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">executeParallel</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">registry</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Registry</span>, <span style="color:#a6e22e">ec</span> <span style="color:#a6e22e">ExecContext</span>, <span style="color:#a6e22e">calls</span> []<span style="color:#a6e22e">ToolCall</span>) []<span style="color:#f92672">*</span><span style="color:#a6e22e">ToolResult</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">8</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">results</span> <span style="color:#f92672">:=</span> make([]<span style="color:#f92672">*</span><span style="color:#a6e22e">ToolResult</span>, len(<span style="color:#a6e22e">calls</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wg</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">WaitGroup</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">call</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">calls</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">idx</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">c</span> <span style="color:#a6e22e">ToolCall</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Done</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">registry</span>.<span style="color:#a6e22e">ExecuteWithAuth</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">ec</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Arguments</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">idx</span>] = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ToolResult</span>{<span style="color:#a6e22e">Success</span>: <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">ErrorCode</span>: <span style="color:#e6db74">&#34;execution_error&#34;</span>, <span style="color:#a6e22e">Message</span>: <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>()}
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">idx</span>] = <span style="color:#a6e22e">result</span>
</span></span><span style="display:flex;"><span>        }(<span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">call</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Wait</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">results</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The timeout is critical. A slow tool shouldn&rsquo;t block the entire response. Return partial results and let the model work with what it has.</p>
<h2 id="observability">Observability</h2>
<p>Log every tool call. But be smart about what you log:</p>
<ul>
<li>Tool name and version</li>
<li>User ID and session ID</li>
<li>Argument hash (not raw arguments &ndash; those may contain PII)</li>
<li>Success/failure and error code</li>
<li>Execution latency</li>
</ul>
<p>This gives you enough to debug failures, detect drift (is the model suddenly calling a tool it never used before?), and identify tools that are slow, failing, or overused.</p>
<h2 id="what-i-wish-i-had-known-earlier">What I wish I had known earlier</h2>
<p>After building several of these systems, a few lessons stand out:</p>
<p><strong>Keep tool descriptions short and precise.</strong> The model reads them on every request. Long descriptions waste tokens and confuse tool selection. One sentence describing the action, one sentence about when to use it.</p>
<p><strong>Version your tool schemas.</strong> When you change a tool&rsquo;s parameters, the model&rsquo;s behavior will change too. Treat schema changes like API migrations.</p>
<p><strong>Test with adversarial inputs.</strong> Ask the model to call tools with garbage arguments, impossible combinations, and injection attempts. Your validation layer should handle all of these cleanly.</p>
<p>Function calling is the interface between language models and real systems. It works when you treat it like infrastructure: boring, reliable, and well-instrumented. The clever part is the model. Your job is to make the execution layer as predictable as possible.</p>
]]></content:encoded></item><item><title>Claude 3.5 Sonnet Analysis: Cost, Coding, and Model Routing</title><link>https://lawzava.com/blog/2024-06-24-claude-35-sonnet-analysis/</link><pubDate>Mon, 24 Jun 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-06-24-claude-35-sonnet-analysis/</guid><description>Claude 3.5 Sonnet changes model routing math for coding, cost, latency, and production AI workloads.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Claude 3.5 Sonnet is the first mid-tier model I&rsquo;d default to for most production workloads. It matches or beats GPT-4 on coding tasks I care about, costs less, and Artifacts is genuinely useful for iteration. If you&rsquo;re still routing everything to your most expensive model, run a side-by-side comparison. You&rsquo;ll likely save money without losing quality.</p>
<hr>
<p>Anthropic released Claude 3.5 Sonnet alongside a new Artifacts interface, and I&rsquo;ve been running it against my usual workloads for a couple of weeks now. This isn&rsquo;t a benchmark review. Benchmarks tell you how a model performs on someone else&rsquo;s problems. I care about how it performs on mine.</p>
<h2 id="the-positioning-shift-that-matters">The positioning shift that matters</h2>
<p>Every model provider has a lineup: cheap-and-fast at the bottom, expensive-and-smart at the top. The default instinct for production teams is to reach for the top tier because the cost of a bad output usually outweighs the cost of inference.</p>
<p>Claude 3.5 Sonnet challenges that instinct. Anthropic is explicitly positioning a mid-tier model as the default for serious work. That isn&rsquo;t just a pricing play. It&rsquo;s a claim that the quality gap between tiers has narrowed enough that the mid-tier clears the bar for most real-world tasks. That is the same routing question behind broader AI inference cost trends: which requests actually deserve the expensive path?</p>
<p>I&rsquo;ve been testing this claim. Here is what stood out.</p>
<h2 id="coding-where-it-actually-impressed-me">Coding: where it actually impressed me</h2>
<p>I ran Sonnet through the types of coding tasks I deal with in my Go-heavy workflow:</p>
<p><strong>Multi-file refactors.</strong> I asked it to rename a package, update all references, and adjust the tests. Sonnet got this right on the first try, including edge cases in test helper files that GPT-4 had missed when I ran the same task a month earlier.</p>
<p><strong>Bug diagnosis from error traces.</strong> I pasted a stack trace from a concurrency bug in a Go service. Sonnet identified the race condition, explained why it manifested only under load, and proposed a fix using <code>sync.Mutex</code> that was correct and idiomatic. It didn&rsquo;t suggest <code>sync.Map</code> when a plain mutex was the right call. That kind of judgment matters.</p>
<p><strong>Documentation from code.</strong> I gave it a 200-line Go package and asked for a README. The output was usable with minor edits. It captured the intent, not just the function signatures.</p>
<p>These are the tasks where I spend real time. A model that handles them reliably at a lower price point changes how I think about routing.</p>
<h2 id="where-it-falls-short">Where it falls short</h2>
<p>Sonnet isn&rsquo;t magic. I found its limits in a few predictable places:</p>
<p><strong>Long-form reasoning across large contexts.</strong> When I loaded a full design document (~15K tokens) and asked for a critique, Sonnet&rsquo;s analysis was surface-level compared to Opus. It identified structural issues but missed a subtle consistency problem that Opus caught.</p>
<p><strong>Ambiguous instructions.</strong> When the prompt is vague, Sonnet tends to make reasonable but sometimes wrong assumptions instead of asking for clarification. This is manageable &ndash; you just need more explicit prompts &ndash; but it means you can&rsquo;t be lazy with your instructions.</p>
<p><strong>Creative writing.</strong> Not my primary use case, but I noticed it. Sonnet&rsquo;s prose is competent but flat. If you need compelling narrative or nuanced tone, Opus is still noticeably better.</p>
<h2 id="artifacts-more-useful-than-i-expected">Artifacts: more useful than I expected</h2>
<p>I was skeptical of Artifacts when I saw the announcement. It looked like a UI gimmick. After using it for two weeks, I changed my mind.</p>
<p>The core idea: when the model produces code, a document, or a visualization, it renders it in a separate panel instead of inline in chat. You can edit it, iterate on it, and share it. The model treats it as a persistent object in the conversation.</p>
<p>Where this is genuinely useful:</p>
<ul>
<li><strong>Prototyping UI components.</strong> Ask for a React component, see it rendered, ask for changes, see the update. The feedback loop is fast.</li>
<li><strong>Drafting specs.</strong> The artifact is a living document that you refine through conversation. Much better than scrolling through a chat history to find the latest version.</li>
<li><strong>Quick visualizations.</strong> SVG diagrams, simple charts, Mermaid flowcharts. The inline render makes iteration practical.</li>
</ul>
<p>This isn&rsquo;t a paradigm shift, but it is a genuine workflow improvement for anyone using an LLM for iterative creation.</p>
<h2 id="how-id-evaluate-this-for-your-team">How I&rsquo;d evaluate this for your team</h2>
<p>Don&rsquo;t take my word for it. Run your own comparison. Here&rsquo;s the approach I recommend:</p>
<ol>
<li><strong>Pick 10-15 real tasks</strong> from your last two sprints. Not toy problems &ndash; actual things your team spent time on. Code reviews, bug fixes, documentation, data analysis.</li>
<li><strong>Run them through Sonnet and your current default model</strong> side by side. Same prompts, same context.</li>
<li><strong>Score on three dimensions:</strong> correctness, usefulness (did you use the output or throw it away), and time saved.</li>
<li><strong>Compare cost and latency.</strong> Sonnet should be meaningfully cheaper and faster. If the quality is comparable, the math is obvious.</li>
</ol>
<p>Do this for a week, not an afternoon. First impressions are unreliable. You need enough data points to see the failure modes, not just the wins.</p>
<h2 id="the-model-routing-question">The model routing question</h2>
<p>The real implication of Sonnet isn&rsquo;t &ldquo;use this instead of Opus.&rdquo; It&rsquo;s &ldquo;think in terms of routing.&rdquo;</p>
<p>Most teams use one model for everything. That was reasonable when the quality gap between tiers was large. Now that the gap is narrowing, a smarter approach is to route by task:</p>
<ul>
<li><strong>Sonnet</strong> for coding, classification, extraction, structured output, and most day-to-day work.</li>
<li><strong>Opus</strong> for complex reasoning, nuanced analysis, and tasks where the cost of a wrong answer is high.</li>
<li><strong>Haiku</strong> for preprocessing, filtering, and high-volume tasks where speed matters more than depth.</li>
</ul>
<p>Keep model identifiers in config, not in code. Make routing a configuration decision, not a code change. That way you can shift traffic as models improve without redeploying.</p>
<h2 id="what-matters">What matters</h2>
<p>Claude 3.5 Sonnet is the first mid-tier model where I stopped reaching for the top-tier by default. It handles my actual workloads well, costs less, and the Artifacts feature makes iteration faster.</p>
<p>The right move isn&rsquo;t to blindly switch. It&rsquo;s to test on your workloads, measure the quality gap, and route intelligently. For most teams, that will mean moving a significant chunk of traffic to Sonnet and saving the heavyweight model for the tasks that genuinely need it.</p>
]]></content:encoded></item><item><title>AI Compliance Without the Theater</title><link>https://lawzava.com/blog/2024-06-10-ai-compliance-enterprise/</link><pubDate>Mon, 10 Jun 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-06-10-ai-compliance-enterprise/</guid><description>Compliance doesn&amp;amp;rsquo;t have to slow you down. But you have to build it into the system from day one, not bolt it on after the demo impresses the board.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI compliance is a design problem, not a paperwork problem. Build a data inventory, a model registry, and audit logging before you ship &ndash; not after legal gets involved. The organizations shipping fastest are the ones that treat compliance as architecture, not bureaucracy.</p>
<hr>
<p>My perspective on AI compliance is shaped by two things: working on AI adoption at large enterprises and my work on national cyber-defense. Those are very different worlds, but they share one uncomfortable truth &ndash; organizations that treat security and compliance as an afterthought tend to have the worst incidents and the slowest response times.</p>
<p>In the defense world, you learn quickly that compliance isn&rsquo;t about checking boxes. It&rsquo;s about building systems that can answer hard questions fast. Where did this data come from? Who authorized this action? What changed between yesterday and today? When something goes wrong at 2 AM, nobody cares about your compliance document. They care about whether your systems can provide answers.</p>
<p>That same principle applies to enterprise AI. Just with lower stakes and, unfortunately, less discipline.</p>
<h2 id="the-questions-that-actually-matter">The questions that actually matter</h2>
<p>I&rsquo;ve sat through dozens of compliance reviews for AI systems. They all converge on the same handful of questions:</p>
<ul>
<li>Where does user data go during inference, and is any of it retained?</li>
<li>Can you trace a specific output back to the model version and prompt that produced it?</li>
<li>How do you detect and handle unsafe, biased, or hallucinated outputs?</li>
<li>Who approved this use case, and what risk assessment was done?</li>
<li>If the model provider changes their terms or has a breach, what&rsquo;s your exit plan?</li>
</ul>
<p>If your engineering team can&rsquo;t answer these within minutes, you aren&rsquo;t ready for production. Full stop. I&rsquo;ve seen AI projects delayed six months because the team couldn&rsquo;t explain their data flow to a procurement review. That isn&rsquo;t a compliance problem. That&rsquo;s a design problem.</p>
<h2 id="data-governance-is-the-foundation">Data governance is the foundation</h2>
<p>Start with a data inventory. Not a theoretical one &ndash; a real, maintained list of what data enters your AI pipeline, how it&rsquo;s classified, where it&rsquo;s processed, and when it&rsquo;s deleted.</p>
<p>This sounds basic. It is. Most teams still skip it because it&rsquo;s boring. Then, three months in, they discover their LLM provider&rsquo;s terms allow training on API inputs, and they&rsquo;ve been sending customer PII through an endpoint with no data processing agreement.</p>
<p>From my national cyber-defense experience: you don&rsquo;t get to decide what data classification matters after the incident. You decide before. The same applies here. Know your data flows. Classify them. Enforce the policies technically, not just on paper.</p>
<h2 id="model-accountability-isnt-optional">Model accountability isn&rsquo;t optional</h2>
<p>You need a model registry. Every inference in production should be traceable to a specific model version, a specific prompt version, and a specific configuration. This isn&rsquo;t overengineering. This is the minimum bar for debugging, incident response, and regulatory compliance.</p>
<p>What to log for each request:</p>
<ul>
<li>A stable request ID</li>
<li>Model identifier and version</li>
<li>Prompt template version</li>
<li>A hash or summary of the output (not the raw output if it contains sensitive content)</li>
<li>Timestamp, user context, and latency</li>
</ul>
<p>In the defense space, we call this &ldquo;chain of custody for decisions.&rdquo; In enterprise AI, it&rsquo;s just good engineering. I&rsquo;m still surprised by how many teams ship without it.</p>
<h2 id="human-oversight-that-actually-works">Human oversight that actually works</h2>
<p>The compliance frameworks I&rsquo;ve seen fail are the ones that require human approval for everything. That doesn&rsquo;t scale. It creates bottlenecks, and people start rubber-stamping just to keep velocity.</p>
<p>Better approach: tier your use cases by risk.</p>
<p><strong>Low risk</strong> (internal tools, human-reviewed outputs): self-service approval, lightweight monitoring. A team lead signs off and you move on.</p>
<p><strong>Medium risk</strong> (customer-facing, influences decisions): security review, data assessment, defined rollback plan. One meeting, not a committee.</p>
<p><strong>High risk</strong> (financial, medical, safety-critical): full review cycle with legal, security, and domain experts. No shortcuts, but a defined timeline.</p>
<p>The goal is to make the approval path proportional to the risk. Low-risk use cases should ship in days, not weeks. High-risk use cases should have rigor, not paralysis.</p>
<h2 id="vendor-risk-is-your-risk">Vendor risk is your risk</h2>
<p>Every AI provider you use is a critical dependency. Treat it that way. I&rsquo;ve reviewed vendor contracts where data-handling terms were buried in an appendix nobody on the engineering team had read.</p>
<p>Key questions for any AI vendor:</p>
<ul>
<li>Is customer data used for model training? Can you opt out?</li>
<li>What&rsquo;s the breach notification timeline?</li>
<li>What happens to your data if you terminate the contract?</li>
<li>Can you run the same workload on a different provider if needed?</li>
</ul>
<p>Lock-in is a compliance risk. If your only option is one provider and they change their terms or have a major incident, you need a plan B that doesn&rsquo;t require rewriting your entire pipeline.</p>
<h2 id="three-artifacts-you-actually-need">Three artifacts you actually need</h2>
<p>Forget the 50-page compliance documents. Maintain three living artifacts:</p>
<ol>
<li><strong>System card.</strong> One page per AI system: what it does, what data it touches, known limitations, risk tier, and owner.</li>
<li><strong>Data inventory.</strong> Where data comes from, where it goes, classification, retention, and deletion procedures.</li>
<li><strong>Model registry.</strong> Model versions in production, evaluation results, prompt versions, and deployment history.</li>
</ol>
<p>Keep them in version control, not in a shared drive nobody checks. Review them quarterly, or whenever the model or data pipeline changes.</p>
<h2 id="the-real-competitive-advantage">The real competitive advantage</h2>
<p>The enterprises shipping AI fastest right now aren&rsquo;t the ones ignoring compliance. They&rsquo;re the ones that built it into their architecture early, kept it lightweight, and made it a development practice instead of a legal review.</p>
<p>Compliance built into the system is invisible. Compliance bolted on afterward is a project that never ends.</p>
]]></content:encoded></item><item><title>Why Your Enterprise AI Pilot Is Stuck</title><link>https://lawzava.com/blog/2024-06-03-enterprise-ai-adoption/</link><pubDate>Mon, 03 Jun 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-06-03-enterprise-ai-adoption/</guid><description>Most enterprise AI projects die between the demo and production. The blockers aren&amp;amp;rsquo;t technical &amp;amp;ndash; they&amp;amp;rsquo;re organizational. Here&amp;amp;rsquo;s what I keep seeing.</description><content:encoded><![CDATA[<p>Every enterprise AI conversation I&rsquo;ve had this year follows the same arc. Someone builds a proof of concept. The demo goes well. Leadership gets excited. Then, three months later, the project is stuck in limbo: security reviews, data access requests, and nobody quite sure who actually owns it.</p>
<p>I see this pattern across telecom and fintech organizations. The demo-to-production gap isn&rsquo;t a technology problem. It&rsquo;s an organizational one.</p>
<h2 id="the-demo-was-the-easy-part">The demo was the easy part</h2>
<p>A POC can skip everything that makes enterprise software hard. It runs on a developer&rsquo;s laptop with test data. It doesn&rsquo;t need to handle real user volumes. During a demo, nobody asks about audit trails or data retention policies.</p>
<p>Then the project moves toward production and reality hits. Security wants a threat model. Legal wants to know where the data goes. The platform team wants to know who pays for compute. The data science team discovers the training data is messier than expected. None of this is surprising. These are the same problems every enterprise system faces, plus a few new AI-specific ones: model drift, prompt management, and probabilistic outputs.</p>
<p>The teams that get stuck are the ones that treated the POC as the starting line instead of a feasibility check.</p>
<h2 id="start-boring-stay-boring">Start boring, stay boring</h2>
<p>The single best predictor of success I&rsquo;ve seen is picking a first use case that&rsquo;s low-risk and internal. Something where a human reviews the output before anything happens. Document summarization for internal teams. Draft generation for support responses that get edited before sending. Classification of inbound requests to route them to the right queue.</p>
<p>These aren&rsquo;t exciting. That&rsquo;s the point. You want a use case where a bad output is an inconvenience, not a liability. One where you can iterate on prompts and evaluate quality without a customer ever seeing an unpolished result.</p>
<p>I keep telling teams the same thing: your first AI feature should be invisible to customers. Ship it internally, prove it works, build the muscle memory for operating AI in production, then expand.</p>
<h2 id="build-the-platform-before-the-pilots-multiply">Build the platform before the pilots multiply</h2>
<p>Here&rsquo;s what happens when you don&rsquo;t have a shared platform: every team builds its own integration. They pick different models, prompt patterns, and logging approaches. Six months later, you have eight AI features and no way to compare quality, manage costs, or enforce policies across them.</p>
<p>The fix is unglamorous. Build a thin shared layer early. It needs three things:</p>
<ol>
<li><strong>Centralized model access</strong> with authentication, rate limiting, and cost tracking.</li>
<li><strong>A prompt registry</strong> so prompts are versioned, reviewable, and not buried in application code.</li>
<li><strong>Evaluation tooling</strong> that every team can use to measure output quality against a golden set.</li>
</ol>
<p>This doesn&rsquo;t need to be perfect or fully featured. It needs to exist before the third team starts building their own AI integration. I&rsquo;ve watched organizations try to consolidate after the fact. It&rsquo;s painful and expensive.</p>
<h2 id="governance-that-enables-instead-of-blocks">Governance that enables instead of blocks</h2>
<p>The worst governance models I see are designed by committee without input from the engineering teams that have to live with them. They produce a 40-page policy document, a six-week review cycle, and a strong incentive for teams to quietly build things without telling anyone.</p>
<p>Good governance is lightweight and fast. A one-page use case template. A clear risk-tier system: low risk gets self-service approval, high risk gets review. A standing meeting where legal, security, and engineering are in the same room instead of a months-long email chain.</p>
<p>One organization I worked with reduced its AI approval cycle from eight weeks to five days by switching from a document-based review to a 30-minute live walkthrough with all stakeholders. Same rigor. Fraction of the time.</p>
<h2 id="the-uncomfortable-truth">The uncomfortable truth</h2>
<p>Most enterprise AI projects don&rsquo;t fail because the technology isn&rsquo;t ready. They fail because the organization isn&rsquo;t ready. The AI works fine in the demo. The procurement process takes four months. The data team can&rsquo;t provide clean training data. The legal review has no precedent to follow, so it defaults to &ldquo;no&rdquo; until someone escalates.</p>
<p>If you want to ship AI in an enterprise, spend less time evaluating models and more time clearing organizational roadblocks. Get a budget owner. Get a security sponsor. Get data access sorted before you write the first prompt.</p>
<p>Process beats talent. Every time.</p>
]]></content:encoded></item><item><title>Building Voice AI That People Actually Use</title><link>https://lawzava.com/blog/2024-05-27-building-voice-ai/</link><pubDate>Mon, 27 May 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-05-27-building-voice-ai/</guid><description>Voice AI is ready to ship. The hard parts are latency, interruptions, and knowing when voice is the wrong interface. Here&amp;amp;rsquo;s how I approach it.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Voice AI works when you treat it like plumbing, not magic. Keep perceived latency under 500ms, treat interruptions as a first-class concern, and keep the task scope narrow. The architecture choice between a modular pipeline and an end-to-end model matters less than your streaming strategy.</p>
<hr>
<p>The gap between a voice AI demo and a voice AI product is about six months of work on things nobody finds exciting: latency tuning, interruption handling, and figuring out what happens when the user mumbles, changes their mind, or goes silent for eight seconds.</p>
<p>I&rsquo;ve been involved in voice interface projects going back to a travel startup I built, and more recently in voice-first support tools. The models have gotten dramatically better. The engineering around them hasn&rsquo;t kept pace.</p>
<h2 id="two-architectures-one-tradeoff">Two architectures, one tradeoff</h2>
<p>You have two practical options for a voice AI system:</p>
<p><strong>Modular pipeline:</strong> Separate services for transcription, reasoning, and synthesis. You can swap components, instrument each stage, and debug failures in isolation. The cost is latency at every boundary.</p>
<pre tabindex="0"><code>mic -&gt; STT service -&gt; LLM -&gt; TTS service -&gt; speaker
         ~200ms       ~800ms     ~300ms
</code></pre><p><strong>End-to-end model:</strong> A single model like GPT-4o that handles audio natively. Lower latency and a more natural feel, but harder to debug, and you&rsquo;re locked to one provider&rsquo;s capabilities.</p>
<p>I lean modular for anything going to production. Here&rsquo;s why: when a user reports &ldquo;the bot said something weird,&rdquo; I need to know whether it was a transcription error, a reasoning failure, or a synthesis artifact. With an end-to-end model, that&rsquo;s a black box.</p>
<h2 id="the-streaming-architecture-that-matters">The streaming architecture that matters</h2>
<p>The biggest latency win isn&rsquo;t model speed. It&rsquo;s streaming. Start synthesizing audio before the full response is generated. In Go, it looks something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">VoiceSession</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sttClient</span>    <span style="color:#a6e22e">STTClient</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">llm</span>          <span style="color:#a6e22e">LLMClient</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ttsClient</span>    <span style="color:#a6e22e">TTSClient</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">audioOut</span>     <span style="color:#66d9ef">chan</span> []<span style="color:#66d9ef">byte</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">interrupted</span>  <span style="color:#a6e22e">atomic</span>.<span style="color:#a6e22e">Bool</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">VoiceSession</span>) <span style="color:#a6e22e">HandleUtterance</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">audio</span> []<span style="color:#66d9ef">byte</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Transcribe</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">transcript</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">sttClient</span>.<span style="color:#a6e22e">Transcribe</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">audio</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;transcription failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Stream LLM response, pipe chunks directly to TTS</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">stream</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">llm</span>.<span style="color:#a6e22e">StreamChat</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">transcript</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;llm stream failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">buf</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">chunk</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">stream</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">interrupted</span>.<span style="color:#a6e22e">Load</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// User interrupted, stop generating</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">WriteString</span>(<span style="color:#a6e22e">chunk</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Flush to TTS at sentence boundaries</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">isSentenceEnd</span>(<span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">String</span>()) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">audioChunk</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">ttsClient</span>.<span style="color:#a6e22e">Synthesize</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">String</span>())
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">continue</span> <span style="color:#75715e">// Degrade gracefully, skip this chunk</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">audioOut</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">audioChunk</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">Reset</span>()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Flush remaining text</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">Len</span>() &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">audioChunk</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">ttsClient</span>.<span style="color:#a6e22e">Synthesize</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">buf</span>.<span style="color:#a6e22e">String</span>())
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">audioOut</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">audioChunk</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The key insight: flush to TTS at sentence boundaries, not at the end of the full response. The user hears the first sentence while the model is still generating the third. Perceived latency drops from 1300ms to under 500ms.</p>
<h2 id="interruptions-arent-edge-cases">Interruptions aren&rsquo;t edge cases</h2>
<p>People interrupt. They talk over the bot. They say &ldquo;wait, no, actually&hellip;&rdquo; halfway through a sentence. If your system can&rsquo;t handle this, users will hate it within 30 seconds.</p>
<p>The interrupt handler needs to do three things fast:</p>
<ol>
<li><strong>Stop audio output immediately.</strong> Not after the current sentence. Now.</li>
<li><strong>Cancel pending TTS and LLM generation.</strong> Don&rsquo;t waste compute on a response nobody will hear.</li>
<li><strong>Accept the new input without resetting the conversation.</strong> Context should carry over.</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">VoiceSession</span>) <span style="color:#a6e22e">HandleInterrupt</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">newAudio</span> []<span style="color:#66d9ef">byte</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">interrupted</span>.<span style="color:#a6e22e">Store</span>(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Drain the audio output channel</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> len(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">audioOut</span>) &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">audioOut</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">interrupted</span>.<span style="color:#a6e22e">Store</span>(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">HandleUtterance</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">newAudio</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is simplified, but the pattern holds. The <code>atomic.Bool</code> flag propagates interrupts to the streaming loop without complex synchronization.</p>
<h2 id="when-voice-is-the-wrong-interface">When voice is the wrong interface</h2>
<p>Voice is great when:</p>
<ul>
<li>The user&rsquo;s hands are busy (driving, cooking, field work)</li>
<li>The task has a narrow, predictable vocabulary</li>
<li>The expected output is short &ndash; a confirmation, a lookup, a simple action</li>
</ul>
<p>Voice is terrible when:</p>
<ul>
<li>The user needs to compare options visually</li>
<li>The output is complex or structured (tables, code, lists)</li>
<li>Precision matters more than speed (medical, legal, financial details)</li>
</ul>
<p>I keep seeing teams try to build &ldquo;voice-first everything&rdquo; products. Don&rsquo;t do this. Voice should be one input mode in a system that gracefully falls back to text or visual UI when the task demands it.</p>
<h2 id="operational-concerns-that-will-bite-you">Operational concerns that will bite you</h2>
<p><strong>Transcription accuracy varies wildly by accent, background noise, and microphone quality.</strong> Test with real users in real environments, not in a quiet office with a studio mic. I learned this the hard way: a prototype that worked perfectly in our office fell apart in a warehouse with forklift noise.</p>
<p><strong>Track these metrics from day one:</strong></p>
<ul>
<li>Transcription word error rate by user segment</li>
<li>Time to first audio byte (perceived latency)</li>
<li>Interruption rate and recovery success</li>
<li>Conversation completion rate vs. abandonment</li>
<li>Fallback-to-text rate</li>
</ul>
<p><strong>Cost adds up fast.</strong> A 30-second voice interaction can involve a STT call, an LLM call with conversation history, and a TTS call. Multiply by thousands of daily users and you need a cost model before you launch, not after.</p>
<h2 id="keep-it-boring">Keep it boring</h2>
<p>The best voice AI products I&rsquo;ve seen are boring. They do one thing, they do it fast, and they handle failure gracefully. A voice ordering system that works for 50 menu items. A voice-controlled inventory check. A hands-free incident report dictation tool.</p>
<p>Nobody is going to have a deep philosophical conversation with your voice bot. They want to get something done and move on. Design for that.</p>
<p>The tech is ready. The hard part is the discipline to ship something narrow and reliable instead of something ambitious and fragile.</p>
]]></content:encoded></item><item><title>GPT-4o Changed the Interface, Not the Hard Part</title><link>https://lawzava.com/blog/2024-05-13-gpt4o-realtime-ai/</link><pubDate>Mon, 13 May 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-05-13-gpt4o-realtime-ai/</guid><description>OpenAI shipped a model that sees, hears, and talks back in real time. The demos look magical. The architecture implications are where it gets interesting.</description><content:encoded><![CDATA[<p>I was on a call with an engineering team when the GPT-4o demo dropped. Someone shared the link in Slack, and within ten minutes nobody was paying attention to the sprint review anymore. The live voice demo, the real-time vision, the emotion in the synthesized speech &ndash; it looked like science fiction shipping on a Tuesday afternoon.</p>
<p>Then the demo high wore off, and the real questions started.</p>
<h2 id="what-actually-shipped">What actually shipped</h2>
<p>GPT-4o is a single model that handles text, images, and audio natively. No more chaining a whisper transcription into GPT-4 into a TTS engine. One model, one round trip, multiple modalities.</p>
<p>That sounds incremental until you think about what it kills: the glue. I&rsquo;ve spent more time than I want to admit debugging pipelines where context got lost between the speech-to-text step and the reasoning step, or where the TTS output sounded robotic because the model had no awareness it was producing spoken words. GPT-4o collapses that entire pipeline into a single inference call.</p>
<p>Fewer seams means fewer places for things to break. That matters more than any benchmark.</p>
<h2 id="where-this-changes-product-design">Where this changes product design</h2>
<p>The interesting shift isn&rsquo;t &ldquo;AI can talk now.&rdquo; It&rsquo;s that users no longer have to context-switch between modalities. Show the camera, describe the problem, get an answer &ndash; all in one continuous loop.</p>
<p>I&rsquo;ve been advising a couple of teams building support tools, and this unlocks patterns that were previously too brittle to ship:</p>
<ul>
<li><strong>Live visual troubleshooting.</strong> User points their phone at the broken thing, explains the issue, and the model responds while looking at the same image. No more &ldquo;please upload a screenshot and describe what happened.&rdquo;</li>
<li><strong>Hands-free workflows.</strong> Voice as primary input, text as structured output. Think field technicians, warehouse workers, anyone whose hands are occupied.</li>
<li><strong>Coaching and tutoring.</strong> The model sees the student&rsquo;s work and talks through corrections in real time. This was a three-service pipeline before. Now it&rsquo;s one call.</li>
</ul>
<p>These aren&rsquo;t hypothetical. They&rsquo;re products teams tried to build last year and abandoned because latency and context loss across the pipeline made them unusable.</p>
<h2 id="the-complexity-doesnt-disappear">The complexity doesn&rsquo;t disappear</h2>
<p>Here is what the demo didn&rsquo;t show: the model is faster and more unified, but the infrastructure around it is still hard.</p>
<p>Streaming audio over unreliable mobile networks is an unsolved problem in most organizations. Encoding images in real time on low-end devices is a performance cliff. And once you&rsquo;re processing audio and video from users, you have entered a privacy and consent minefield that most teams haven&rsquo;t mapped.</p>
<p>A single model simplifies the AI layer. It doesn&rsquo;t simplify the transport layer, the device layer, or the compliance layer. If anything, it makes those harder because the demo sets expectations that the infrastructure can&rsquo;t meet yet.</p>
<p>I told a team last week: &ldquo;The model is ready. Your CDN isn&rsquo;t.&rdquo;</p>
<h2 id="how-id-evaluate-this">How I&rsquo;d evaluate this</h2>
<p>When API access is fresh and the documentation is still evolving, the worst thing you can do is build something ambitious. Pick the narrowest possible workflow. Something like: user speaks a question, model responds with text and audio. No vision, no tool calling, just the core loop.</p>
<p>Measure three things:</p>
<ol>
<li>Does the end-to-end interaction feel natural, or does the latency break the illusion?</li>
<li>How does it behave with bad audio &ndash; background noise, accents, interruptions?</li>
<li>What does failure look like, and can the UI recover without the user noticing?</li>
</ol>
<p>If you can&rsquo;t answer those three questions with your prototype, you aren&rsquo;t ready to expand scope. Ship the boring version first.</p>
<h2 id="the-consent-problem-nobody-talks-about">The consent problem nobody talks about</h2>
<p>Real-time multimodal means you&rsquo;re potentially recording and processing audio and video from real people. That&rsquo;s a different legal and ethical surface than processing text prompts.</p>
<p>You need explicit consent flows. You need to decide what gets stored and what gets discarded after inference. You need a plan for when the model misinterprets visual input in a way that&rsquo;s embarrassing or harmful. Most of the teams I&rsquo;ve talked to are hand-waving this. Don&rsquo;t be one of them.</p>
<h2 id="what-matters">What matters</h2>
<p>GPT-4o is a genuine architecture shift. One model, multiple modalities, real-time responses. That eliminates an entire class of integration problems and makes products possible that weren&rsquo;t viable six months ago.</p>
<p>But the hard part was never the model. The hard part is reliable transport, device compatibility, privacy, and graceful degradation. The teams that win with this will be the ones who treat the model as the easy layer and invest in everything around it.</p>
]]></content:encoded></item><item><title>LLM Structured Output in Go: JSON Schema, Validation, Retries</title><link>https://lawzava.com/blog/2024-04-29-structured-output-patterns/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-04-29-structured-output-patterns/</guid><description>How to get reliable JSON from LLMs in Go with schemas, validation, repair loops, and typed contracts.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Structured output is a contract-enforcement problem, not a prompting problem. Define a schema, constrain the prompt, validate every response, and build a repair loop for when the model drifts. I do this in Go with about 300 lines of reusable code. Here is all of it.</p>
<hr>
<p>I have a rule for any  <a href="/blog/2023-08-07-building-ai-features/"
   
   >LLM feature</a>
 that feeds a downstream system: if you can&rsquo;t <code>json.Unmarshal</code> the response into a typed struct, it isn&rsquo;t done.</p>
<p>That sounds obvious. In practice, it isn&rsquo;t. I still see production systems parsing LLM output with string splitting and regex. They work until they don&rsquo;t, and when they break, they fail in ways that are hard to diagnose because the failure is subtle data corruption, not a crash.</p>
<p>Structured output from LLMs is a solved problem if you treat it as contract enforcement. Define what you expect. Tell the model exactly what you expect. Validate what you get. Repair what breaks. Here is how I do it in Go. This is one of the control surfaces that belongs in any serious AI-native architecture and  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation pipeline</a>
.</p>
<h2 id="the-failure-modes-are-predictable">The failure modes are predictable</h2>
<p>LLMs generate text. They don&rsquo;t generate data structures. Even with strong prompting, they will occasionally:</p>
<ul>
<li>Wrap the JSON in markdown code fences or explanatory prose</li>
<li>Omit fields they consider &ldquo;obvious&rdquo; or irrelevant</li>
<li>Use wrong types (string <code>&quot;null&quot;</code> instead of JSON <code>null</code>, number as string)</li>
<li>Rename fields to something they think is more descriptive</li>
<li>Produce partial output when hitting token limits</li>
</ul>
<p>Every pattern in this post targets one of these failures. They aren&rsquo;t edge cases. They&rsquo;re the normal operating reality of structured LLM output.</p>
<h2 id="define-the-contract-as-go-types">Define the contract as Go types</h2>
<p>Start with the output structure. This isn&rsquo;t just documentation &ndash; it&rsquo;s both the validation target and the deserialization target. One definition serves both purposes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ContactInfo</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Name</span>    <span style="color:#66d9ef">string</span>  <span style="color:#e6db74">`json:&#34;name&#34;    validate:&#34;required,min=1&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Email</span>   <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;email&#34;   validate:&#34;omitempty,email&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Company</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;company&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Role</span>    <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;role&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Nullable fields use pointers. Required fields use value types. The <code>validate</code> tags drive runtime validation. This struct is the single source of truth: the prompt references it, the validator enforces it, and the calling code consumes it.</p>
<p>I also generate a JSON Schema from the struct for inclusion in prompts. This keeps the prompt and validation in sync automatically:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">SchemaFor</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">any</span>]() ([]<span style="color:#66d9ef">byte</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">reflector</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jsonschema</span>.<span style="color:#a6e22e">Reflector</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">RequiredFromJSONSchemaTags</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">DoNotReference</span>:             <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">schema</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">reflector</span>.<span style="color:#a6e22e">Reflect</span>(new(<span style="color:#a6e22e">T</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">MarshalIndent</span>(<span style="color:#a6e22e">schema</span>, <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;  &#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>One definition. One schema. No drift between what you ask for and what you validate.</p>
<h2 id="build-the-prompt-to-minimize-ambiguity">Build the prompt to minimize ambiguity</h2>
<p>The prompt should be rigid and specific. No motivational language. No &ldquo;please try your best.&rdquo; Just the schema, the rules, and the input.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">BuildExtractionPrompt</span>(<span style="color:#a6e22e">schema</span> []<span style="color:#66d9ef">byte</span>, <span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`Extract structured data from the input. Return ONLY valid JSON matching this schema:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">%s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Rules:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- Use null for missing fields, not empty strings
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- Lowercase email addresses
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- No additional keys beyond the schema
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- No markdown, no explanation, just the JSON object
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Input:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">%s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">JSON:`</span>, string(<span style="color:#a6e22e">schema</span>), <span style="color:#a6e22e">input</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>JSON:</code> at the end is a small trick that helps. It primes the model to start generating JSON immediately instead of opening with &ldquo;Here is the extracted data:&rdquo; or similar preamble.</p>
<h2 id="the-extraction-pipeline">The extraction pipeline</h2>
<p>This is the core of the system: call the model, clean the response, parse it, validate it, and retry on failure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Extractor</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">any</span>] <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">client</span>     <span style="color:#a6e22e">LLMClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">validator</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">validator</span>.<span style="color:#a6e22e">Validate</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">schema</span>     []<span style="color:#66d9ef">byte</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">maxRetries</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewExtractor</span>[<span style="color:#a6e22e">T</span> <span style="color:#66d9ef">any</span>](<span style="color:#a6e22e">client</span> <span style="color:#a6e22e">LLMClient</span>, <span style="color:#a6e22e">maxRetries</span> <span style="color:#66d9ef">int</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Extractor</span>[<span style="color:#a6e22e">T</span>], <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">schema</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">SchemaFor</span>[<span style="color:#a6e22e">T</span>]()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;generating schema: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Extractor</span>[<span style="color:#a6e22e">T</span>]{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">client</span>:     <span style="color:#a6e22e">client</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">validator</span>:  <span style="color:#a6e22e">validator</span>.<span style="color:#a6e22e">New</span>(),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">schema</span>:     <span style="color:#a6e22e">schema</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">maxRetries</span>: <span style="color:#a6e22e">maxRetries</span>,
</span></span><span style="display:flex;"><span>	}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">e</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Extractor</span>[<span style="color:#a6e22e">T</span>]) <span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">T</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildExtractionPrompt</span>(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">schema</span>, <span style="color:#a6e22e">input</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">lastErr</span> <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">maxRetries</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">raw</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Generate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">prompt</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;llm call failed: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">cleaned</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cleanJSONResponse</span>(<span style="color:#a6e22e">raw</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">result</span> <span style="color:#a6e22e">T</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">cleaned</span>), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">result</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">lastErr</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;attempt %d: json parse error: %w&#34;</span>, <span style="color:#a6e22e">attempt</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">prompt</span> = <span style="color:#a6e22e">buildRepairPrompt</span>(<span style="color:#a6e22e">prompt</span>, <span style="color:#a6e22e">raw</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">validator</span>.<span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">result</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">lastErr</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;attempt %d: validation error: %w&#34;</span>, <span style="color:#a6e22e">attempt</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">prompt</span> = <span style="color:#a6e22e">buildRepairPrompt</span>(<span style="color:#a6e22e">prompt</span>, <span style="color:#a6e22e">raw</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;extraction failed after %d attempts: %w&#34;</span>, <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">maxRetries</span>, <span style="color:#a6e22e">lastErr</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A few things to notice. The generic type parameter means this extractor works for any output struct: <code>ContactInfo</code>, <code>InvoiceData</code>, whatever. The cleaning step handles the most common format issues before parsing. And on failure, the repair prompt feeds the error back to the model so it can fix the specific problem.</p>
<h2 id="cleaning-the-response">Cleaning the response</h2>
<p>Models love to wrap JSON in markdown code fences or add explanatory text. This function strips that away:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">cleanJSONResponse</span>(<span style="color:#a6e22e">raw</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">raw</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Strip markdown code fences</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">HasPrefix</span>(<span style="color:#a6e22e">s</span>, <span style="color:#e6db74">&#34;```&#34;</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">lines</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">s</span>, <span style="color:#e6db74">&#34;\n&#34;</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#75715e">// Remove first line (```json) and last line (```)</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">start</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">end</span> <span style="color:#f92672">:=</span> len(<span style="color:#a6e22e">lines</span>) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">end</span> &gt; <span style="color:#a6e22e">start</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">lines</span>[<span style="color:#a6e22e">end</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;```&#34;</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">end</span> = <span style="color:#a6e22e">end</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">s</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">lines</span>[<span style="color:#a6e22e">start</span>:<span style="color:#a6e22e">end</span>], <span style="color:#e6db74">&#34;\n&#34;</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Find the first { and last } to extract the JSON object</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">firstBrace</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Index</span>(<span style="color:#a6e22e">s</span>, <span style="color:#e6db74">&#34;{&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">lastBrace</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">LastIndex</span>(<span style="color:#a6e22e">s</span>, <span style="color:#e6db74">&#34;}&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">firstBrace</span> <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">lastBrace</span> &gt; <span style="color:#a6e22e">firstBrace</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">s</span> = <span style="color:#a6e22e">s</span>[<span style="color:#a6e22e">firstBrace</span> : <span style="color:#a6e22e">lastBrace</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">s</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This isn&rsquo;t pretty. It doesn&rsquo;t need to be. It handles the three wrapping patterns I most often see in production: code fences, leading prose, and trailing explanation.</p>
<h2 id="the-repair-prompt">The repair prompt</h2>
<p>When parsing or validation fails, the repair prompt tells the model exactly what went wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">buildRepairPrompt</span>(<span style="color:#a6e22e">originalPrompt</span>, <span style="color:#a6e22e">badOutput</span>, <span style="color:#a6e22e">errorMsg</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`%s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Your previous output was invalid:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">%s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Error: %s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Fix the error and return ONLY valid JSON.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">JSON:`</span>, <span style="color:#a6e22e">originalPrompt</span>, <span style="color:#a6e22e">badOutput</span>, <span style="color:#a6e22e">errorMsg</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is where the retry loop earns its keep. The model gets the original instructions, sees its own bad output, and gets a specific error message to fix.</p>
<p>From what I&rsquo;ve seen, this recovers about 80% of validation failures on the first retry. The remaining 20% usually indicate a genuinely ambiguous input that needs human review.</p>
<h2 id="use-json-mode-when-available">Use JSON mode when available</h2>
<p>Most model APIs now offer a JSON-only response mode. Use it. It eliminates prose wrapping entirely and significantly reduces parsing failures.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">e</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Extractor</span>[<span style="color:#a6e22e">T</span>]) <span style="color:#a6e22e">Extract</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">input</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">T</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">BuildExtractionPrompt</span>(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">schema</span>, <span style="color:#a6e22e">input</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">opts</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">GenerateOptions</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ResponseFormat</span>: <span style="color:#a6e22e">ResponseFormatJSON</span>, <span style="color:#75715e">// Use JSON mode</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// ... rest of the extraction logic</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>But &ndash; and I can&rsquo;t stress this enough &ndash; JSON mode doesn&rsquo;t mean you skip validation. The model can still omit required fields, use wrong types, or produce a valid JSON object that doesn&rsquo;t match your schema. JSON mode guarantees parseable JSON. It doesn&rsquo;t guarantee <em>correct</em> JSON for your use case.</p>
<h2 id="monitoring-structured-output-in-production">Monitoring structured output in production</h2>
<p>Three metrics I track for every structured-output pipeline:</p>
<ol>
<li><strong>Parse success rate.</strong> What percentage of responses parse and validate on the first attempt? If this drops below 95%, something changed: the model updated, the prompt drifted, or the input distribution shifted.</li>
<li><strong>Retry rate and recovery rate.</strong> How often do you need retries, and how often do retries succeed? A high retry rate with good recovery means the repair loop is working. A high retry rate with low recovery means something is fundamentally wrong.</li>
<li><strong>Field-level error distribution.</strong> Which fields cause the most validation failures? This tells you where the prompt needs to be more explicit or where the schema needs adjustment.</li>
</ol>
<p> <a href="/blog/2023-08-21-llm-observability/"
   
   >I log every extraction attempt</a>
: success or failure, first try or retry, with the raw model output. When something goes wrong in production, I want to see exactly what the model returned, not just that it failed.</p>
<h2 id="the-pattern-summarized">The pattern, summarized</h2>
<p>Every structured-output pipeline I build follows the same sequence:</p>
<ol>
<li>Define the contract as a Go struct with validation tags.</li>
<li>Generate the JSON Schema from that struct.</li>
<li>Build a rigid prompt that includes the schema and leaves no room for interpretation.</li>
<li>Clean the raw response to handle common wrapping patterns.</li>
<li>Parse and validate against the struct.</li>
<li>On failure, retry with a repair prompt that includes the specific error.</li>
<li>Monitor parse rates, retry rates, and field-level errors.</li>
</ol>
<p>This isn&rsquo;t clever. It isn&rsquo;t novel. It&rsquo;s disciplined application of the same contract-enforcement thinking we use everywhere else in software engineering. The model is an unreliable data source. Treat it like one.</p>
]]></content:encoded></item><item><title>Most AI Developer Tools Are Not Worth Adopting Yet</title><link>https://lawzava.com/blog/2024-04-15-ai-developer-tooling/</link><pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-04-15-ai-developer-tooling/</guid><description>The AI tooling landscape is exploding. Most of it adds complexity without removing real friction. Here is how I decide what earns a spot in the stack.</description><content:encoded><![CDATA[<p>Everyone has a favorite AI developer tool now: code assistants, LLM frameworks, vector databases, eval harnesses, observability platforms, deployment wrappers. The landscape is overwhelming, and most of it isn&rsquo;t worth your time.</p>
<p>That isn&rsquo;t cynicism. It&rsquo;s experience. I&rsquo;ve watched teams adopt tools that solve problems they don&rsquo;t have, add abstraction layers they can&rsquo;t debug, and create dependencies they can&rsquo;t unwind. The result is a stack that&rsquo;s harder to understand than the problem it was supposed to simplify.</p>
<h2 id="the-framework-trap">The framework trap</h2>
<p>Here is my unpopular opinion: most teams shouldn&rsquo;t be using an LLM framework. LangChain, LlamaIndex, whatever ships next week &ndash; they are solving a real problem, but they are solving it for a use case most teams haven&rsquo;t reached yet.</p>
<p>If your application calls one model with one prompt and parses the output, you don&rsquo;t need a framework. You need an HTTP client and solid error handling. A framework adds routing, memory, tool calling, and chain-of-thought orchestration that you might need in six months. Right now, it mostly adds layers you can&rsquo;t see through when something breaks.</p>
<p>Start without the framework. Add it when you can name the specific pieces it replaces and what maintenance burden it removes. Not before.</p>
<h2 id="code-assistants-are-useful-stop-pretending-they-are-magic">Code assistants are useful. Stop pretending they are magic.</h2>
<p>I use Copilot daily. It&rsquo;s good at boilerplate, decent at suggesting patterns I&rsquo;ve seen before, and occasionally impressive on unfamiliar code. It&rsquo;s also confidently wrong often enough that accepting suggestions uncritically is dangerous.</p>
<p>Teams getting real value from code assistants treat the output as a first draft. It goes through the same code review process as any other contribution. Teams getting hurt are the ones accepting suggestions because they &ldquo;look right&rdquo; without checking whether they actually are.</p>
<p>The productivity gain is real, but smaller than the marketing suggests. It also comes with a hidden cost: style drift. The assistant doesn&rsquo;t know your team&rsquo;s conventions. Over time, the codebase starts to feel inconsistent unless you actively enforce standards on AI-generated code.</p>
<h2 id="what-actually-earns-its-place">What actually earns its place</h2>
<p>After working with several teams on their AI tooling stacks, I have a short list of what I think is genuinely worth adopting:</p>
<p><strong>Eval harnesses.</strong> Whatever helps you measure output quality against a test set. This can be a framework or a 200-line script. It doesn&rsquo;t matter. What matters is that it exists and runs on every change.</p>
<p><strong>Structured logging for LLM calls.</strong> Not a fancy observability platform &ndash; just disciplined logging of prompts, responses, latency, and token counts. You will need this data the moment something goes wrong. Which will be soon.</p>
<p><strong>A simple abstraction over model providers.</strong> Not a framework. Just a thin interface that lets you swap models without rewriting calling code. I build these in Go in an afternoon. They pay for themselves the first time a provider changes their API.</p>
<p>That&rsquo;s it. Everything else should prove its value before it gets a spot in <code>go.mod</code>.</p>
<h2 id="the-decision-filter">The decision filter</h2>
<p>Before adopting any AI tool, answer one question: what specific friction does this remove that I can&rsquo;t solve with under a day of custom code?</p>
<p>If the answer is &ldquo;it makes things easier&rdquo; or &ldquo;everyone is using it,&rdquo; that isn&rsquo;t good enough. If the answer is &ldquo;it replaces 500 lines of boilerplate I maintain across three services,&rdquo; then fine. Adopt it.</p>
<p>Keep the stack small. Keep it legible. The tooling landscape will look completely different in six months anyway.</p>
]]></content:encoded></item><item><title>Agentic Workflows: From Demo Magic to Production Reality</title><link>https://lawzava.com/blog/2024-04-01-agentic-workflows-production/</link><pubDate>Mon, 01 Apr 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-04-01-agentic-workflows-production/</guid><description>AI agents that can take actions are fundamentally different from chatbots. The engineering bar must match the blast radius.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>An agent that can read data and change state isn&rsquo;t a chatbot with extra steps. It&rsquo;s a system with real blast radius. Constrain it with explicit policies, prefer structured workflows over free-form loops, and invest in observability before you invest in capabilities. The boring stuff is what makes agents safe to ship.</p>
<hr>
<p>There&rsquo;s a moment in every agentic AI demo that makes the audience gasp. The agent reads a database, reasons about the results, drafts an email, and sends it. Autonomously. It feels like magic.</p>
<p>Then someone asks: &ldquo;What happens if it sends the wrong email?&rdquo; And the room gets quiet.</p>
<p>I&rsquo;ve been building agentic systems for several months now. The demo-to-production gap here is wider than almost anywhere else in AI engineering. A chatbot that hallucinates is annoying. An agent that hallucinates and then <em>acts on the hallucination</em> is a liability.</p>
<p>The difference between teams that ship agents successfully and teams that revert after a week comes down to three things: boundaries, structure, and boring reliability work.</p>
<h2 id="boundaries-first-capabilities-second">Boundaries first, capabilities second</h2>
<p>Almost every team starts with capabilities. &ldquo;What tools should the agent have? What actions can it take?&rdquo; Wrong starting point.</p>
<p>Start with constraints. What is the agent <em>not</em> allowed to do? What&rsquo;s the maximum blast radius of a single run? What happens when it goes wrong?</p>
<p>A policy config is the simplest way to make these constraints explicit and auditable:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">agent_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">allowed_tools</span>: [<span style="color:#ae81ff">read_db, write_ticket, send_email_draft]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_steps</span>: <span style="color:#ae81ff">8</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_runtime_seconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_cost_usd</span>: <span style="color:#ae81ff">0.50</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">approval_required</span>: [<span style="color:#ae81ff">send_email, issue_refund, modify_production]</span>
</span></span></code></pre></div><p>This isn&rsquo;t a suggestion. It&rsquo;s the foundation. The allowed tools list is an allowlist, not a blocklist &ndash; the agent can only use what&rsquo;s explicitly permitted. Step and time limits prevent runaway loops. Cost caps prevent a single request from draining your budget. The approval list separates actions that are safe to automate from actions that need a human in the loop.</p>
<p>At one delivery company I worked with, a team skipped the approval step for &ldquo;low-risk&rdquo; actions. One of those low-risk actions turned out to be updating customer records. An agent misinterpreted a support request and bulk-updated addresses for a batch of orders. The fix took two days. The approval gate would have taken two seconds.</p>
<p>If the policy feels too restrictive, relax it intentionally and document why. If you can&rsquo;t explain why a tool is on the allowed list, it shouldn&rsquo;t be there.</p>
<h2 id="structured-workflows-beat-free-form-loops">Structured workflows beat free-form loops</h2>
<p>The temptation with agents is to give them a goal and let them figure out the steps. This works beautifully in demos. In production, it creates systems that are impossible to debug, test, or audit.</p>
<p>I prefer structured workflows with a small number of decision points. The model chooses among defined paths. Deterministic logic handles state transitions. The result is a system you can trace, test, and explain.</p>
<p>Think of it as a state machine where the model influences transitions but doesn&rsquo;t control them entirely. The model might decide whether a customer inquiry needs escalation or can be handled automatically. But the escalation path itself &ndash; what happens, in what order, and with what approvals &ndash; is defined in code, not improvised by the model.</p>
<p>When a task genuinely doesn&rsquo;t fit a clean workflow, isolate it. Put the free-form reasoning in a narrow, heavily instrumented sandbox with tight constraints. Don&rsquo;t make it the default path for everything.</p>
<h2 id="the-boring-reliability-checklist">The boring reliability checklist</h2>
<p>I know this section won&rsquo;t go viral. That&rsquo;s fine. It&rsquo;s the section that keeps your agent from becoming an incident.</p>
<p><strong>Idempotent steps.</strong> If a step fails and retries, it shouldn&rsquo;t duplicate work. The agent shouldn&rsquo;t send two emails because the first one timed out after actually sending. Design every action to be safe to retry.</p>
<p><strong>Checkpointing.</strong> Long-running workflows should save their state at each step. If the process crashes or the model call times out, the workflow should resume from the last checkpoint, not start over.</p>
<p><strong>Time and step caps.</strong> Hard limits. Non-negotiable. An agent stuck in a reasoning loop should hit a wall after N steps or M seconds, return whatever partial results it has, and report the failure. I set these conservatively and loosen them only after seeing production data.</p>
<p><strong>Retry discipline.</strong> Retry on clearly transient failures &ndash; rate limits, network timeouts. Don&rsquo;t retry on semantic failures &ndash; the model misunderstood the task, or the tool returned an error because the input was wrong. Retrying bad logic just wastes money and time.</p>
<h2 id="observability-isnt-optional">Observability isn&rsquo;t optional</h2>
<p>If you can&rsquo;t trace what an agent did &ndash; every tool call, every model response, every decision point &ndash; you can&rsquo;t debug it. And you <em>will</em> need to debug it.</p>
<p>Structured logging for every step:</p>
<ul>
<li>What tool was called and with what inputs</li>
<li>What the model returned and what confidence signal it provided</li>
<li>Whether an approval was required and who approved it</li>
<li>How long each step took and how many tokens it consumed</li>
<li>The final outcome and whether it matched the intent</li>
</ul>
<p>This log isn&rsquo;t just for debugging. It&rsquo;s your feedback loop. It tells you which prompts need refinement, which tools are unreliable, which workflows cost too much, and where the model consistently makes bad decisions.</p>
<p>One caution: be disciplined about what you log. Inputs and outputs may contain sensitive data. Define retention policies and access controls before you ship, not after an auditor asks.</p>
<h2 id="rolling-out-without-regret">Rolling out without regret</h2>
<p>The teams that succeed with agentic workflows share a rollout pattern:</p>
<ol>
<li><strong>Shadow mode first.</strong> The agent runs alongside the existing process but doesn&rsquo;t take any actions. Log what it <em>would</em> have done. Compare to what the human actually did. This gives you real quality data without any risk.</li>
<li><strong>Low-risk tasks with clear success criteria.</strong> Start with internal tasks where a mistake is inconvenient, not catastrophic. Ticket triage. Data enrichment. Report drafting.</li>
<li><strong>Expand only after stability.</strong> Once reliability, cost, and quality are stable for the initial scope, add more tools or more complex workflows. One step at a time.</li>
</ol>
<p>This pacing is unglamorous. It&rsquo;s also the only approach I&rsquo;ve seen work consistently.</p>
<h2 id="the-uncomfortable-truth">The uncomfortable truth</h2>
<p>Agents are powerful. They&rsquo;re also the highest-risk AI feature you can ship. Every other AI feature is advisory &ndash; the model suggests, the user decides. An agent <em>acts</em>. That means every bug, every hallucination, every misunderstanding has real consequences.</p>
<p>Treat agents as systems engineering, not prompt engineering. Define the blast radius. Build the constraints. Invest in the observability. Ship slow.</p>
<p>The teams that move carefully are the ones still running agents in production six months later. The teams that rush are the ones writing postmortems.</p>
]]></content:encoded></item><item><title>LLM Prompt Caching in Go: Cut Costs Without Breaking Things</title><link>https://lawzava.com/blog/2024-03-25-prompt-caching-strategies/</link><pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-03-25-prompt-caching-strategies/</guid><description>Caching LLM responses is the highest-leverage optimization most teams skip. How I implement it in Go &amp;amp;ndash; keys, invalidation, and safety patterns.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your LLM is answering the same questions repeatedly and you&rsquo;re paying for every single call. Exact-match caching alone can cut 30-50% of your API spend with zero quality loss. Add semantic caching carefully after that. The hard part isn&rsquo;t the cache &ndash; it&rsquo;s the key design and invalidation discipline.</p>
<hr>
<p>I was reviewing API logs last month and found something depressing. About 40% of their LLM requests were functionally identical. Same system prompt, same user question (give or take whitespace), same model. They were paying full price for every single one.</p>
<p>Caching is the most boring and most effective optimization you can make to an LLM application. It isn&rsquo;t glamorous. It doesn&rsquo;t involve new models or clever prompt tricks. It just saves money and makes things faster. Here is how I build it in Go.</p>
<h2 id="start-with-exact-match-caching">Start with exact match caching</h2>
<p>Don&rsquo;t get fancy. The first layer is simple: hash the request, check the cache, return the cached response if it exists. This catches identical requests and costs almost nothing to implement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CacheKey</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Version</span>    <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;v&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Model</span>      <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;model&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">PromptHash</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;prompt_hash&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ToolsHash</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;tools_hash&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ParamsHash</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;params_hash&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewCacheKey</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">LLMRequest</span>) <span style="color:#a6e22e">CacheKey</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">CacheKey</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Version</span>:    <span style="color:#e6db74">&#34;v1&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Model</span>:      <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Model</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">PromptHash</span>: <span style="color:#a6e22e">sha256Hash</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">SystemPrompt</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;\n&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">UserPrompt</span>),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ToolsHash</span>:  <span style="color:#a6e22e">sha256Hash</span>(<span style="color:#a6e22e">marshalTools</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Tools</span>)),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ParamsHash</span>: <span style="color:#a6e22e">sha256Hash</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%f:%d&#34;</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Temperature</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">MaxTokens</span>)),
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">k</span> <span style="color:#a6e22e">CacheKey</span>) <span style="color:#a6e22e">String</span>() <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">b</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">k</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sha256Hash</span>(string(<span style="color:#a6e22e">b</span>))
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">sha256Hash</span>(<span style="color:#a6e22e">s</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">h</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sha256</span>.<span style="color:#a6e22e">Sum256</span>([]byte(<span style="color:#a6e22e">s</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">hex</span>.<span style="color:#a6e22e">EncodeToString</span>(<span style="color:#a6e22e">h</span>[:])
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The key includes everything that can change the output: model, prompt content, tools, and sampling parameters. If any of those differ, you get a different key. If they are all the same, you get a cache hit.</p>
<p>Notice the version field. When you change your key schema &ndash; and you will &ndash; bump the version. This prevents old entries with a different key structure from colliding with new ones.</p>
<h2 id="the-cache-layer-itself">The cache layer itself</h2>
<p>I keep the cache interface simple so the backing store can be swapped. In production I usually start with Redis. For testing and small deployments, an in-memory LRU works fine.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">LLMCache</span> <span style="color:#66d9ef">interface</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">CachedResponse</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">resp</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CachedResponse</span>, <span style="color:#a6e22e">ttl</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>) <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Delete</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CachedResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Content</span>   <span style="color:#66d9ef">string</span>    <span style="color:#e6db74">`json:&#34;content&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Model</span>     <span style="color:#66d9ef">string</span>    <span style="color:#e6db74">`json:&#34;model&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">TokensIn</span>  <span style="color:#66d9ef">int</span>       <span style="color:#e6db74">`json:&#34;tokens_in&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">TokensOut</span> <span style="color:#66d9ef">int</span>       <span style="color:#e6db74">`json:&#34;tokens_out&#34;`</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">CachedAt</span>  <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;cached_at&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Service</span>) <span style="color:#a6e22e">Generate</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">LLMRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">LLMResponse</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">key</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewCacheKey</span>(<span style="color:#a6e22e">req</span>).<span style="color:#a6e22e">String</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cached</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">cache</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">cached</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">CacheHit</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Model</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">LLMResponse</span>{
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Content</span>:  <span style="color:#a6e22e">cached</span>.<span style="color:#a6e22e">Content</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Model</span>:    <span style="color:#a6e22e">cached</span>.<span style="color:#a6e22e">Model</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">FromCache</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>		}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">metrics</span>.<span style="color:#a6e22e">CacheMiss</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">Model</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">llmClient</span>.<span style="color:#a6e22e">Generate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">cached</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">CachedResponse</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Content</span>:   <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Content</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Model</span>:     <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Model</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">TokensIn</span>:  <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">TokensIn</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">TokensOut</span>: <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">TokensOut</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">CachedAt</span>:  <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Fire and forget -- cache write failure should not block the response</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">setErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">cache</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">cached</span>, <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">ttlFor</span>(<span style="color:#a6e22e">req</span>)); <span style="color:#a6e22e">setErr</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">Warn</span>(<span style="color:#e6db74">&#34;cache set failed&#34;</span>, <span style="color:#e6db74">&#34;key&#34;</span>, <span style="color:#a6e22e">key</span>, <span style="color:#e6db74">&#34;error&#34;</span>, <span style="color:#a6e22e">setErr</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A few things to note. The cache write is fire-and-forget. A failed cache write should never block or degrade the response to the user. The <code>FromCache</code> flag on the response is important for monitoring &ndash; you need to know what percentage of traffic is served from cache.</p>
<h2 id="ttl-strategy">TTL strategy</h2>
<p>This is where people get it wrong. They set a blanket TTL and call it done. Different content ages at different rates.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Service</span>) <span style="color:#a6e22e">ttlFor</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">LLMRequest</span>) <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Responses grounded in static reference data can live longer</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">HasStaticContext</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">24</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Hour</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Responses involving real-time data should be short-lived</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">HasLiveDataRetrieval</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Default: conservative TTL</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Hour</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Static context &ndash; like a system prompt explaining how to format output, or reference documentation that changes monthly &ndash; can tolerate a long TTL. Responses that depend on live data need short TTLs or no caching at all. When in doubt, err toward shorter TTLs. A cache miss costs money. A stale response costs trust.</p>
<h2 id="invalidation-beyond-ttls">Invalidation beyond TTLs</h2>
<p>TTLs are your baseline. But you also need event-driven invalidation for cases where you <em>know</em> the cache is stale.</p>
<p>Prompt changes are the big one. Every time you update a system prompt or retrieval pipeline, the old cached responses are wrong. The versioned key handles this naturally &ndash; a new prompt produces a new hash, which produces a new key, which misses the cache. Old entries expire on their own TTL.</p>
<p>For data-driven invalidation, I use a simple pattern:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Service</span>) <span style="color:#a6e22e">OnKnowledgeBaseUpdate</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">docIDs</span> []<span style="color:#66d9ef">string</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Invalidate any cached responses that used these documents</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">docID</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">docIDs</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">keys</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">cacheIndex</span>.<span style="color:#a6e22e">KeysForDocument</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">docID</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#e6db74">&#34;failed to lookup cache keys for document&#34;</span>, <span style="color:#e6db74">&#34;doc_id&#34;</span>, <span style="color:#a6e22e">docID</span>, <span style="color:#e6db74">&#34;error&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">key</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">keys</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">cache</span>.<span style="color:#a6e22e">Delete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">key</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This requires maintaining a secondary index that maps documents to cache keys. It&rsquo;s more work, but for applications where correctness matters &ndash; and it usually does &ndash; it&rsquo;s worth it.</p>
<h2 id="what-not-to-cache">What NOT to cache</h2>
<p>Not every response should be cached. I have a short list of exclusions:</p>
<ul>
<li><strong>User-specific sensitive responses.</strong> Unless your cache has strict tenant isolation, don&rsquo;t risk serving User A&rsquo;s response to User B. I&rsquo;ve seen this bug in production. It&rsquo;s exactly as bad as it sounds.</li>
<li><strong>Responses that depend on time-sensitive external state.</strong> Stock prices, live inventory, anything where a one-hour-old answer is wrong.</li>
<li><strong>Creative or generative tasks where variability is the feature.</strong> If the user expects a different response each time, caching defeats the purpose.</li>
</ul>
<h2 id="measuring-what-matters">Measuring what matters</h2>
<p>You need four metrics from day one:</p>
<ol>
<li><strong>Cache hit rate by request type.</strong> Not a global number. A 60% overall hit rate might mean 90% for classification and 10% for analysis. The per-type breakdown tells you where to focus.</li>
<li><strong>Latency with and without cache.</strong> This quantifies the speed improvement and justifies the infrastructure cost.</li>
<li><strong>Cost savings.</strong> Track tokens not consumed due to cache hits. Multiply by your per-token rate. Show this number to whoever pays the bills.</li>
<li><strong>Quality signals on cached responses.</strong> User corrections, retries, and thumbs-down ratings. If cached responses get worse quality signals than fresh ones, your TTL is too long or your keys are too broad.</li>
</ol>
<h2 id="roll-out-behind-a-flag">Roll out behind a flag</h2>
<p>Don&rsquo;t flip caching on for all traffic at once. Use a feature flag. Start with one request type that has high repetition and low sensitivity. Measure hit rate, latency, and quality for a week. Then expand.</p>
<p>When something goes wrong &ndash; and something always goes wrong &ndash; you want to be able to turn caching off in seconds. A feature flag gives you that.</p>
<h2 id="what-matters">What matters</h2>
<p>Caching isn&rsquo;t sexy. It isn&rsquo;t a new model or a clever prompting technique. It&rsquo;s the same infrastructure discipline we&rsquo;ve applied to every other expensive external service call for decades. The difference is that LLM calls are expensive enough that a 40% hit rate translates to real savings.</p>
<p>Build the cache. Version your keys. Keep TTLs honest. Monitor quality. The money you save on API calls will pay for a lot of actual engineering work.</p>
]]></content:encoded></item><item><title>Why I Run Multiple Models in Production</title><link>https://lawzava.com/blog/2024-03-18-multi-model-strategies/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-03-18-multi-model-strategies/</guid><description>Betting on a single model provider is like having a single database with no failover. Here is why multi-model is the only sane production strategy.</description><content:encoded><![CDATA[<p>Let me tell you about a fun morning I had last month. A major model provider had a partial outage. Not a full downtime &ndash; worse. Elevated latency and intermittent 500s that made the retry logic work overtime without actually resolving anything. The team had bet everything on that one provider. Their AI features were effectively down for four hours.</p>
<p>Another team, running a multi-model setup, barely noticed. Their routing layer shifted traffic to the fallback model within seconds. Quality dipped slightly on complex tasks. Users didn&rsquo;t complain.</p>
<p>Guess which architecture I recommend now.</p>
<h2 id="the-case-is-boring-and-thats-the-point">The case is boring, and that&rsquo;s the point</h2>
<p>Multi-model isn&rsquo;t about chasing the latest release or playing model arbitrage. It&rsquo;s about the same boring infrastructure principles we&rsquo;ve applied to databases, CDNs, and DNS for decades. Don&rsquo;t have a single point of failure. Don&rsquo;t lock yourself into one vendor. Have a plan for when things break.</p>
<p>With LLMs, the failure modes are broader than traditional services. A provider can go down entirely. Latency can spike. A model update can silently change behavior. Rate limits can throttle you during a traffic spike. Any of these will degrade your product if you have no alternative path.</p>
<h2 id="how-i-think-about-routing">How I think about routing</h2>
<p>Routing doesn&rsquo;t need to be sophisticated. I&rsquo;ve seen teams over-engineer this with ML-powered classifiers that decide which model gets each request. That&rsquo;s fun to build and painful to debug.</p>
<p>What works: simple rules based on task type and complexity.</p>
<p>Short classification tasks? Small, fast model. Interactive chat with a paying user? Mid-tier model with good latency. Complex analysis that needs deep reasoning? Big model. Fallback on timeout or error? The next model in the chain.</p>
<p>You can express this in a config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">default</span>: <span style="color:#e6db74">&#34;sonnet&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">task</span>: <span style="color:#e6db74">&#34;classify&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">model</span>: <span style="color:#e6db74">&#34;haiku&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">task</span>: <span style="color:#e6db74">&#34;analyze&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">complexity</span>: <span style="color:#e6db74">&#34;high&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">model</span>: <span style="color:#e6db74">&#34;opus&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fallback_chain</span>: [<span style="color:#e6db74">&#34;sonnet&#34;</span>, <span style="color:#e6db74">&#34;haiku&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">timeout_ms</span>: <span style="color:#ae81ff">10000</span>
</span></span></code></pre></div><p>That&rsquo;s it. No neural router. No reinforcement learning. Just explicit rules you can read, debug, and change in five minutes.</p>
<p>The key insight: routing is configuration, not code. When a new model drops or pricing changes, you update the config. You don&rsquo;t refactor a service.</p>
<h2 id="the-fallback-chain-is-everything">The fallback chain is everything</h2>
<p>I can&rsquo;t stress this enough. Your fallback chain is more important than your primary model choice. Because the primary model <em>will</em> be unavailable at some point.</p>
<p>Keep the chain short &ndash; two or three models. Set aggressive timeouts. And critically: log which model actually served each request. If you don&rsquo;t, you have no idea what quality your users are actually getting. You think they&rsquo;re getting Opus but half the traffic is silently falling back to Haiku because of rate limits.</p>
<p>I made this mistake early on in a project at a telecom company. We had a fallback in place but no logging on which model served the request. For two weeks, the primary model was rate-limited during peak hours and the fallback was handling 40% of traffic. We didn&rsquo;t notice until a quality review showed unexpected patterns. Now I log every routing decision. Non-negotiable.</p>
<h2 id="cost-management-as-a-feature">Cost management as a feature</h2>
<p>Multi-model is also the most effective cost control mechanism I&rsquo;ve found. Instead of running every request through the most capable (and expensive) model, you match model capability to task complexity.</p>
<p>The math is straightforward. If 60% of your requests are simple enough for a small model at one-tenth the cost per token, you just cut your AI spend by roughly half. That&rsquo;s real money at scale. Working with larger companies always surfaces this &ndash; teams are shocked when they see how much they&rsquo;re spending on GPT-4 for tasks that a 7B model could handle.</p>
<h2 id="what-goes-wrong">What goes wrong</h2>
<p>Three failure modes I see repeatedly:</p>
<p><strong>Silent fallbacks.</strong> The system falls back gracefully, but nobody knows. Quality degrades slowly. Users get frustrated. By the time someone investigates, there are weeks of bad data.</p>
<p><strong>Stale routing rules.</strong> A rule made sense three months ago when Model X was the best at coding tasks. Now Model Y is better and cheaper. But nobody updated the config because nobody owns it.</p>
<p><strong>No cross-model evaluation.</strong> Teams evaluate their primary model carefully and treat the fallback as &ldquo;good enough.&rdquo; Then the fallback serves 30% of traffic during a bad week and nobody has measured whether it&rsquo;s actually good enough for those tasks.</p>
<p>The fix for all three is the same: monitor, measure, review. Log every routing decision. Run evals against every model in your chain. Review the routing config monthly. This isn&rsquo;t exciting work. It&rsquo;s the work that keeps production systems stable.</p>
<h2 id="keep-it-simple">Keep it simple</h2>
<p>Multi-model doesn&rsquo;t mean complex. It means intentional. Pick two or three models that cover your cost and capability range. Write routing rules you can read. Log everything. Measure quality per model. Review monthly.</p>
<p>The teams shipping reliable AI features aren&rsquo;t the ones with the cleverest model selection algorithm. They&rsquo;re the ones that can swap a model in five minutes, measure the impact in an hour, and roll back in seconds.</p>
<p>That&rsquo;s the whole strategy. Boring, effective, resilient.</p>
]]></content:encoded></item><item><title>Claude 3 First Impressions: Three Models, One Decision Framework</title><link>https://lawzava.com/blog/2024-03-04-claude-3-first-look/</link><pubDate>Mon, 04 Mar 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-03-04-claude-3-first-look/</guid><description>Anthropic shipped three models instead of one. That is actually the most interesting part of the release.</description><content:encoded><![CDATA[<p>I was halfway through migrating an extraction pipeline to a new prompt format when Anthropic dropped Claude 3: three models &ndash; Opus, Sonnet, and Haiku &ndash; with different capability tiers, price points, and latency profiles.</p>
<p>My first reaction: finally, someone is admitting that one model doesn&rsquo;t fit every job.</p>
<p>My second reaction: now I have to rerun all my evals.</p>
<h2 id="the-lineup">The lineup</h2>
<p>Anthropic did something smart here. Instead of releasing one model and calling it &ldquo;the best,&rdquo; they gave you a menu with clear trade-offs.</p>
<p><strong>Opus</strong> is the heavyweight. Complex reasoning, deep analysis, demanding coding tasks. It&rsquo;s slower and more expensive than the others, but the quality ceiling is noticeably higher. I ran it against some gnarly extraction cases I&rsquo;ve been working on &ndash; multi-page contracts with nested clauses and ambiguous references. It handled nuance that the previous generation fumbled.</p>
<p><strong>Sonnet</strong> is the workhorse. Good enough for most production workloads, fast enough for interactive use, and priced so it is still viable at volume. This is where I expect most teams to land as a default.</p>
<p><strong>Haiku</strong> is the speed demon. Lightweight tasks, high-volume classification, anything where latency matters more than depth. I tested it on a categorization pipeline &ndash; hundreds of short inputs, simple labels &ndash; and it ripped through them. The quality was adequate for the task, and the speed was impressive.</p>
<p>The real value isn&rsquo;t any single model. It&rsquo;s the fact that you can route between them based on what the task actually needs.</p>
<h2 id="what-i-noticed-in-practice">What I noticed in practice</h2>
<p>A few things stood out during my first week of testing.</p>
<p>Instruction following is substantially better. Prompts that previously needed careful phrasing to avoid drift now work with more natural language. This is the kind of improvement that doesn&rsquo;t show up in benchmarks but saves real time in production prompt maintenance.</p>
<p>Vision capabilities are real. I fed Opus some architectural diagrams from a past project and asked it to describe the data flow. The descriptions were useful &ndash; not perfect, but useful enough to save someone from manually transcribing a whiteboard photo.</p>
<p>The context window is large, but I&rsquo;ve learned not to treat large context as a substitute for good retrieval. Stuffing 200k tokens of raw documents into context and hoping for the best is still a bad strategy. I got better results with targeted retrieval feeding a smaller context window.</p>
<p>One thing that frustrated me: the API rate limits during launch week were tight. I burned through my allocation faster than expected while running evals. Plan for this if you&rsquo;re testing around a major release.</p>
<h2 id="how-im-thinking-about-adoption">How I&rsquo;m thinking about adoption</h2>
<p>The question isn&rsquo;t &ldquo;should I use Claude 3?&rdquo; It&rsquo;s &ldquo;which tier maps to which workflow?&rdquo;</p>
<p>Before switching any production traffic, I work through these questions:</p>
<ul>
<li><strong>Latency budget.</strong> Interactive features need sub-3-second responses. That might mean Haiku for the fast path and Sonnet for a follow-up detail request.</li>
<li><strong>Quality threshold.</strong> Classification and routing tasks don&rsquo;t need Opus. Contract analysis probably does.</li>
<li><strong>Cost sensitivity.</strong> High-volume features should default to the cheapest model that meets the quality bar. Upgrade selectively.</li>
<li><strong>Rollback plan.</strong> What happens if quality regresses after the switch? If you don&rsquo;t have an answer, you aren&rsquo;t ready.</li>
</ul>
<p>I route by task type, not by model hype. Haiku handles the lightweight stuff. Sonnet is the default for anything interactive. Opus gets called when the task genuinely needs deeper reasoning. This isn&rsquo;t a Claude-specific strategy &ndash; it&rsquo;s how I think about any multi-model setup.</p>
<h2 id="the-honest-assessment">The honest assessment</h2>
<p>Claude 3 is a meaningful step forward. The quality improvements are real, especially in instruction following and structured output. The tiered model approach is the right direction for the industry &ndash; it forces you to think about routing, evaluation, and cost management instead of treating the model as a magic box.</p>
<p>But it&rsquo;s still a model. It still hallucinates. It still needs evaluation. It still needs guardrails and fallback paths. The teams that will get the most out of Claude 3 are the ones that already have those systems in place.</p>
<p>For everyone else, the release is a good excuse to finally build them.</p>
]]></content:encoded></item><item><title>LLM Evaluation: Stop Shipping on Vibes</title><link>https://lawzava.com/blog/2024-02-19-evaluating-llm-applications/</link><pubDate>Mon, 19 Feb 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-02-19-evaluating-llm-applications/</guid><description>Your LLM feature looks great in demos and breaks in production. Here is how to build an evaluation loop that catches regressions before your users do.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>If your evaluation process is &ldquo;I tried a few prompts and it seemed fine,&rdquo; you don&rsquo;t have evaluation. You have hope. Build a small test set, automate checks, monitor production, and block deploys that regress. It isn&rsquo;t hard. It&rsquo;s just work nobody wants to do.</p>
<hr>
<p>I was on a call last month with a team. They had an AI-powered document analysis feature and wanted help figuring out why users were complaining about accuracy. My first question: &ldquo;What does your evaluation suite look like?&rdquo;</p>
<p>Silence. Then: &ldquo;We test it manually before releases.&rdquo;</p>
<p>That isn&rsquo;t evaluation. That&rsquo;s a prayer.</p>
<h2 id="the-core-problem">The core problem</h2>
<p>LLMs are convincing even when they&rsquo;re wrong. A hallucinated answer looks exactly like a correct one to someone who doesn&rsquo;t already know the answer. This makes casual testing actively dangerous &ndash; it gives you false confidence.</p>
<p>The non-determinism makes it worse. Change one word in a system prompt and the behavior shifts in ways you can&rsquo;t predict by reading the diff. The only way to know whether a change helped or hurt is to measure it against a stable reference.</p>
<h2 id="what-to-actually-measure">What to actually measure</h2>
<p>Not everything matters equally. I&rsquo;ve seen teams build elaborate dashboards with dozens of metrics that nobody looks at. Start with the signals that map directly to user value.</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>What it tells you</th>
          <th>When it matters</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Task success rate</td>
          <td>Does the feature accomplish what users need?</td>
          <td>Always</td>
      </tr>
      <tr>
          <td>Format compliance</td>
          <td>Can downstream systems parse the output?</td>
          <td>Structured output, pipelines</td>
      </tr>
      <tr>
          <td>Factual accuracy</td>
          <td>Is the output correct?</td>
          <td>Knowledge-heavy features</td>
      </tr>
      <tr>
          <td>Safety compliance</td>
          <td>Does the output follow policy?</td>
          <td>User-facing, sensitive domains</td>
      </tr>
      <tr>
          <td>Latency (p50/p95)</td>
          <td>Is the feature fast enough?</td>
          <td>Interactive features</td>
      </tr>
      <tr>
          <td>Cost per task</td>
          <td>Is this economically viable?</td>
          <td>High-volume features</td>
      </tr>
  </tbody>
</table>
<p>Keep the list short. Four to six metrics is plenty. If you can&rsquo;t explain why a metric is on the list, remove it.</p>
<h2 id="build-a-test-set-that-looks-like-reality">Build a test set that looks like reality</h2>
<p>This is where most teams cut corners, and it shows. A test set of five happy-path examples tells you nothing useful. You need cases that reflect the actual distribution of inputs your feature sees in production.</p>
<p>What a decent test set includes:</p>
<ul>
<li><strong>Typical cases.</strong> The bread-and-butter inputs that make up 80% of traffic.</li>
<li><strong>Edge cases.</strong> Long inputs, short inputs, ambiguous inputs, inputs in unexpected formats.</li>
<li><strong>Known failure modes.</strong> Cases that broke in the past. These are gold.</li>
<li><strong>Adversarial inputs.</strong> Prompt injection attempts, confusing instructions, contradictory context.</li>
</ul>
<p>Tag every case with a category. This prevents your overall score from hiding category-level failures. I&rsquo;ve seen a system score 90% overall while completely failing on one important category because the other categories were easy.</p>
<p>Start with 30-50 cases. That&rsquo;s enough to catch major regressions. Grow it as you learn.</p>
<h2 id="the-evaluation-methods-compared">The evaluation methods compared</h2>
<p>There&rsquo;s no single evaluation technique that works for everything. The right approach depends on what you&rsquo;re measuring.</p>
<table>
  <thead>
      <tr>
          <th>Method</th>
          <th>Speed</th>
          <th>Consistency</th>
          <th>Best for</th>
          <th>Limitations</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exact match</td>
          <td>Instant</td>
          <td>Perfect</td>
          <td>Structured output, classifications</td>
          <td>Useless for open-ended tasks</td>
      </tr>
      <tr>
          <td>Rule-based checks</td>
          <td>Instant</td>
          <td>Perfect</td>
          <td>Format validation, required fields</td>
          <td>Can&rsquo;t judge quality or nuance</td>
      </tr>
      <tr>
          <td>Model-as-judge</td>
          <td>Fast</td>
          <td>Good (but noisy)</td>
          <td>Open-ended quality, tone, relevance</td>
          <td>Needs calibration, can drift</td>
      </tr>
      <tr>
          <td>Human review</td>
          <td>Slow</td>
          <td>Variable</td>
          <td>Subjective quality, edge cases</td>
          <td>Expensive, doesn&rsquo;t scale</td>
      </tr>
      <tr>
          <td>A/B testing (production)</td>
          <td>Slow</td>
          <td>Good (with volume)</td>
          <td>Real-world impact</td>
          <td>Requires traffic, slow feedback</td>
      </tr>
  </tbody>
</table>
<p>My recommendation: layer them. Use exact match and rule-based checks for everything you can. Use model-as-judge for quality on open-ended outputs, but calibrate it monthly against human reviewers. Reserve human review for cases where the automated signals disagree or when you&rsquo;re exploring a new failure mode.</p>
<h2 id="offline-vs-online-different-jobs">Offline vs. online: different jobs</h2>
<p>This distinction matters more than most people realize.</p>
<p><strong>Offline evaluation</strong> runs during development. It answers: &ldquo;Did this prompt change improve behavior on known cases?&rdquo; Run it before every deploy. Run it when you change prompts, retrieval logic, or model versions. It&rsquo;s your regression gate.</p>
<p><strong>Online evaluation</strong> runs in production. It answers: &ldquo;Does this actually work for real users with real inputs?&rdquo; Monitor task success, collect user signals (did they accept, edit, or reject the output?), and track drift over time.</p>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th>Offline</th>
          <th>Online</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Purpose</td>
          <td>Catch regressions</td>
          <td>Validate real-world quality</td>
      </tr>
      <tr>
          <td>Data source</td>
          <td>Curated test set</td>
          <td>Production traffic</td>
      </tr>
      <tr>
          <td>Timing</td>
          <td>Pre-deploy</td>
          <td>Continuous</td>
      </tr>
      <tr>
          <td>Feedback speed</td>
          <td>Minutes</td>
          <td>Hours to days</td>
      </tr>
      <tr>
          <td>Blind spots</td>
          <td>Can&rsquo;t predict novel inputs</td>
          <td>Hard to attribute cause</td>
      </tr>
  </tbody>
</table>
<p>You need both. A clean offline score without production monitoring is a false sense of security. I&rsquo;ve personally seen features pass every offline test and fail in production because the test set didn&rsquo;t represent the actual input distribution.</p>
<h2 id="operationalize-it-or-it-dies">Operationalize it or it dies</h2>
<p>Evaluation that lives in a notebook and runs when someone remembers isn&rsquo;t evaluation. It&rsquo;s a side project. Make it part of the delivery process.</p>
<p>The loop I use:</p>
<ol>
<li><strong>Maintain a baseline.</strong> Your current production version&rsquo;s scores on the test set. This is the bar.</li>
<li><strong>Run evals on every change.</strong> Prompt edits, model swaps, retrieval changes &ndash; all of it gets measured.</li>
<li><strong>Block deploys that regress.</strong> Not on every metric &ndash; pick the ones that matter and set thresholds.</li>
<li><strong>Refresh the test set.</strong> Add cases from production failures. Remove cases that no longer match product goals. Monthly is a good cadence.</li>
<li><strong>Review model-as-judge calibration.</strong> Monthly, have a human review a sample of the judge&rsquo;s ratings. Adjust the grading prompt if it drifted.</li>
</ol>
<p>The tooling to do this isn&rsquo;t exotic. A script that runs your test set through the system, compares outputs to expected behavior, and produces a report. I&rsquo;ve built these in a few hundred lines of Go. The hard part isn&rsquo;t the code. It&rsquo;s the discipline to actually run it every time.</p>
<h2 id="the-gap-is-discipline-not-tooling">The gap is discipline, not tooling</h2>
<p>I keep coming back to this. The tools exist. The techniques are well-understood. The test sets aren&rsquo;t that hard to build. What&rsquo;s missing is the organizational willingness to treat AI output quality with the same rigor as test coverage or uptime.</p>
<p>If you wouldn&rsquo;t ship a backend service without tests, you shouldn&rsquo;t ship an AI feature without evaluation. Same principle. Same discipline. Different domain.</p>
<p>Build the test set. Automate the checks. Block the regressions. Everything else is details.</p>
]]></content:encoded></item><item><title>Architecting AI-Native Applications (Without the Delusion)</title><link>https://lawzava.com/blog/2024-02-05-ai-native-architecture/</link><pubDate>Mon, 05 Feb 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-02-05-ai-native-architecture/</guid><description>AI-native apps are fundamentally different from a model bolted onto a CRUD app. How I structure them &amp;amp;ndash; with code, layers, and hard-won opinions.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI-native means the model is in the critical path, not a sidebar. That requires confidence-aware routing, structured feedback loops, explicit fallback chains, and a UX that doesn&rsquo;t pretend the system is deterministic. This is the architecture I use.</p>
<hr>
<p>There&rsquo;s a particular kind of architectural diagram I keep seeing in pitch decks. A clean box labeled &ldquo;AI&rdquo; sits neatly between the frontend and the database, connected by two arrows. Everything looks tidy. Everything is a lie.</p>
<p>AI-native applications are messy. The model is non-deterministic. Responses vary in quality. Latency is unpredictable. Costs scale with usage in ways that don&rsquo;t match traditional compute. And yet &ndash; the product&rsquo;s core value depends on this unreliable component working well enough, often enough, that users trust it.</p>
<p>I&rsquo;ve been building these systems for the past year across telcos and fintech companies. The architecture that actually works looks nothing like that clean diagram.</p>
<h2 id="what-ai-native-actually-means">What &ldquo;AI-native&rdquo; actually means</h2>
<p>Let me be precise. An AI-native application is one where removing the AI component wouldn&rsquo;t leave you with a simpler app &ndash; it would leave you with no app. The AI isn&rsquo;t a feature. It&rsquo;s the product.</p>
<p>This creates three architectural consequences you can&rsquo;t ignore:</p>
<ol>
<li><strong>Non-determinism is in the critical path.</strong> The same input can produce different outputs. Your architecture must absorb this instead of pretending it away.</li>
<li><strong>Quality is a spectrum, not a boolean.</strong> You evaluate on ranges and intent, not exact matches.</li>
<li><strong>The system must learn from usage.</strong> Feedback isn&rsquo;t a nice-to-have &ndash; it&rsquo;s what keeps the product from degrading.</li>
</ol>
<h2 id="the-layered-architecture-i-actually-use">The layered architecture I actually use</h2>
<p>After building several of these systems, I&rsquo;ve settled on a layered approach. Not because layers are fashionable, but because each layer has a distinct failure mode and a distinct owner.</p>
<pre tabindex="0"><code>┌─────────────────────────────────────┐
│         Experience Layer            │  &lt;- Uncertainty communication, UI
├─────────────────────────────────────┤
│       Orchestration Layer           │  &lt;- Routing, fallbacks, workflows
├─────────────────────────────────────┤
│         AI Services Layer           │  &lt;- Model calls, retrieval, tools
├─────────────────────────────────────┤
│      Quality &amp; Safety Layer         │  &lt;- Validation, filtering, policy
├─────────────────────────────────────┤
│       Data &amp; Context Layer          │  &lt;- Knowledge, memory, embeddings
├─────────────────────────────────────┤
│     Feedback &amp; Analytics Layer      │  &lt;- Learning, monitoring, eval
└─────────────────────────────────────┘
</code></pre><p>These don&rsquo;t need to be separate services. In most systems I build, they start as packages within a single Go binary. The point is that each responsibility exists, is testable, and has clear ownership.</p>
<h2 id="designing-for-uncertainty">Designing for uncertainty</h2>
<p>This is the part most teams get wrong. They treat the model like a function: input goes in, correct output comes out. Then they&rsquo;re shocked when production users get hallucinated garbage.</p>
<p>The architecture needs to absorb uncertainty at every level. Here is how I handle it in the orchestration layer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Confidence</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceHigh</span>   <span style="color:#a6e22e">Confidence</span> = <span style="color:#66d9ef">iota</span> <span style="color:#75715e">// Route directly to user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceMedium</span>                    <span style="color:#75715e">// Add verification step</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceLow</span>                       <span style="color:#75715e">// Escalate or fallback</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">AIResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Content</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Confidence</span> <span style="color:#a6e22e">Confidence</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Latency</span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">TokensUsed</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Service</span>) <span style="color:#a6e22e">HandleRequest</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">aiResp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">aiClient</span>.<span style="color:#a6e22e">Generate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">ToPrompt</span>())
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">fallbackResponse</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">aiResp</span>.<span style="color:#a6e22e">Confidence</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceHigh</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">directResponse</span>(<span style="color:#a6e22e">aiResp</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceMedium</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">verified</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">verify</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">aiResp</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">directResponse</span>(<span style="color:#a6e22e">aiResp</span>), <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// Degrade gracefully</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">verified</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceLow</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">escalate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">aiResp</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">fallbackResponse</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Confidence doesn&rsquo;t need to be a number shown to the user. It&rsquo;s an internal signal that controls what happens next. High confidence goes straight through. Medium confidence gets a verification step &ndash; maybe a retrieval check, maybe a second model call with a stricter prompt. Low confidence hits the fallback path.</p>
<p>The fallback path is critical. Every AI-native app needs one, and it should be designed before the happy path. What does the product do when the model is down? When it returns garbage? When it takes 30 seconds to respond? If the answer is &ldquo;crash&rdquo; or &ldquo;show a spinner forever,&rdquo; the architecture isn&rsquo;t ready for production.</p>
<h2 id="feedback-loops-as-architecture-not-afterthought">Feedback loops as architecture, not afterthought</h2>
<p>Every request through the system should produce a feedback record. Not because you have time to look at them all, but because without them you&rsquo;re blind to degradation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FeedbackRecord</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">RequestID</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Prompt</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Response</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>     <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Confidence</span>  <span style="color:#a6e22e">Confidence</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Latency</span>     <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">UserSignal</span>  <span style="color:#a6e22e">UserSignal</span>  <span style="color:#75715e">// Accepted, rejected, edited, ignored</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Outcome</span>     <span style="color:#a6e22e">Outcome</span>     <span style="color:#75715e">// Success, partial, failure</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Timestamp</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UserSignal</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalNone</span>     <span style="color:#a6e22e">UserSignal</span> = <span style="color:#66d9ef">iota</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalAccepted</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalRejected</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalEdited</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalIgnored</span>
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The user signal is the most valuable field. Did the user accept the output? Edit it? Ignore it entirely? That data drives everything: prompt improvements, model selection changes, confidence calibration.</p>
<p>I learned this the hard way on a project where we shipped an AI feature without feedback instrumentation. Two months later, we had no idea whether the model&rsquo;s quality had drifted or whether users had simply stopped trusting it. We were debugging with anecdotes. Never again.</p>
<h2 id="routing-without-the-phd">Routing without the PhD</h2>
<p>You don&rsquo;t need a machine learning model to route requests to the right model. A few rules go a long way.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RouterConfig</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Rules</span> []<span style="color:#a6e22e">RoutingRule</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RoutingRule</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Condition</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Timeout</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxTokens</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">DefaultRouter</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">RouterConfig</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">RouterConfig</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Rules</span>: []<span style="color:#a6e22e">RoutingRule</span>{
</span></span><span style="display:flex;"><span>			{
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">TokenEstimate</span>() &lt; <span style="color:#ae81ff">200</span> },
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;fast-small&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">512</span>,
</span></span><span style="display:flex;"><span>			},
</span></span><span style="display:flex;"><span>			{
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">RequiresReasoning</span>() },
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;capable-large&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <span style="color:#ae81ff">30</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">4096</span>,
</span></span><span style="display:flex;"><span>			},
</span></span><span style="display:flex;"><span>			{
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> }, <span style="color:#75715e">// Default</span>
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;balanced-medium&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <span style="color:#ae81ff">15</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">2048</span>,
</span></span><span style="display:flex;"><span>			},
</span></span><span style="display:flex;"><span>		},
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Small requests get the fast model. Reasoning-heavy requests get the capable one. Everything else gets the balanced option. This isn&rsquo;t clever. It doesn&rsquo;t need to be. It just needs to keep costs predictable and latency acceptable.</p>
<p>The rules are configuration, not code. When you want to change routing &ndash; because a new model dropped, or costs shifted, or you learned that certain request types need more capability &ndash; you change the config. You don&rsquo;t redeploy.</p>
<h2 id="ux-that-respects-the-users-intelligence">UX that respects the user&rsquo;s intelligence</h2>
<p>The biggest UX mistake in AI-native apps is pretending the system is certain when it isn&rsquo;t. Users can handle uncertainty. They can&rsquo;t handle being lied to.</p>
<p>A few principles I follow:</p>
<ul>
<li><strong>Show your work when confidence is low.</strong> If the model retrieved documents to answer a question, show which ones. Let the user verify.</li>
<li><strong>Offer refinement, not just results.</strong> A &ldquo;try again&rdquo; button is lazy. A &ldquo;here is what I found, want me to focus on X?&rdquo; is useful.</li>
<li><strong>Keep the UI stable on failure.</strong> When the model times out, the product should still work. Maybe with reduced functionality, but it shouldn&rsquo;t break.</li>
</ul>
<p>The best AI-native UIs I&rsquo;ve seen treat the model like a very fast but occasionally wrong colleague. You check their work on important things. You trust them on routine things. The UI should support that mental model.</p>
<h2 id="the-data-layer-determines-everything">The data layer determines everything</h2>
<p>I have a saying I repeat in these situations: your AI feature is only as good as the data you feed it.</p>
<p>The context layer needs to support structured facts (database records, configuration), unstructured knowledge (documents, guides, prior conversations), and session memory (what happened earlier in this interaction).</p>
<p>Retrieval quality matters more than model quality for most applications. I&rsquo;ve seen teams spend weeks prompt-engineering their way around a bad retrieval pipeline. Fix the retrieval. The prompts will get simpler.</p>
<h2 id="operational-discipline">Operational discipline</h2>
<p>Production AI-native apps need monitoring that goes beyond uptime checks:</p>
<ul>
<li><strong>Quality monitoring.</strong> Track your confidence distribution over time. If low-confidence responses are increasing, something changed.</li>
<li><strong>Cost tracking per request type.</strong> Not aggregate cost &ndash; per-type. You need to know which workflows are expensive.</li>
<li><strong>Latency budgets.</strong> Set them per workflow, not globally. A search feature and a document analysis feature have different acceptable latencies.</li>
<li><strong>Drift detection.</strong> Model behavior changes. Provider behavior changes. Your data changes. Monitor for all of it.</li>
</ul>
<h2 id="the-honest-version">The honest version</h2>
<p>AI-native architecture isn&rsquo;t a clean diagram. It&rsquo;s a set of hard choices about where to trust the model, where to verify, where to fall back, and how to learn from every interaction. The teams that accept this build reliable products. The teams that draw clean boxes build impressive demos that break in production.</p>
<p>Build the fallback first. Instrument everything. Let the feedback loop make the system smarter over time. That&rsquo;s the architecture that actually ships.</p>
]]></content:encoded></item><item><title>Stop Paying OpenAI to Test Your Prompts</title><link>https://lawzava.com/blog/2024-01-22-local-llms-development/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-01-22-local-llms-development/</guid><description>Local LLMs are finally good enough for development. Use them for iteration, keep the API bills for production.</description><content:encoded><![CDATA[<p>I keep watching developers iterate on prompts by hitting GPT-4 hundreds of times a day. Every keystroke, another API call. Every experiment, another line on the invoice. Then they act surprised when the monthly bill shows up.</p>
<p>This is dumb. Not because the hosted models are bad &ndash; they are great. But because you don&rsquo;t need frontier-model quality to test whether your prompt template works, your parsing logic handles edge cases, or your UI renders a streamed response correctly.</p>
<p>Run a local model. Iterate fast. Save the API calls for when you actually need them.</p>
<h2 id="the-actual-reasons-to-go-local">The actual reasons to go local</h2>
<p>Forget the hand-wavy &ldquo;sovereignty&rdquo; arguments for a moment. The practical reasons are simple:</p>
<p><strong>Speed.</strong> No network round-trip. No rate limits. No waiting in a queue behind someone else&rsquo;s batch job. I can test a prompt change in under a second on a MacBook with Ollama running a 7B model. That feedback loop matters when you&rsquo;re doing fifty iterations in an afternoon.</p>
<p><strong>Cost.</strong> Zero marginal cost per request. I ran through over a thousand prompt variations last month while building an extraction pipeline. On GPT-4, that would have been a few hundred dollars. Locally, it was electricity.</p>
<p><strong>Privacy.</strong> Some of my work involves data I can&rsquo;t send to a third-party API. Full stop. Local inference solves that problem without paperwork.</p>
<h2 id="the-trade-offs-are-real-so-stop-pretending-otherwise">The trade-offs are real, so stop pretending otherwise</h2>
<p>Local models aren&rsquo;t frontier models. A 7B parameter model running on your laptop isn&rsquo;t going to match GPT-4 on complex reasoning tasks. That&rsquo;s fine. You aren&rsquo;t using it for production quality &ndash; you&rsquo;re using it for development velocity.</p>
<p>Where local models genuinely fall short:</p>
<ul>
<li>Multi-step reasoning. They lose the thread.</li>
<li>Long context windows. Most local models tap out well before 128k tokens.</li>
<li>Consistent formatting. They drift more on structured output tasks.</li>
<li>Nuanced instruction following. Subtle prompt changes sometimes get ignored.</li>
</ul>
<p>If your development workflow requires frontier-quality responses at every step, local models aren&rsquo;t for you. But honestly, most development workflows don&rsquo;t. You need a model that&rsquo;s good enough to validate your integration logic, and local models clear that bar easily.</p>
<h2 id="my-actual-setup">My actual setup</h2>
<p>I keep it simple. Ollama for the runtime, a 7B model as default, and an environment variable to swap between local and remote.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getLLMConfig</span>() <span style="color:#a6e22e">LLMConfig</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;USE_LOCAL_LLM&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;true&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">LLMConfig</span>{
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">BaseURL</span>: <span style="color:#e6db74">&#34;http://localhost:11434&#34;</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Model</span>:   <span style="color:#e6db74">&#34;mistral&#34;</span>,
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">LLMConfig</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">BaseURL</span>: <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;LLM_API_URL&#34;</span>),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Model</span>:   <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;LLM_MODEL&#34;</span>),
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That&rsquo;s it. The rest of the application doesn&rsquo;t care which model it&rsquo;s talking to. The interface is the same, the error handling is the same, the retry logic is the same. When I want to validate quality against the real model, I flip the variable and run my eval suite.</p>
<h2 id="the-workflow-that-actually-works">The workflow that actually works</h2>
<ol>
<li><strong>Develop locally.</strong> Prompt changes, parsing logic, UI work, error handling. All against the local model.</li>
<li><strong>Eval against remote.</strong> Before merging, run the same test cases against the production model. Compare outputs.</li>
<li><strong>Ship with confidence.</strong> The integration is tested. The quality is validated. The bill is reasonable.</li>
</ol>
<p>The key insight: your development model and your production model don&rsquo;t need to be the same. They need to share the same interface.</p>
<h2 id="when-to-skip-local-entirely">When to skip local entirely</h2>
<p>Be honest about the cases where local doesn&rsquo;t help:</p>
<ul>
<li>You&rsquo;re doing few-shot prompt engineering where response quality <em>is</em> the variable you&rsquo;re testing.</li>
<li>Your feature depends on capabilities only frontier models have (vision, very long context, tool use with complex chains).</li>
<li>You&rsquo;re evaluating model-specific behavior like safety responses or refusal patterns.</li>
</ul>
<p>In those cases, just use the API. The point isn&rsquo;t religious purity about local inference. The point isn&rsquo;t burning money on API calls when a local model would have told you the same thing.</p>
<h2 id="stop-overthinking-it">Stop overthinking it</h2>
<p>Install Ollama. Pull a model. Point your dev config at localhost. You will iterate faster, spend less, and keep sensitive data on your own machine. When you need the real thing, it&rsquo;s one environment variable away.</p>
<p>This isn&rsquo;t complicated. It&rsquo;s just discipline.</p>
]]></content:encoded></item><item><title>AI Engineering Is Its Own Discipline Now</title><link>https://lawzava.com/blog/2024-01-08-ai-engineering-discipline/</link><pubDate>Mon, 08 Jan 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-01-08-ai-engineering-discipline/</guid><description>AI engineering is not ML research with a product hat. It is the discipline of making models behave in production &amp;amp;ndash; and it demands its own skill set.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop hiring ML researchers to do integration work. AI engineering is the craft of turning probabilistic models into reliable product features. Different job, different skills, different mindset.</p>
<hr>
<p>After a year of working on AI integration across different organizations, the pattern I keep seeing is the same: a team hires a machine learning engineer, points them at a product feature, and wonders why the result is a brilliant notebook that falls apart the moment a real user touches it.</p>
<p>The problem isn&rsquo;t the engineer. The problem is a category error.</p>
<h2 id="this-isnt-ml-this-isnt-backend-its-its-own-thing">This isn&rsquo;t ML. This isn&rsquo;t backend. It&rsquo;s its own thing.</h2>
<p>AI engineering sits in an awkward gap. On one side, you have model training &ndash; the research-heavy work of building and improving models. On the other, traditional software engineering &ndash; APIs, databases, deployment pipelines, the stuff we&rsquo;ve been doing for decades.</p>
<p>AI engineering is neither. It&rsquo;s the work of taking someone else&rsquo;s model and making it do something useful, reliably, in production. That means prompt design, retrieval pipelines, evaluation harnesses, cost management, safety guardrails, and graceful failure handling. It means caring deeply about the 2% of cases where the model confidently produces garbage.</p>
<p>I spent years building backend systems across fintech and cloud infrastructure. The shift to AI engineering felt familiar in some ways &ndash; you still think about latency, error handling, observability. But the non-determinism changes everything. You can&rsquo;t unit test your way to confidence when the same input produces different outputs on Tuesday.</p>
<h2 id="the-skill-set-looks-different">The skill set looks different</h2>
<p>When I talk to CTOs about what to look for in AI engineering hires, I push them away from the classic ML job description. The competencies that actually matter are:</p>
<ul>
<li><strong>Prompt design and testing.</strong> Not prompt &ldquo;engineering&rdquo; as a parlor trick. Systematic testing across edge cases, with version control and regression detection.</li>
<li><strong>Retrieval and context assembly.</strong> Getting the right information to the model at the right time. This is where most applications succeed or fail.</li>
<li><strong>Integration discipline.</strong> Error handling, latency budgets, fallback paths. The boring stuff that separates demos from products.</li>
<li><strong>Evaluation loops.</strong> If you can&rsquo;t measure whether your AI feature got better or worse after a change, you aren&rsquo;t doing engineering. You&rsquo;re doing improv.</li>
<li><strong>Safety and guardrails.</strong> Especially when the model can take actions or access private data.</li>
</ul>
<p>None of this requires a PhD. It requires someone who has shipped software, understands production systems, and has the patience to wrangle probabilistic outputs into predictable behavior.</p>
<h2 id="its-a-set-of-responsibilities-not-a-stack">It&rsquo;s a set of responsibilities, not a stack</h2>
<p>People keep trying to draw AI engineering as a neat layer diagram. In practice, it&rsquo;s a set of cross-cutting responsibilities. You&rsquo;re choosing models, preparing data, shaping prompts, monitoring quality, controlling costs, and enforcing safety &ndash; all at once. The reason the role feels distinct is that it spans product thinking, system design, and ongoing operational care in a way that neither pure ML nor pure backend roles typically do.</p>
<p>At one large telecom, I watched teams try to split these responsibilities across existing roles. The ML team owned prompts. The backend team owned integration. The product team owned evaluation. Nobody owned the whole thing. The result was predictable: finger-pointing when quality dropped and no single person who could trace a bad output from user input to model response to product impact.</p>
<h2 id="how-to-actually-build-these-skills">How to actually build these skills</h2>
<p>Depth beats breadth. Don&rsquo;t chase every new framework or technique. A solid path:</p>
<ol>
<li>Build a feature that calls a model and returns something useful. Ship it.</li>
<li>Add retrieval so the model&rsquo;s answers are grounded in real data instead of vibes.</li>
<li>Build an evaluation loop that catches regressions before your users do.</li>
<li>Add guardrails and define what happens when the model fails. Because it will.</li>
</ol>
<p>The practice is learned by shipping and iterating. Blog posts help (including this one, I hope), but they aren&rsquo;t a substitute for watching your carefully crafted prompt fall apart on production traffic.</p>
<h2 id="where-this-fits-in-your-org">Where this fits in your org</h2>
<p>In smaller teams, AI engineering looks like a product-focused engineer who owns the AI feature end to end. At larger companies, it becomes a dedicated role that sits between product, platform, and security.</p>
<p>The interaction model is clean. Product defines intent and user experience. Platform provides infrastructure and monitoring. Security sets the safety bar. AI engineering turns those constraints into working features that don&rsquo;t embarrass anyone.</p>
<p>The demand for this role is growing fast. Job descriptions are finally separating AI engineering from ML research, and the expectations center on integration, evaluation, and reliability rather than paper-publishing and model architecture. Good. That separation was overdue.</p>
<h2 id="the-discipline-not-the-hype">The discipline, not the hype</h2>
<p>AI engineering isn&rsquo;t a buzzword rotation. It&rsquo;s the recognition that making models useful in production is real engineering work &ndash; with its own tools, its own failure modes, and its own career path. The teams that treat it as a distinct discipline are shipping better features. The teams that don&rsquo;t are still arguing about whether their demo &ldquo;works.&rdquo;</p>
<p>Discipline over heroics. That&rsquo;s the whole game.</p>
]]></content:encoded></item><item><title>2023: The Year Everything Changed (and I Barely Kept Up)</title><link>https://lawzava.com/blog/2023-12-25-year-in-review-2023/</link><pubDate>Mon, 25 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-25-year-in-review-2023/</guid><description>A personal look back at 2023 &amp;amp;ndash; watching AI reshape the industry in real time, and figuring out what matters next.</description><content:encoded><![CDATA[<p>I&rsquo;m writing this on Christmas morning with coffee that&rsquo;s too hot and a year that went too fast. 2023 was the most professionally intense year since I left a deep-tech founder program in 2019 and started figuring out what kind of career I actually wanted. This year I found out.</p>
<h2 id="fintech-infrastructure">Fintech Infrastructure</h2>
<p>The biggest thread of 2023 for me was working on open-source financial ledger infrastructure. The kind of work where correctness isn&rsquo;t a nice-to-have &ndash; it&rsquo;s the entire point. Every line of code I touched had to be right because the alternative was someone&rsquo;s money being wrong.</p>
<p>I came in to help with their Go codebase and ended up deep at the intersection of financial systems and AI. The question that kept coming up: can we use AI to help users interact with the ledger? To query transactions in natural language? To catch anomalies? The answer, frustratingly, was &ldquo;sort of, but not the way you think.&rdquo;</p>
<p>AI in fintech isn&rsquo;t a feature you bolt on. It&rsquo;s an engineering challenge that touches trust, auditability, and regulatory compliance at every level. I spent months thinking about how to make AI features that are safe enough for financial data. I&rsquo;m still thinking about it.</p>
<p>The team was exceptional. Small, focused, opinionated about the right things. Working with open-source infrastructure reminded me why I love building tools for developers. The feedback loop is honest. If your tool is bad, people will tell you. If it&rsquo;s good, they will contribute.</p>
<h2 id="the-ai-explosion">The AI Explosion</h2>
<p>I don&rsquo;t need to tell you what happened in AI this year. You were there. But living through it as someone who builds production systems was a specific kind of experience.</p>
<p>January started with everyone experimenting. By March, teams were asking when they could ship AI features. By summer, the questions changed from &ldquo;should we use AI&rdquo; to &ldquo;how do we make it reliable enough for production.&rdquo; By November, OpenAI DevDay reset the baseline for what the platform provides out of the box.</p>
<p>The speed was genuinely disorienting. I wrote a blog post about agent architecture in September and parts of it felt outdated by November. I built a RAG pipeline in October and the Assistants API made half of it unnecessary in November. The technical landscape shifted faster than I could blog about it.</p>
<p>What I learned: the teams that did well in 2023 weren&rsquo;t the ones who moved fastest. They were the ones who picked a lane, built evaluation infrastructure, and iterated with discipline. The teams that chased every new capability announcement ended up with half-built features and no quality baseline.</p>
<h2 id="reflections">Reflections</h2>
<p>This year cemented something I&rsquo;ve been discovering over the last few years: I like going deep on a problem, building something that works, and then moving on to the next challenge. The variety keeps me sharp. Working on fintech infrastructure, thinking about security from my national cyber-defense background, contributing to Go upstream &ndash; the breadth makes me a better engineer on each individual project.</p>
<p>The downside is context switching. Some weeks I had different codebases open and had to remember which architecture decisions belonged to which project. I&rsquo;ve gotten better at it. My secret: extensive notes. Not fancy systems. Just a text file per project with decisions, open questions, and things that confused me. Future me always appreciates past me&rsquo;s notes.</p>
<h2 id="go">Go</h2>
<p>I kept contributing to the Go ecosystem. Nothing dramatic &ndash; bug fixes, documentation improvements, the kind of work that keeps an open-source project healthy. Go remains my language of choice for production systems. It&rsquo;s boring in the best way. The code I write in Go today looks like the code I wrote three years ago, and that&rsquo;s a feature, not a bug.</p>
<p>The AI tooling landscape in Go is still immature compared to Python. I find myself writing Go wrappers around Python services more than I&rsquo;d like. But I&rsquo;d rather have a reliable Go service calling a Python sidecar than a Python monolith that I have to babysit.</p>
<h2 id="what-stayed-hard">What Stayed Hard</h2>
<p>Evaluation. I wrote about it multiple times this year because it remained the hardest unsolved problem in AI engineering. Everyone agrees it matters. Nobody has a great solution for multi-step workflows. I got better at building lightweight eval suites, but they&rsquo;re still more art than science.</p>
<p>Trust. One confidently wrong answer can undo weeks of user adoption. I saw this happen at two different companies this year. The AI feature was great 95% of the time and catastrophically wrong 5% of the time, and users only remembered the 5%.</p>
<p>Cost management. Token-based pricing sounds simple until you multiply it by production volume and realize your prompt changes have budget implications. I now review prompt changes like I review infrastructure changes &ndash; with a cost estimate attached.</p>
<h2 id="looking-at-2024">Looking at 2024</h2>
<p>I don&rsquo;t do predictions. But I know what I&rsquo;m going to focus on: making AI systems more reliable and more auditable. The hype cycle will do what hype cycles do. The engineering work of making these systems trustworthy is the real job, and it&rsquo;s the job I want to be doing.</p>
<p>2023 was the year AI became real. 2024 will be the year we find out if it can stay real.</p>
<p>Happy holidays. Go take a break. The codebase will be there when you get back.</p>
]]></content:encoded></item><item><title>Your AI Infrastructure Is Not Ready for Scale. Neither Is Mine.</title><link>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</guid><description>GPU shortage is real, rate limits are a production constraint, and your AI demo will collapse under real traffic. Annoyed thoughts on infrastructure realism.</description><content:encoded><![CDATA[<p>I&rsquo;m going to be blunt: the state of AI infrastructure heading into 2024 is embarrassing.</p>
<p>We have models that can write poetry, generate code, and analyze images. We don&rsquo;t have enough GPUs to run them reliably. We don&rsquo;t have pricing that makes sense at scale. And we definitely don&rsquo;t have the operational maturity to treat these systems like the production dependencies they have become.</p>
<p>I&rsquo;ve spent December watching AI features I helped build at a fintech company run into every scaling problem distributed systems teams have been solving for twenty years. Rate limits. Cascading failures. Cost explosions. Latency spikes. The problems aren&rsquo;t new. The industry is just re-learning them with a fresh coat of hype.</p>
<h2 id="the-gpu-situation-is-absurd">The GPU Situation Is Absurd</h2>
<p>You can&rsquo;t get H100s. You can&rsquo;t reliably get inference capacity from any major provider unless you sign a months-long commitment or an enterprise contract that costs more than most startups raise in a seed round. The entire industry is building products on top of infrastructure that&rsquo;s supply-constrained, and nobody wants to talk about what happens when demand doubles next year.</p>
<p>I tried to reserve inference capacity for a production workload last month. The response from one provider was &ldquo;we can put you on a waitlist.&rdquo; A waitlist. For compute. In 2023. This isn&rsquo;t a technology problem. It&rsquo;s a supply chain problem wearing a technology costume.</p>
<h2 id="rate-limits-are-a-production-constraint">Rate Limits Are a Production Constraint</h2>
<p>Every AI API has rate limits. At low volume, you don&rsquo;t notice them. At production scale, they become the hardest ceiling in your architecture.</p>
<p>I hit OpenAI&rsquo;s rate limit during a load test and watched requests queue up until the entire feature became unusable. Not degraded &ndash; unusable. The fix wasn&rsquo;t clever engineering. It was a priority queue, backpressure, and load shedding. Distributed systems 101. The fact that most AI teams are learning this for the first time worries me.</p>
<h2 id="your-demo-wont-survive-real-traffic">Your Demo Won&rsquo;t Survive Real Traffic</h2>
<p>Here is what happens when your AI feature goes from 100 requests per day to 10,000:</p>
<p>Latency goes from &ldquo;acceptable&rdquo; to &ldquo;users are closing the tab.&rdquo; Costs go from &ldquo;rounding error&rdquo; to &ldquo;someone just Slacked asking why the API bill tripled.&rdquo; A provider outage that used to affect a handful of test users now takes down a production feature that the sales team just promised to a client.</p>
<p>I&rsquo;ve seen all three of these happen at the same company. In the same month.</p>
<h2 id="what-you-actually-need">What You Actually Need</h2>
<p><strong>Queues and backpressure.</strong> Treat your AI traffic as a managed stream, not an open pipe. Priority queues for critical requests. Backpressure when the system is saturated. Load shedding for low-priority work. This isn&rsquo;t optional once you have real users.</p>
<p><strong>Circuit breakers.</strong> Your model provider will have bad hours. Mine had a bad day last week. Circuit breakers stop a provider outage from cascading through your entire system. They&rsquo;re boring. They&rsquo;re essential. I&rsquo;ve been building systems with circuit breakers since my telecom days. The pattern hasn&rsquo;t changed. The dependency has.</p>
<p><strong>Graceful degradation.</strong> When GPT-4 is down, what happens? If the answer is &ldquo;the feature breaks,&rdquo; you don&rsquo;t have a production system. You have a demo with users. Fall back to cached responses. Fall back to a smaller, faster model. Fall back to a static message that says &ldquo;this feature is temporarily unavailable.&rdquo; Anything is better than a spinning loader.</p>
<p><strong>Cost controls that are actually enforced.</strong> Per-tenant budgets. Per-feature budgets. Daily caps. If you don&rsquo;t enforce them, you&rsquo;ll get a surprise invoice that triggers an emergency meeting. I&rsquo;ve seen a single prompt change &ndash; adding two paragraphs of context &ndash; increase monthly costs by 35%. Token pricing is deceptively simple until you multiply it by production volume.</p>
<p><strong>Caching.</strong> Exact-match caching is trivial to implement and saves real money. Same question, same context, same answer &ndash; serve it from cache. Semantic caching is fancier and worth exploring, but start with the easy wins.</p>
<h2 id="this-is-distributed-systems-work">This Is Distributed Systems Work</h2>
<p>None of this is novel. Queues, circuit breakers, graceful degradation, cost controls, caching &ndash; these are patterns from every distributed systems textbook ever written. The only thing that&rsquo;s new is the dependency type.</p>
<p>What frustrates me is that the AI community is treating infrastructure as a solved problem while building on top of infrastructure that&rsquo;s anything but solved. The models are impressive. The plumbing is held together with optimism and rate limit retries.</p>
<p>Build your AI features like you would build any production system that depends on an unreliable, expensive, supply-constrained external service. Because that&rsquo;s exactly what it is.</p>
]]></content:encoded></item><item><title>Multimodal AI: Five Use Cases That Actually Work (and Three That Do Not)</title><link>https://lawzava.com/blog/2023-12-11-multimodal-ai-applications/</link><pubDate>Mon, 11 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-11-multimodal-ai-applications/</guid><description>GPT-4V is out and everyone is building vision features. After testing it across real workflows, here is what ships well and what falls apart.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Vision-capable models are legitimately useful for document extraction, UI review, and accessibility. They&rsquo;re unreliable for precise measurements, tiny text, and anything that requires counting. Treat it like a smart intern who&rsquo;s great at describing what they see but bad at details. Build for uncertainty, validate outputs, and keep a fallback path.</p>
<p>GPT-4V dropped and my first reaction was to throw every image I could find at it. Receipts. Architecture diagrams. Screenshots. Photos of whiteboards from meetings. The results ranged from &ldquo;holy shit, this actually works&rdquo; to &ldquo;that&rsquo;s confidently wrong in a way that would cost money.&rdquo;</p>
<p>After a few weeks of serious testing, I have a clearer picture of where multimodal AI is ready for production and where it will get you in trouble.</p>
<h2 id="what-actually-ships">What Actually Ships</h2>
<h3 id="1-invoice-and-receipt-extraction">1. Invoice and Receipt Extraction</h3>
<p>This is the killer use case at a fintech company. We process financial documents. Extracting vendor name, amount, date, and line items from a photo of a receipt used to require a dedicated OCR pipeline, post-processing rules, and a prayer. Now I send the image to GPT-4V with a structured prompt and get JSON back.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Analyze this invoice image. Return JSON with these fields:
</span></span><span style="display:flex;"><span>- vendor_name (string)
</span></span><span style="display:flex;"><span>- total_amount (string, include currency)
</span></span><span style="display:flex;"><span>- invoice_date (string, YYYY-MM-DD)
</span></span><span style="display:flex;"><span>- line_items (array of {description, amount})
</span></span><span style="display:flex;"><span>If a field is not visible, return null.
</span></span></code></pre></div><p>Hit rate on clean documents is around 90%. On crumpled receipts with bad lighting, it drops to maybe 65%. Good enough for a first pass with human review on low-confidence results.</p>
<h3 id="2-ui-review">2. UI Review</h3>
<p>I started using it to review screenshots of our admin dashboards. &ldquo;List any layout issues, missing states, or accessibility concerns in this screenshot.&rdquo; The results aren&rsquo;t comprehensive, but they catch obvious problems &ndash; misaligned elements, missing error states, low contrast text &ndash; faster than a manual review pass.</p>
<h3 id="3-accessibility">3. Accessibility</h3>
<p>Alt text generation. Genuinely good at this. Feed it a product image or a chart and ask for a concise description. The output is usually better than what most developers write manually, which is a low bar, but still.</p>
<h3 id="4-architecture-diagram-interpretation">4. Architecture Diagram Interpretation</h3>
<p>This one surprised me. I photographed a whiteboard diagram from a system design session and asked the model to describe the components and data flow. It got the high-level architecture right. Not perfect on every label, but the structure was correct. Useful for converting whiteboard photos into documentation drafts.</p>
<h3 id="5-visual-anomaly-detection">5. Visual Anomaly Detection</h3>
<p>For predictable environments &ndash; &ldquo;does this photo show the expected setup?&rdquo; &ndash; the model is decent at spotting obvious differences. Missing components, wrong configurations, visible damage. It works best when you can describe what &ldquo;normal&rdquo; looks like and ask the model to flag deviations.</p>
<h2 id="what-doesnt-work-yet">What Doesn&rsquo;t Work (Yet)</h2>
<h3 id="counting">Counting</h3>
<p>Ask it to count items in a busy image. Watch it fail. It will confidently give you a number that&rsquo;s wrong. Small objects, overlapping items, dense arrangements &ndash; the model can&rsquo;t reliably count. Don&rsquo;t build features that depend on this.</p>
<h3 id="precise-measurements">Precise Measurements</h3>
<p>&ldquo;How far apart are these two components?&rdquo; The model doesn&rsquo;t do spatial precision. It can tell you something is &ldquo;on the left&rdquo; or &ldquo;near the top&rdquo; but asking for millimeter-level accuracy is asking for trouble.</p>
<h3 id="tiny-or-low-quality-text">Tiny or Low-Quality Text</h3>
<p>Faded labels, handwritten notes in bad lighting, text smaller than about 10px on a screenshot &ndash; all unreliable. The model will either skip the text entirely or hallucinate plausible content. This is the failure mode that scares me most because it&rsquo;s indistinguishable from correct output unless you verify.</p>
<h2 id="the-cost-problem">The Cost Problem</h2>
<p>Vision calls are expensive. A single image analysis costs roughly 10-20x what a text-only call costs, depending on image size and detail level. At scale, this adds up fast.</p>
<p>My rules:</p>
<ul>
<li><strong>Resize aggressively.</strong> Crop to the region of interest. A full-resolution photo of a receipt when all you need is the total amount is wasting tokens and money.</li>
<li><strong>Use low detail mode for simple tasks.</strong> GPT-4V supports a detail parameter. Use &ldquo;low&rdquo; for tasks like &ldquo;is there text in this image?&rdquo; and &ldquo;high&rdquo; only when you need it.</li>
<li><strong>Cache everything.</strong> Same image, same question, same answer. Don&rsquo;t re-process.</li>
<li><strong>Batch when possible.</strong> Multiple questions about the same image should be a single API call, not five separate ones.</li>
</ul>
<h2 id="building-for-uncertainty">Building for Uncertainty</h2>
<p>The single most important design principle: assume the model will be wrong sometimes, and build your product flow to handle it gracefully.</p>
<p>For document extraction at a fintech company, every result goes through a confidence check. If any field comes back null or if the extracted amount doesn&rsquo;t parse as a valid number, it routes to human review. The model handles the easy 70-80% automatically. Humans handle the rest. The total cost is still lower than having humans process everything manually.</p>
<p>Ask the model to cite visible evidence. &ldquo;What text did you read to determine the vendor name?&rdquo; If it can&rsquo;t point to specific text in the image, the answer is probably a hallucination.</p>
<p>Keep an OCR fallback for critical text extraction. The vision model is better at understanding context. Traditional OCR is better at reading exact characters. Use both.</p>
<p>Multimodal AI isn&rsquo;t magic. It&rsquo;s a new tool with a specific reliability profile. Know where it&rsquo;s strong, know where it fails, and design your system to handle both. That&rsquo;s the boring answer. It&rsquo;s also the right one.</p>
]]></content:encoded></item><item><title>Two Weeks With the Assistants API: What I Like, What I Hate</title><link>https://lawzava.com/blog/2023-12-04-building-with-assistants-api/</link><pubDate>Mon, 04 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-04-building-with-assistants-api/</guid><description>I built three things with the Assistants API. One shipped, one got scrapped, and one taught me where the API&amp;amp;rsquo;s limits really are.</description><content:encoded><![CDATA[<p>I&rsquo;ve spent the past two weeks building with the Assistants API. Not toy examples &ndash; actual tools that real people will use. Here is what I found.</p>
<h2 id="the-good-speed-to-something-real">The Good: Speed to Something Real</h2>
<p>I built an internal documentation assistant for a fintech project in about four hours. Upload the docs, write a focused system prompt, wire up a simple Go client that manages threads. Done. The retrieval isn&rsquo;t perfect, but it&rsquo;s good enough for &ldquo;which endpoint handles X&rdquo; type questions. Previously this would have required a vector store, an embedding pipeline, chunking logic, and a retrieval chain. Now it&rsquo;s an API call.</p>
<p>The code interpreter is surprisingly useful. I hooked it up to a tool that lets internal users ask data questions in plain English. &ldquo;How many transactions failed last week?&rdquo; gets translated into Python, executed in OpenAI&rsquo;s sandbox, and the result comes back formatted. It took me a day. Building a safe code execution sandbox from scratch would have taken a week minimum.</p>
<h2 id="the-bad-opacity-everywhere">The Bad: Opacity Everywhere</h2>
<p>The retrieval is a black box. I can&rsquo;t control how it chunks my documents. I can&rsquo;t see what it retrieved before generating an answer. I can&rsquo;t tune the similarity threshold or re-rank results. For the documentation assistant, this is tolerable &ndash; the stakes are low and approximate recall is fine.</p>
<p>For anything involving financial data at the fintech company, it&rsquo;s a non-starter. I need to know exactly what context the model saw. I need to audit the retrieval path. I need to explain to compliance why the system gave a specific answer. The Assistants API can&rsquo;t do any of that.</p>
<p>Thread management is also trickier than it looks. Threads accumulate context over time, and stale context degrades answers. I learned this the hard way when the documentation assistant started mixing up API versions because it was carrying context from a conversation about v1 into a question about v2. Now I have a policy: new thread for every topic change. It&rsquo;s crude but it works.</p>
<h2 id="the-ugly-runs-are-flaky">The Ugly: Runs Are Flaky</h2>
<p>A &ldquo;Run&rdquo; is one execution of an assistant against a thread. It can succeed, fail, stall, or time out. In my first week, I had runs that just&hellip; hung. No error. No timeout. Just pending forever. I added my own timeout logic around every run, with a hard kill after 30 seconds and a retry with a fresh thread if it fails twice.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">30</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">run</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">CreateRun</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">threadID</span>, <span style="color:#a6e22e">assistantID</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;create run: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Poll until complete or timeout.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">status</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">GetRun</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">threadID</span>, <span style="color:#a6e22e">run</span>.<span style="color:#a6e22e">ID</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;check run status: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;completed&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;failed&#34;</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;expired&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;run %s: %s&#34;</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Status</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">LastError</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#ae81ff">500</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This isn&rsquo;t elegant. It works. The API really needs webhooks or server-sent events instead of polling, but we work with what we&rsquo;ve got.</p>
<h2 id="where-im-using-it">Where I&rsquo;m Using It</h2>
<p><strong>Internal tools with low stakes.</strong> Documentation Q&amp;A, data exploration, onboarding helpers. The Assistants API is perfect here. Fast to build, good enough quality, and the opacity doesn&rsquo;t matter because the stakes are low.</p>
<p><strong>Prototypes that need to prove value.</strong> If the question is &ldquo;would this feature be useful?&rdquo; the Assistants API gets you an answer in days instead of weeks. Then you can decide whether to build custom infrastructure for the production version.</p>
<h2 id="where-im-not">Where I&rsquo;m Not</h2>
<p><strong>Anything with compliance requirements.</strong> Financial data, personal information, regulated workflows. If I can&rsquo;t audit the retrieval path and explain every answer, I can&rsquo;t use it.</p>
<p><strong>Anything that needs precise orchestration.</strong> If the workflow involves multiple models, conditional branching, or complex tool chains, the Assistants API is too constrained. You&rsquo;ll fight the abstraction instead of benefiting from it.</p>
<h2 id="the-verdict">The Verdict</h2>
<p>The Assistants API is the right default for a lot of use cases. It&rsquo;s fast, it&rsquo;s cheap, and it handles the boring parts &ndash; thread management, tool execution, file retrieval &ndash; so you don&rsquo;t have to. The cost is control, and for many applications that&rsquo;s a trade worth making.</p>
<p>Just go in with your eyes open. Know what you&rsquo;re giving up. Have a plan for when you need to go custom. And for the love of all that&rsquo;s holy, add your own timeouts.</p>
]]></content:encoded></item><item><title>OpenAI DevDay Happened and I Have Opinions</title><link>https://lawzava.com/blog/2023-11-27-openai-devday-review/</link><pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-11-27-openai-devday-review/</guid><description>OpenAI DevDay was not just a product launch. It was a platform play that changes the build-vs-buy calculus for every team shipping AI features.</description><content:encoded><![CDATA[<p>I was on a call with a fintech company engineer when the DevDay keynote started streaming. We had the livestream on one monitor and a half-finished RAG implementation on the other. About twenty minutes in, we both went quiet. Then he said, &ldquo;So&hellip; do we still need this?&rdquo;</p>
<p>That question &ndash; &ldquo;do we still need this?&rdquo; &ndash; is the real story of DevDay. Not GPT-4 Turbo. Not the Assistants API. Not Custom GPTs. The story is that OpenAI just told every team building on their platform: we&rsquo;re going to own more of the stack now. And you need to decide how you feel about that.</p>
<h2 id="what-actually-shipped">What Actually Shipped</h2>
<p><strong>GPT-4 Turbo</strong> is the one that matters most for day-to-day work. 128K context window. Better instruction following. JSON mode that actually works. Lower prices. The practical effect is immediate: prompts I was carefully engineering to fit in 8K can now be sloppy and long. Function calling went from &ldquo;fragile hack&rdquo; to &ldquo;usable feature.&rdquo; Cost assumptions that made certain products unviable are suddenly different.</p>
<p>I rewrote two prompts that week. Both got simpler. Both worked better. That&rsquo;s the kind of improvement I respect &ndash; not a new capability, but a dramatic reduction in friction for existing ones.</p>
<p><strong>The Assistants API</strong> is more interesting and more concerning. It bundles threads, tool execution, file retrieval, and conversation state into a managed service. You describe an assistant, feed it files, and it handles the orchestration. For prototypes and internal tools, this is incredible. I spun up a document Q&amp;A assistant in about an hour that would have taken days with our custom setup.</p>
<p>The concern is control. When OpenAI manages the thread, the retrieval, and the tool execution, you lose visibility into what&rsquo;s happening. You can&rsquo;t tune the retrieval. You can&rsquo;t inspect the intermediate reasoning. For a quick prototype, that&rsquo;s fine. For a production system handling financial data at the fintech company, I need to see what&rsquo;s happening under the hood.</p>
<p><strong>Custom GPTs</strong> are ChatGPT plugins done right. No-code assistants that anyone can build and share. For developers, this is a double-edged sword. It&rsquo;s a distribution channel &ndash; you can ship lightweight tools that live inside ChatGPT. It&rsquo;s also competition &ndash; because everyone else can, including non-developers. If your startup is &ldquo;ChatGPT but with this one extra feature,&rdquo; you now have a problem.</p>
<h2 id="the-build-vs-buy-shift">The Build-vs-Buy Shift</h2>
<p>This is where it gets strategic. Before DevDay, the standard architecture for an AI feature was: pick a model, build a RAG pipeline, manage conversation state, wire up tools, handle the orchestration yourself. Lots of plumbing. Lots of control.</p>
<p>After DevDay, OpenAI is offering to handle most of that plumbing. The question is no longer &ldquo;can we build this ourselves?&rdquo; It&rsquo;s &ldquo;should we?&rdquo;</p>
<p>My framework: use the managed path for anything that isn&rsquo;t a core differentiator. If your product&rsquo;s value comes from the quality of your retrieval, the specificity of your tool calls, or strict data governance, keep building custom. If the AI feature is a nice-to-have or an internal tool, the Assistants API will get you there in a fraction of the time.</p>
<p>The danger is the middle ground. Features that feel custom but aren&rsquo;t actually differentiated. These are the ones that will get swallowed by the platform, and the teams building them will realize too late that they have been maintaining infrastructure OpenAI now gives away.</p>
<h2 id="rag-isnt-dead-but-the-bar-just-went-up">RAG Isn&rsquo;t Dead (But the Bar Just Went Up)</h2>
<p>I keep seeing &ldquo;RAG is dead&rdquo; takes. They&rsquo;re wrong, but the kernel of truth is real. With 128K context and built-in retrieval, the bar for justifying a custom RAG pipeline just got much higher.</p>
<p>If you&rsquo;re stuffing a few documents into context and asking questions, the Assistants API does this out of the box. If you need precise control over chunking, embedding models, re-ranking, or compliance with data residency requirements, custom RAG is still the answer.</p>
<p>At the fintech company, we&rsquo;ll keep our custom retrieval. Financial data has strict requirements that a black-box retrieval system can&rsquo;t satisfy. But I&rsquo;d estimate that 60-70% of the RAG implementations I&rsquo;ve seen in the wild could be replaced by the Assistants API with no loss in quality. Those teams should take the free lunch.</p>
<h2 id="what-im-doing-about-it">What I&rsquo;m Doing About It</h2>
<p>The same week as DevDay, I started a review of every custom component in our AI pipeline. The question for each one: does this still earn its maintenance cost?</p>
<p>Three things survived the review. Everything else is getting migrated or simplified.</p>
<p>That&rsquo;s the right response to DevDay. Not panic. Not hype. A sober assessment of what&rsquo;s now commodity and what&rsquo;s still worth owning. OpenAI moved the line. The smart move is to acknowledge it and redraw your architecture accordingly.</p>
]]></content:encoded></item></channel></rss>