<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nolan Vale</title>
    <description>The latest articles on DEV Community by Nolan Vale (@nolanvale).</description>
    <link>https://dev.to/nolanvale</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3969185%2F5fa49145-7052-4e4c-855b-a6a2157df24d.png</url>
      <title>DEV Community: Nolan Vale</title>
      <link>https://dev.to/nolanvale</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nolanvale"/>
    <language>en</language>
    <item>
      <title>Designing an On-Call Schedule That Doesn't Burn Out Your Team</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:04:35 +0000</pubDate>
      <link>https://dev.to/nolanvale/designing-an-on-call-schedule-that-doesnt-burn-out-your-team-3i68</link>
      <guid>https://dev.to/nolanvale/designing-an-on-call-schedule-that-doesnt-burn-out-your-team-3i68</guid>
      <description>&lt;p&gt;On-call rotations are one of those systems that quietly determine engineering retention more than most teams realize. A well-designed rotation is barely noticeable. A poorly designed one shows up in resignation letters months after the actual pattern of bad nights started, by which point the damage is already done. A few structural choices tend to make the biggest difference between the two outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation length matters more than rotation frequency
&lt;/h2&gt;

&lt;p&gt;A common instinct is to spread on-call thin, more people in the rotation means each person is on call less often. This helps, but it interacts with rotation length in ways that aren't always intuitive. A one-week rotation with eight people in the pool means each person is on call roughly every two months, which sounds reasonable, but a full week of being interruptible, including nights, is a meaningfully different cognitive load than the same total hours spread across shorter, more frequent shifts.&lt;/p&gt;

&lt;p&gt;Some teams find that shorter rotations, three or four days, with a slightly larger pool, produce less cumulative fatigue than longer rotations with a smaller pool, even when the total on-call hours per person over a quarter work out similar. The difference seems to come down to how much a full week of disrupted sleep and attention compounds compared to shorter stretches with more recovery time between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alert quality determines whether on-call is sustainable at all
&lt;/h2&gt;

&lt;p&gt;No rotation schedule survives contact with a noisy alerting system. If on-call engineers are being paged for issues that don't actually require immediate human intervention, the schedule itself becomes almost irrelevant, because the actual problem is alert fatigue, not rotation design.&lt;/p&gt;

&lt;p&gt;A useful practice is tracking, for every page, whether it required real-time action or could have waited until business hours. Alerts that consistently fall into the second category are strong candidates for downgrading to a non-paging notification, or for fixing the underlying issue that's generating them in the first place. Teams that review this data monthly tend to see page volume drop significantly over a few quarters, simply by removing alerts that were never actually actionable at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compensation and recognition need to be explicit, not implied
&lt;/h2&gt;

&lt;p&gt;On-call work is real work, and treating it as an unstated expectation of the job rather than something explicitly compensated, whether through pay, time off in lieu, or another mechanism, tends to create quiet resentment that doesn't show up directly in complaints but does show up in attrition and in reluctance to volunteer for the rotation.&lt;/p&gt;

&lt;p&gt;Teams that handle this well tend to be explicit and consistent: a fixed on-call stipend, or a clear policy of comp time for any incident response outside working hours, removes the ambiguity and signals that the disruption is recognized rather than assumed as a baseline expectation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation paths need to actually work, not just exist on paper
&lt;/h2&gt;

&lt;p&gt;A common failure mode is an escalation policy that looks complete in the documentation but has never been tested in practice. The primary on-call person doesn't respond within the expected window, and the secondary escalation either doesn't trigger correctly or nobody remembers who's supposed to pick it up. This gap is usually invisible until an actual incident exposes it, at the worst possible time.&lt;/p&gt;

&lt;p&gt;Periodically testing the escalation path, not just reviewing it on paper, catches configuration drift and staffing gaps before they matter during a real incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting recovery time after a bad on-call shift
&lt;/h2&gt;

&lt;p&gt;A rotation that technically ends on schedule but doesn't account for a rough night, several pages, disrupted sleep, still leaves someone expected to be fully present the next morning. A policy that allows for a delayed start or a lighter workload the day immediately following a disruptive on-call night, without requiring the engineer to justify or negotiate it individually, removes a source of quiet burnout that pure schedule design doesn't address on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal that a rotation needs redesign
&lt;/h2&gt;

&lt;p&gt;Volunteer rate for on-call duty is a more honest signal than survey responses. If engineers are actively avoiding the rotation, negotiating out of it, or the same small subset of people keep ending up covering more than their share, that's a more reliable indicator that something structural needs to change than any satisfaction score collected after the fact. Schedules that are actually sustainable tend to have engineers rotating in without needing to be convinced, because the system, page quality, compensation, recovery time, has been designed around what a person can reasonably sustain rather than around minimum coverage requirements alone.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>management</category>
      <category>mentalhealth</category>
      <category>sre</category>
    </item>
    <item>
      <title>how to run a blameless postmortem that actually changes anything</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Thu, 16 Jul 2026 16:26:15 +0000</pubDate>
      <link>https://dev.to/nolanvale/how-to-run-a-blameless-postmortem-that-actually-changes-anything-30j4</link>
      <guid>https://dev.to/nolanvale/how-to-run-a-blameless-postmortem-that-actually-changes-anything-30j4</guid>
      <description>&lt;p&gt;most engineering teams say they run blameless postmortems. fewer actually do. the difference usually shows up not in the meeting itself but in what happens to the document afterward, and in whether the same category of incident shows up again six months later.&lt;/p&gt;

&lt;p&gt;here is what separates a postmortem that changes system behavior from one that is just a ritual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the timing matters more than people think&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;running the postmortem too soon after an incident means people are still defensive, still tired, and still reconstructing the timeline from memory rather than from logs. running it too late means details are lost and the sense of urgency has faded, so action items get deprioritized before they are even written down.&lt;/p&gt;

&lt;p&gt;a reasonable window is 24 to 72 hours after resolution. enough time to gather logs, traces, and a clear timeline. not so much time that the incident stops feeling relevant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the facilitator should not be the person who caused the incident&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;this is not about assigning blame indirectly through facilitation. it is a practical point: whoever is closest to the incident is usually still processing it emotionally, and facilitating a meeting while also being the subject of scrutiny in that meeting is a difficult position to put someone in. a neutral facilitator, someone from another team or a rotating role, keeps the conversation focused on the system rather than the individual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;separate the timeline from the analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;a common failure mode is jumping straight into "why did this happen" before the group has agreed on "what actually happened, in what order." without a shared, factual timeline first, the analysis conversation tends to fragment into different people arguing from different mental models of the incident.&lt;/p&gt;

&lt;p&gt;build the timeline first, sourced from logs and monitoring data wherever possible rather than from memory. only move to root cause discussion once everyone agrees on the sequence of events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ask "why did our systems allow this" not "who did this"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;the language used in the room shapes the outcome. "who deployed the change that caused this" invites defensiveness. "what allowed this change to reach production without being caught" invites systems thinking. the second framing tends to surface more useful findings, because it assumes the individual acted reasonably given the information and tooling available to them at the time, and asks what about the environment made the mistake possible or likely.&lt;/p&gt;

&lt;p&gt;this reframing is not about avoiding accountability. it is about recognizing that a single engineer making a single mistake is rarely, on its own, a sufficient explanation for a production incident. the more useful question is why the surrounding system, code review, testing, monitoring, alerting, did not catch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;write action items that are specific and owned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"improve monitoring" is not an action item. it is a wish. a real action item names a specific alert to add, a specific dashboard to build, a specific runbook to write, and it has an owner and a rough timeframe attached to it.&lt;/p&gt;

&lt;p&gt;teams that skip this step tend to produce postmortem documents full of good intentions that never get scheduled against actual sprint work, because vague action items compete poorly against concrete feature requests when priorities get set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;track whether the fixes actually shipped&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;this is the step most teams drop. a postmortem document gets written, gets reviewed once, and then nobody checks back in a month later to see whether the listed action items were completed. a simple practice that closes this gap: review open postmortem action items at a fixed cadence, monthly is common, and report on completion rate the same way any other engineering commitment gets reported.&lt;/p&gt;

&lt;p&gt;teams that track this consistently tend to notice something useful over time: certain categories of action items chronically do not get completed, which is itself a signal about where organizational priorities and stated safety goals are misaligned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the real test of a blameless postmortem process&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;not whether people feel comfortable in the meeting, though that matters. the real test is whether the same category of incident happens again. if a similar outage repeats within a year, that is a signal the previous postmortem's action items either were not completed, were not the right fixes, or were not aimed at the actual systemic cause. a postmortem process that consistently prevents repeat incidents is doing its job. one that produces well-written documents but the same recurring failures is a ritual, not a practice.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>management</category>
      <category>softwareengineering</category>
      <category>sre</category>
    </item>
    <item>
      <title>THE INEVITABLE SHIFT TOWARD DATA SOVEREIGNTY</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Tue, 14 Jul 2026 18:04:52 +0000</pubDate>
      <link>https://dev.to/nolanvale/the-inevitable-shift-toward-data-sovereignty-4mk6</link>
      <guid>https://dev.to/nolanvale/the-inevitable-shift-toward-data-sovereignty-4mk6</guid>
      <description>&lt;p&gt;For the past two years, the entire software engineering community has been absolutely mesmerized by the magic of external application programming interfaces. We have spent countless hours wiring our internal databases to massive cloud models owned by third party vendors. It was a necessary and incredibly exciting phase of rapid prototyping. We proved that the foundational technology works and that it can fundamentally change how we interact with computers. &lt;/p&gt;

&lt;p&gt;But as systems architects, we know that shipping our private data across the public internet to rent intelligence is not the final destination. It is merely a transitional bridge. I am deeply optimistic about what comes next. We are currently standing on the threshold of a massive architectural renaissance. The future of enterprise technology is not about connecting to the biggest public cloud. The future is about bringing the intelligence directly into your own private network. &lt;/p&gt;

&lt;p&gt;We are entering the era of the sovereign intelligence operating system.&lt;/p&gt;

&lt;p&gt;To understand why this is such an exciting engineering challenge, we have to talk about the concept of data gravity. In computer science, data gravity is the idea that as data accumulates, it becomes heavier and more difficult to move. The applications and the processing power naturally need to move closer to the data to reduce latency and friction. &lt;/p&gt;

&lt;p&gt;Right now, the industry is operating in direct defiance of data gravity. We are taking our heaviest, most valuable, and most sensitive corporate data and trying to push it through tiny network pipes to external vendors. This requires building massive, brittle middleware systems to scrub personally identifiable information before it ever leaves our perimeter. It is computationally expensive and structurally inelegant.&lt;/p&gt;

&lt;p&gt;The beautiful solution, and the one that the best engineering teams are secretly building right now, is to reverse the flow. Instead of sending the data to the intelligence, we are finally capable of bringing the intelligence directly to the data. &lt;/p&gt;

&lt;p&gt;When you deploy a foundational model inside your own private operating environment, an incredible amount of engineering friction simply vanishes overnight. You no longer have to spend months writing complex masking algorithms to hide customer names or financial numbers. Since the data never actually leaves your secure perimeter, the entire security posture of your application stack becomes radically simplified. Your engineers can stop building defensive wrappers and start focusing exclusively on building incredible user experiences.&lt;/p&gt;

&lt;p&gt;This shift unlocks something I like to call the unified context architecture. In our current fragmented state, if you buy ten different intelligent software tools, you are essentially creating ten different isolated brains. The tool your legal team uses cannot talk to the tool your marketing team uses. &lt;/p&gt;

&lt;p&gt;But when you build a singular, private operating space for your organization, you create a shared cognitive layer. You can build a central vector database that acts as the memory bank for your entire company. Because it is completely private and self hosted, you can safely index absolutely everything. Every contract, every codebase, every architectural decision record, and every financial model can live in one unified space. &lt;/p&gt;

&lt;p&gt;When a new engineer queries the system to understand a piece of legacy code, the local intelligence can instantly cross reference the original product requirements document written by the product manager three years ago. It creates a level of cross functional alignment that was previously impossible. We are finally realizing the ultimate dream of microservices architecture. We are creating specialized tools that all tap into the exact same foundational truth without compromising security.&lt;/p&gt;

&lt;p&gt;We also have to acknowledge the incredible renaissance happening in the hardware and open source model space right now. A year ago, running a highly capable model locally required a massive server farm and millions of dollars in capital expenditure. That is no longer true. The open source community has achieved absolute miracles in model quantization and optimization. We can now run incredibly sophisticated reasoning engines on standard enterprise hardware. &lt;/p&gt;

&lt;p&gt;This completely changes the unit economics of software development. When you rely on external application programming interfaces, your costs scale linearly with your usage. The more successful your internal tool becomes, the more you are penalized by massive cloud billing invoices at the end of the month. It creates a perverse incentive where companies actually try to limit how much their employees use the system.&lt;/p&gt;

&lt;p&gt;When you own the operating environment and host the models yourself, your marginal cost of generating a response drops to zero. You want your employees to query the system ten thousand times a day. You want them to automate every single mundane task they have. The compute becomes a fixed capital asset rather than a variable operational tax. This financial predictability allows architecture teams to experiment wildly and build things that would have been financially ruinous under the old pay per request model.&lt;/p&gt;

&lt;p&gt;This is why I am so deeply energized by the current state of our industry. We are moving away from being passive renters of intelligence. We are becoming true builders and owners of our own cognitive infrastructure. &lt;/p&gt;

&lt;p&gt;The transition from public cloud environments to private, sovereign workspaces is not a retreat driven by fear or compliance requirements. It is a massive leap forward driven by the desire for better performance, deeper integration, and absolute architectural elegance. &lt;/p&gt;

&lt;p&gt;The teams that recognize this shift today are the ones who are going to build the most resilient and powerful companies of the next decade. We are laying the bricks for a completely new kind of operating system, one where privacy is guaranteed by mathematics and capability is only limited by our own imagination. It is a fantastic time to be a software architect.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>data</category>
      <category>privacy</category>
    </item>
    <item>
      <title>What Nobody Tells You About Vector Index Configuration Until Your Retrieval Breaks at Scale</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:11:59 +0000</pubDate>
      <link>https://dev.to/nolanvale/what-nobody-tells-you-about-vector-index-configuration-until-your-retrieval-breaks-at-scale-1k8l</link>
      <guid>https://dev.to/nolanvale/what-nobody-tells-you-about-vector-index-configuration-until-your-retrieval-breaks-at-scale-1k8l</guid>
      <description>&lt;p&gt;Most teams configure their vector index once during initial setup, run some tests, see good results, and never touch it again. This works fine until query volume grows, the document corpus expands significantly, or retrieval quality starts degrading in ways that are difficult to diagnose.&lt;/p&gt;

&lt;p&gt;The index configuration decisions that seem unimportant at small scale become load-bearing at production scale. I want to describe the specific decisions that matter and why, because the documentation for most vector databases explains what the parameters do but not how to think about setting them for a real enterprise deployment.&lt;/p&gt;

&lt;p&gt;The most consequential configuration decision is the choice between exact nearest neighbor search and approximate nearest neighbor search. Exact search always returns the true top-k most similar vectors. Approximate search returns results that are similar but not guaranteed to be the exact top-k, in exchange for dramatically faster query times.&lt;/p&gt;

&lt;p&gt;At a corpus of ten thousand documents, exact search is fast enough that the choice does not matter much. At a million document chunks, exact search becomes prohibitively slow and approximate search is the only practical option. The transition point where this matters is lower than most teams expect because enterprise knowledge bases grow faster than anticipated when you include email archives, Slack history, meeting transcripts, and support tickets alongside structured documentation.&lt;/p&gt;

&lt;p&gt;Understanding which approximate nearest neighbor algorithm your vector database uses and what its quality-speed tradeoffs are is not optional for production deployments. Pinecone uses HNSW internally. Weaviate uses HNSW. Qdrant supports both HNSW and a scalar quantization variant. Chroma uses HNSW by default. The key parameters for HNSW are ef_construction (controls index build quality, higher is better but slower to build) and ef (controls search quality at query time, higher is better but slower to search).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Weaviate HNSW configuration example
&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EnterpriseDocument&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vectorIndexConfig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ef&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;# search quality parameter, higher = better recall, slower queries
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;efConstruction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# build quality parameter, higher = better index, slower builds
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxConnections&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# graph connectivity, affects memory and quality
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vectorIndexType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hnsw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Rule of thumb starting points:
# ef: 2x to 4x your k value (if retrieving top 10, start with ef=64-128)
# efConstruction: 2x ef minimum, often 2-4x ef for better quality
# maxConnections: 16 for most use cases, 32-64 for high-accuracy requirements
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ef parameter at query time is the one with the most direct impact on the quality-speed tradeoff and the one most worth tuning empirically against your actual query patterns. The relationship between ef and recall is not linear: going from ef=64 to ef=128 typically improves recall significantly, while going from ef=256 to ef=512 often produces diminishing returns that are not worth the latency cost.&lt;/p&gt;

&lt;p&gt;The distance metric choice matters more than most teams realize. Cosine similarity measures the angle between vectors, making it appropriate for most text embedding use cases where the magnitude of the vector is not meaningful. Dot product similarity measures both direction and magnitude, which is appropriate for models specifically trained for dot product retrieval. Euclidean distance measures geometric distance and is less common for text retrieval.&lt;/p&gt;

&lt;p&gt;Using the wrong distance metric for your embedding model is one of the most common silent quality problems in RAG deployments. The embedding model's documentation specifies which distance metric it was trained for. If you are using a model trained for cosine similarity with a dot product configuration, or vice versa, your retrieval quality will be systematically lower than the model is capable of. Check this explicitly; do not assume the default is correct for your model.&lt;/p&gt;

&lt;p&gt;Quantization is the configuration area where teams most often trade quality for performance without fully understanding the tradeoff. Scalar quantization reduces each float32 embedding value to an int8, cutting memory requirements by 4x at the cost of some retrieval quality. Product quantization goes further, achieving much higher compression ratios at more significant quality cost.&lt;/p&gt;

&lt;p&gt;For most enterprise deployments where retrieval quality matters and hardware cost is not the binding constraint, I recommend avoiding quantization or using only scalar quantization with careful evaluation of the quality impact. The memory savings from quantization are appealing, but the retrieval quality impact is real and should be measured against your evaluation set before enabling it in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Qdrant scalar quantization configuration
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;qdrant_client.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ScalarQuantizationConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ScalarType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QuantizationConfig&lt;/span&gt;

&lt;span class="n"&gt;quantization_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QuantizationConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;scalar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ScalarQuantizationConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ScalarType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;quantile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# clip outlier values at the 99th percentile
&lt;/span&gt;        &lt;span class="n"&gt;always_ram&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# keep quantized vectors in RAM for faster access
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Measure quality impact before enabling:
# Run your eval suite with and without quantization
# Accept only if recall@5 degrades by less than your acceptable threshold
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index segment size is a configuration parameter that rarely appears in getting-started guides but matters significantly for write-heavy workloads. When you continuously ingest new documents, the index is updated incrementally. If the segment configuration is not tuned for your ingestion rate, you can end up with a fragmented index that serves queries from many small segments rather than a few large optimized ones, degrading query performance progressively over time.&lt;/p&gt;

&lt;p&gt;Most vector databases handle this with periodic index optimization or merging. Make sure this is configured to run automatically rather than relying on manual intervention. The performance degradation from a fragmented index is gradual and easy to miss in monitoring until it becomes significant.&lt;/p&gt;

&lt;p&gt;The configuration I described above represents the decisions worth understanding for any production enterprise RAG deployment. The specific values to use for your deployment depend on your corpus size, your query volume, your latency requirements, and your hardware. The right approach is to treat these as parameters to tune empirically against your evaluation set rather than values to set once and forget.&lt;/p&gt;

&lt;p&gt;Index configuration is infrastructure. Like all infrastructure, it requires ongoing attention as the system scales and as usage patterns evolve. The teams that build and maintain excellent RAG systems treat index tuning as a recurring operational practice, not a one-time setup task.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Caching in RAG Systems: What to Cache, What Not To, and Why It Matters More Than You Think</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Fri, 10 Jul 2026 11:43:48 +0000</pubDate>
      <link>https://dev.to/nolanvale/caching-in-rag-systems-what-to-cache-what-not-to-and-why-it-matters-more-than-you-think-a06</link>
      <guid>https://dev.to/nolanvale/caching-in-rag-systems-what-to-cache-what-not-to-and-why-it-matters-more-than-you-think-a06</guid>
      <description>&lt;p&gt;Caching is one of the highest-leverage optimizations in a production RAG system and one of the most underused. Most teams cache at the obvious layer, the final LLM response, and miss the more valuable caching opportunities earlier in the pipeline.&lt;/p&gt;

&lt;p&gt;Let me walk through the full caching picture for a RAG system, because the right answer is different at each layer.&lt;/p&gt;

&lt;p&gt;The embedding layer is where caching has the clearest value proposition. Computing embeddings is deterministic: the same text run through the same embedding model always produces the same vector. Every time a user submits a query you have seen before, you are paying to compute an embedding you already have.&lt;/p&gt;

&lt;p&gt;In practice, enterprise RAG systems see high query repetition. Employees ask similar questions. "What is our PTO policy," "how do I submit a reimbursement," "what are the Q3 targets" get asked by many different people. Caching query embeddings means these repeated queries pay the embedding cost once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lru_cache&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedEmbedder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_store&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedding_model&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_store&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emb:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 24 hour TTL
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL matters here. Embedding models are versioned, and if you upgrade your embedding model, cached embeddings from the old model will be wrong. Either include the model version in the cache key or set a TTL that expires before you expect to update the model.&lt;/p&gt;

&lt;p&gt;The retrieval result layer is where most teams try to cache and often get it wrong. Retrieval results are not purely deterministic. The same query will return different results if the underlying document corpus has changed. If you cache retrieval results without accounting for corpus changes, users get stale results.&lt;/p&gt;

&lt;p&gt;The correct approach is cache invalidation tied to document updates rather than time-based TTL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetrievalCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_registry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_store&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document_registry&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;embedding_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;corpus_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_corpus_version&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieval:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;embedding_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;corpus_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corpus version is a hash or incrementing counter that changes whenever any document is added, updated, or removed. When the corpus changes, all retrieval cache keys that include the old version become invalid automatically, without needing explicit cache invalidation logic.&lt;/p&gt;

&lt;p&gt;The LLM response layer is the most expensive but also the most dangerous to cache. LLM responses are generated given a specific context at a specific time. Caching them means users may get responses that were accurate when generated but are stale now.&lt;/p&gt;

&lt;p&gt;My general rule is to only cache LLM responses for queries where the answer is stable over the cache duration. Static reference information, definitions, historical facts. Not policy questions, not questions about current state, not anything where the correct answer might change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConditionalResponseCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;CACHEABLE_QUERY_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;definition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;historical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;RESPONSE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;definition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;604800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# 7 days
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;historical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 30 days
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# 1 day
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_freshness_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CACHEABLE_QUERY_TYPES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context_freshness_days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# don't cache if source docs are stale
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caching opportunity that teams consistently miss is prompt prefix caching. If your system prompt is large and constant across all requests, you are paying to process it on every single request. Both Anthropic and OpenAI support prompt caching that charges significantly reduced rates for the cached portion. On a high-volume deployment, this can represent 15 to 25% reduction in inference cost for zero change in functionality.&lt;/p&gt;

&lt;p&gt;The only requirement is that the cacheable content appears at the beginning of the prompt in the same position across requests. If your system prompt is dynamically assembled with session-specific content inserted before the stable portion, restructure the prompt so the stable content comes first.&lt;/p&gt;

&lt;p&gt;Caching done well makes your RAG system faster and cheaper without degrading quality. Caching done poorly gives users confidently wrong answers from stale cache entries. The difference is designing each caching layer around the specific properties of the data being cached rather than applying a generic caching strategy across the whole pipeline.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>performance</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building Guardrails for Enterprise LLMs That Actually Work</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:33:43 +0000</pubDate>
      <link>https://dev.to/nolanvale/building-guardrails-for-enterprise-llms-that-actually-work-24bb</link>
      <guid>https://dev.to/nolanvale/building-guardrails-for-enterprise-llms-that-actually-work-24bb</guid>
      <description>&lt;p&gt;The term "guardrails" gets used loosely in conversations about enterprise AI safety. Some people mean input filtering. Some mean output filtering. Some mean a combination of both with a constitutional AI layer and a human review queue thrown in. The imprecision matters because different guardrail approaches address different failure modes, and deploying the wrong one gives you a false sense of security.&lt;/p&gt;

&lt;p&gt;I want to describe the specific guardrail architecture I have landed on for enterprise RAG deployments after getting this wrong in several different ways and learning from each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure modes guardrails are actually trying to address&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before building anything, you need to be specific about what you are preventing. Guardrails that are not targeted at specific failure modes end up either too restrictive, blocking legitimate use cases, or too permissive, missing the actual problems.&lt;/p&gt;

&lt;p&gt;The failure modes I treat as in-scope for enterprise knowledge management deployments:&lt;/p&gt;

&lt;p&gt;Retrieval of content the requesting user is not authorized to see. This is an access control problem at the retrieval layer, not a guardrail problem, but it often gets misclassified as something guardrails should address. The correct solution is authorization enforcement before retrieval, not content filtering after generation.&lt;/p&gt;

&lt;p&gt;Generation of responses that misrepresent organizational facts. The AI produces an answer that sounds authoritative but does not reflect what the source documents actually say. The guardrail approach here is groundedness checking: verifying that claims in the generated output are supported by the retrieved context.&lt;/p&gt;

&lt;p&gt;Prompt injection through document content. A document in the knowledge base contains instruction-like text designed to override the system prompt or change the AI's behavior. The guardrail approach is input sanitization and structural separation between system instructions and retrieved content.&lt;/p&gt;

&lt;p&gt;Out-of-scope responses. The AI answers questions that are outside the intended scope of the deployment, either drawing on general training knowledge rather than organizational documents, or engaging with topics unrelated to the deployment's purpose. The guardrail approach is scope enforcement through both system prompt design and output classification.&lt;/p&gt;

&lt;p&gt;Sensitive information leakage through inference. A user does not directly retrieve a restricted document but asks a question that the AI can only answer correctly by drawing on information from that document. This is the hardest guardrail problem because it requires reasoning about the provenance of generated content, not just filtering explicit retrievals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer one: Input processing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first guardrail layer operates on user queries before they reach the retrieval system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InputGuardrail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;INJECTION_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore (previous|above|all) instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;you are now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forget your (system|previous) (prompt|instructions)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;act as (if you are|a|an)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disregard.*instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new instructions:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;override.*system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;MAX_QUERY_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;  &lt;span class="c1"&gt;# tokens approximately
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_classifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pii_detector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope_classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope_classifier&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pii_detector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pii_detector&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

        &lt;span class="c1"&gt;# Check for injection attempts
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INJECTION_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injection_attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched_pattern&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="c1"&gt;# Check query length
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAX_QUERY_LENGTH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="c1"&gt;# Check if query is in scope
&lt;/span&gt;        &lt;span class="n"&gt;scope_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope_classifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scope_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;scope_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out_of_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope_confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;

        &lt;span class="c1"&gt;# Check for PII in query that should not be there
&lt;/span&gt;        &lt;span class="n"&gt;pii_detected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pii_detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pii_detected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pii_in_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pii_types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pii_detected&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injection_attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query contains patterns associated with instruction injection.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query exceeds maximum length.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The injection pattern matching is a necessary but insufficient defense. Sophisticated injection attempts will not match simple regex patterns. The pattern matching catches the obvious cases and logs flagged queries for review, but should not be relied on as the sole defense against prompt injection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two: Retrieved content sanitization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before retrieved document chunks are assembled into the prompt context, they pass through a sanitization step that checks for instruction-like content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetrievedContentGuardrail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;SUSPICIOUS_CONTENT_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(system|assistant|user):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[INST\]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;s&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore.*previous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new task:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;\|.*\|&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# various prompt formatting markers
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sanitize_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUSPICIOUS_CONTENT_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="c1"&gt;# Log but do not necessarily remove - some patterns may be legitimate
&lt;/span&gt;                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_suspicious_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sanitized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_safe_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieved_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Structurally separate retrieved content from instructions
&lt;/span&gt;        &lt;span class="c1"&gt;# using clear delimiters that are unlikely to appear in documents
&lt;/span&gt;        &lt;span class="n"&gt;safe_parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved_chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;sanitized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow_flagged_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;safe_parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[SOURCE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sanitized&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[END SOURCE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe_parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structural separation with explicit delimiters is more important than the pattern matching. An LLM that is given a clear structural distinction between "these are instructions" and "this is retrieved content" is more resistant to injection than one that receives a flat prompt where instructions and content are interleaved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer three: Output validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generated responses pass through two output validation steps before being returned to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutputGuardrail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groundedness_checker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_classifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sensitivity_classifier&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groundedness_checker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groundedness_checker&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope_classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope_classifier&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sensitivity_classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensitivity_classifier&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;retrieved_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;validation_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

        &lt;span class="c1"&gt;# Check 1: Is the response grounded in the retrieved context?
&lt;/span&gt;        &lt;span class="n"&gt;groundedness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groundedness_checker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retrieved_context&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groundedness_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groundedness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
        &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ungrounded_claims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groundedness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ungrounded_claims&lt;/span&gt;

        &lt;span class="c1"&gt;# Check 2: Does the response contain sensitivity markers suggesting
&lt;/span&gt;        &lt;span class="c1"&gt;# it drew on content the user may not be authorized for?
&lt;/span&gt;        &lt;span class="n"&gt;sensitivity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sensitivity_classifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sensitivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tier_for_permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user_permissions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;potential_unauthorized_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detected_sensitivity_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensitivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;

        &lt;span class="c1"&gt;# Check 3: Is the response in scope?
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope_classifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;in_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_out_of_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_validation_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation_results&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;validation_results&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_validation_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;potential_unauthorized_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I cannot provide information on this topic. Please contact your administrator if you believe you should have access.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groundedness_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Low groundedness - add explicit uncertainty marker
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;[Note: Some claims in this response may not be fully supported by the available documentation. Please verify with primary sources.]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_out_of_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This question is outside the scope of what I can help with in this context.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The groundedness threshold of 0.6 is a starting point, not a universal answer. The right threshold depends on your use case and the cost of false positives (blocking valid responses) versus false negatives (allowing ungrounded responses). For high-stakes queries in regulated contexts, you want a higher threshold. For general knowledge retrieval, a lower threshold avoids over-blocking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure mode this architecture does not address&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I want to be honest about the limit of what output-layer guardrails can do for the sensitive information inference problem.&lt;/p&gt;

&lt;p&gt;If a user asks "what is the most sensitive project currently underway at this company," and the AI can answer that question by synthesizing information from documents the user has access to, there is no guardrail that catches this cleanly. The user is authorized for each source document. The synthesized answer might reveal something that no individual document reveals. The guardrail does not know what inferences the user might draw from the response.&lt;/p&gt;

&lt;p&gt;This problem is genuinely hard and I have not seen a satisfying general solution. The approaches that reduce the risk:&lt;/p&gt;

&lt;p&gt;Query intent classification that flags synthesis queries involving organizational sensitivity assessments and routes them to human review. This adds latency and requires human reviewer capacity, but it catches the class of query where inference risk is highest.&lt;/p&gt;

&lt;p&gt;Restricting the AI's ability to reason about organizational structure, hierarchy, or competitive positioning from document content. Implemented through system prompt constraints and output classification, this reduces the utility of the system for some legitimate queries but also reduces the most concerning inference pathways.&lt;/p&gt;

&lt;p&gt;Accepting that a sufficiently determined insider can use an AI system to learn things a naive user would not, and designing the access control architecture (which documents are indexed and accessible to which users) to minimize the sensitivity of what can be inferred rather than trying to catch all inferences at generation time.&lt;/p&gt;

&lt;p&gt;The guardrail architecture I described above handles most of the common failure modes reasonably well. For the inference problem, the honest answer is that it requires architectural decisions about what you index and who can access it, not just guardrails on the output.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>rag</category>
      <category>security</category>
    </item>
    <item>
      <title>Token Economics: Why Your LLM Architecture is Bleeding Cash</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Wed, 08 Jul 2026 17:00:04 +0000</pubDate>
      <link>https://dev.to/nolanvale/token-economics-why-your-llm-architecture-is-bleeding-cash-5773</link>
      <guid>https://dev.to/nolanvale/token-economics-why-your-llm-architecture-is-bleeding-cash-5773</guid>
      <description>&lt;p&gt;I audit AI architectures for startups and mid-market enterprises, and I can usually tell you if a company is going to run out of runway just by looking at their API billing dashboard. &lt;/p&gt;

&lt;p&gt;Engineers are currently treating Large Language Models the exact same way they treat a Postgres database: query it whenever you want, passing as much context as possible, because compute is cheap. &lt;/p&gt;

&lt;p&gt;Except in the AI world, compute isn't cheap. You are paying by the token, and your microservices architecture is quietly bankrupting you.&lt;/p&gt;

&lt;p&gt;Here is the teardown of why your system is bleeding cash and how to stop the hemorrhage.&lt;/p&gt;

&lt;p&gt;[THE CONTEXT WINDOW TAX]&lt;/p&gt;

&lt;p&gt;The biggest architectural sin I see is "prompt bloat." A developer builds a system prompt to give the AI its persona and constraints. It starts at 200 tokens. By month three, after fixing a bunch of edge cases, that system prompt is 2,000 tokens long. &lt;/p&gt;

&lt;p&gt;If your application handles 10,000 conversational turns a day, and you are passing that 2,000-token system prompt on every single API call just to ask a user a 10-token question, you are mathematically destroying your own margins. You are paying a heavy toll tax just to say hello.&lt;/p&gt;

&lt;p&gt;[THE ROUTER PATTERN MISSING LINK]&lt;/p&gt;

&lt;p&gt;The second mistake is using a sledgehammer to crack a peanut. Teams default to routing every single query to GPT-4o or Claude 3.5 Sonnet because it’s "the best." &lt;/p&gt;

&lt;p&gt;Does a massive, frontier model need to extract a date from a plain text string? Does it need to classify a sentiment as positive or negative? Absolutely not. &lt;/p&gt;

&lt;p&gt;If you are not using an LLM Routing Architecture, you are wasting money. You need a fast, cheap gateway model (like Llama 3 8B or a basic text classifier) sitting in front of your expensive frontier models. If the task is simple extraction or classification, the gateway handles it for fractions of a penny. You only escalate to the expensive API when complex reasoning is actually required.&lt;/p&gt;

&lt;p&gt;[THE LACK OF SEMANTIC CACHING]&lt;/p&gt;

&lt;p&gt;If a web app queries a database for the same user profile 100 times, you use Redis to cache it. Why aren't you doing this with LLM generations?&lt;/p&gt;

&lt;p&gt;If 50 different users ask your internal HR bot, "What are the holidays for 2024?", your architecture is likely sending that exact same query to OpenAI 50 separate times, paying the exact same inference cost over and over to generate the exact same answer.&lt;/p&gt;

&lt;p&gt;You need a Semantic Cache layer (like RedisVL or GPTCache). When a query comes in, you embed it, check your cache for a high vector similarity (e.g., a 0.95 match score), and serve the pre-generated answer instantly. Cost: $0. Latency: 10ms.&lt;/p&gt;

&lt;p&gt;Stop deploying LLMs like they are free utilities. If your engineering team doesn't have a strict token budget and a caching strategy, your infrastructure costs are going to scale exponentially faster than your revenue.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Event-Driven AI Agents Scale Better Than Request-Driven Ones</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:03:54 +0000</pubDate>
      <link>https://dev.to/nolanvale/why-event-driven-ai-agents-scale-better-than-request-driven-ones-a09</link>
      <guid>https://dev.to/nolanvale/why-event-driven-ai-agents-scale-better-than-request-driven-ones-a09</guid>
      <description>&lt;p&gt;When people first build an AI agent, the architecture is usually simple.&lt;/p&gt;

&lt;p&gt;A user asks a question.&lt;/p&gt;

&lt;p&gt;The agent thinks.&lt;/p&gt;

&lt;p&gt;The agent responds.&lt;/p&gt;

&lt;p&gt;It feels intuitive because that's exactly how we interact with ChatGPT.&lt;/p&gt;

&lt;p&gt;The problem is that enterprise software rarely works that way.&lt;/p&gt;

&lt;p&gt;Business systems aren't driven by conversations.&lt;/p&gt;

&lt;p&gt;They're driven by events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is an event?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An event is simply something that has happened.&lt;/p&gt;

&lt;p&gt;A customer submits an order.&lt;/p&gt;

&lt;p&gt;A payment fails.&lt;/p&gt;

&lt;p&gt;A contract gets signed.&lt;/p&gt;

&lt;p&gt;An employee uploads a document.&lt;/p&gt;

&lt;p&gt;A ticket changes from "Open" to "Resolved."&lt;/p&gt;

&lt;p&gt;These events happen continuously across an organization, whether AI is involved or not.&lt;/p&gt;

&lt;p&gt;Instead of waiting for someone to ask a question, modern software reacts to these events automatically.&lt;/p&gt;

&lt;p&gt;That's the foundation of event-driven architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why request-driven AI reaches its limits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most first-generation AI assistants are request-driven.&lt;/p&gt;

&lt;p&gt;Nothing happens until someone opens a chat window and types a prompt.&lt;/p&gt;

&lt;p&gt;This works well for individual productivity.&lt;/p&gt;

&lt;p&gt;It becomes less effective when dozens of teams rely on AI to support ongoing business operations.&lt;/p&gt;

&lt;p&gt;Imagine a finance department.&lt;/p&gt;

&lt;p&gt;Every invoice over a certain amount requires review.&lt;/p&gt;

&lt;p&gt;If employees have to remember to ask an AI assistant every single time, the workflow depends on human memory.&lt;/p&gt;

&lt;p&gt;That's not automation.&lt;/p&gt;

&lt;p&gt;That's assisted work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An event-driven agent behaves differently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of waiting for a prompt, the agent reacts automatically.&lt;/p&gt;

&lt;p&gt;A new invoice arrives.&lt;/p&gt;

&lt;p&gt;The workflow begins.&lt;/p&gt;

&lt;p&gt;The AI extracts key information.&lt;/p&gt;

&lt;p&gt;Business rules are evaluated.&lt;/p&gt;

&lt;p&gt;Potential risks are highlighted.&lt;/p&gt;

&lt;p&gt;A reviewer receives a notification only if human judgment is needed.&lt;/p&gt;

&lt;p&gt;Nobody has to remember to start the process.&lt;/p&gt;

&lt;p&gt;The event itself becomes the trigger.&lt;/p&gt;

&lt;p&gt;This small architectural change has a surprisingly large impact on scalability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loose coupling makes systems easier to evolve&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another advantage of event-driven systems is that components remain loosely connected.&lt;/p&gt;

&lt;p&gt;Suppose a company adds fraud detection to its payment workflow.&lt;/p&gt;

&lt;p&gt;In a tightly coupled system, developers may need to modify existing services directly.&lt;/p&gt;

&lt;p&gt;In an event-driven architecture, the new service simply listens for the same payment event.&lt;/p&gt;

&lt;p&gt;The payment system doesn't even need to know the fraud service exists.&lt;/p&gt;

&lt;p&gt;Each component evolves independently.&lt;/p&gt;

&lt;p&gt;As organizations grow, that flexibility becomes increasingly valuable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI becomes another participant—not the center of the system&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One mistake I often see is designing the entire workflow around the AI.&lt;/p&gt;

&lt;p&gt;Everything waits for the model.&lt;/p&gt;

&lt;p&gt;Every decision flows through the assistant.&lt;/p&gt;

&lt;p&gt;That architecture looks impressive in demonstrations.&lt;/p&gt;

&lt;p&gt;It also creates unnecessary dependencies.&lt;/p&gt;

&lt;p&gt;A healthier approach is to treat AI like any other service.&lt;/p&gt;

&lt;p&gt;The workflow continues to belong to the business.&lt;/p&gt;

&lt;p&gt;The AI contributes when its capabilities add value.&lt;/p&gt;

&lt;p&gt;If the model becomes unavailable, the business should still be able to continue operating, even if some tasks temporarily become manual.&lt;/p&gt;

&lt;p&gt;Architectures that depend entirely on AI are usually less resilient than architectures where AI enhances existing workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions I would ask during an architecture review&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whenever I review an AI system, I find these questions more useful than discussing model benchmarks.&lt;/p&gt;

&lt;p&gt;What business event starts this workflow?&lt;/p&gt;

&lt;p&gt;Which parts actually require AI?&lt;/p&gt;

&lt;p&gt;Can the workflow continue if the model is unavailable?&lt;/p&gt;

&lt;p&gt;Which decisions must always remain under human control?&lt;/p&gt;

&lt;p&gt;How will new services be added two years from now?&lt;/p&gt;

&lt;p&gt;Those questions reveal much more about long-term scalability than another comparison between language models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final thought&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good AI architecture isn't about inserting AI into every workflow.&lt;/p&gt;

&lt;p&gt;It's about designing workflows that remain reliable as the business grows.&lt;/p&gt;

&lt;p&gt;Event-driven systems achieve that because they respond to business activity rather than waiting for human prompts.&lt;/p&gt;

&lt;p&gt;In the long run, organizations rarely struggle because their models are too small.&lt;/p&gt;

&lt;p&gt;They struggle because their architecture wasn't designed to evolve.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Context Compression: Fitting More Useful Information Into Your LLM's Context Window</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Mon, 06 Jul 2026 10:18:32 +0000</pubDate>
      <link>https://dev.to/nolanvale/context-compression-fitting-more-useful-information-into-your-llms-context-window-4dlc</link>
      <guid>https://dev.to/nolanvale/context-compression-fitting-more-useful-information-into-your-llms-context-window-4dlc</guid>
      <description>&lt;p&gt;There is a tension at the heart of every enterprise RAG system. Better retrieval recall means more documents in the context. More documents in the context means longer prompts. Longer prompts mean higher inference cost, higher latency, and, past a certain length, degraded generation quality as the model's effective attention dilutes across too much content.&lt;/p&gt;

&lt;p&gt;The way most teams resolve this tension is by limiting the number of retrieved documents passed to the LLM. Pull five chunks, pass five chunks. The limit is arbitrary but practically reasonable.&lt;/p&gt;

&lt;p&gt;Context compression offers a different resolution. Instead of limiting how many documents you include, you reduce how many tokens each document contribution requires while preserving the information that is actually relevant to the specific query. The goal is to make the information denser rather than making the selection narrower.&lt;/p&gt;

&lt;p&gt;This is not summarization. Summarization loses information indiscriminately. Context compression preserves information that is relevant to the query and discards information that is not. The output is a more efficient representation of the documents for that specific query, not a shorter version of the documents in general.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters in practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a typical enterprise retrieval scenario. An employee asks about the approval process for a specific expense category. The retrieval system pulls five chunks. One chunk is from the expense policy document and contains exactly the answer, embedded in two paragraphs about the approval process surrounded by three paragraphs about submission timelines, receipt requirements, and currency conversion for international expenses. The other four chunks are from adjacent policy sections that contain some relevant context but also significant irrelevant content.&lt;/p&gt;

&lt;p&gt;Without compression, the LLM receives roughly 2,500 tokens across these five chunks, of which maybe 600 tokens are directly relevant to the question. The other 1,900 tokens are noise relative to this query, and at sufficient volume they degrade generation quality by diluting the model's attention on the relevant content.&lt;/p&gt;

&lt;p&gt;With compression applied, the LLM receives the relevant portions of each chunk, perhaps 700 tokens total, representing a 72% token reduction with no loss of query-relevant information. The answer it generates is not just cheaper and faster. It is more accurate because the relevant content is not competing with irrelevant content for the model's attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementing LLM-based context compression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most straightforward compression approach uses a small, fast LLM to extract the query-relevant portions of each retrieved chunk before passing the results to the main generation model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.retrievers.document_compressors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLMChainExtractor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.retrievers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextualCompressionRetriever&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# Or replace with your self-hosted inference endpoint
&lt;/span&gt;&lt;span class="n"&gt;compression_llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# small/fast model for compression
&lt;/span&gt;    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;compressor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLMChainExtractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compression_llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;compression_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ContextualCompressionRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_compressor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vectorstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage - retrieves 8 chunks, compresses each to query-relevant portions,
# returns compressed versions to generation model
&lt;/span&gt;&lt;span class="n"&gt;compressed_docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compression_retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_relevant_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compressor calls the compression LLM once per retrieved document, passing the document content and the original query, and asking the model to extract only the portions relevant to answering the query. Documents where no portion is relevant return an empty extraction and are dropped entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding a compression step adds latency and LLM cost. Whether this is net positive depends on your specific situation.&lt;/p&gt;

&lt;p&gt;The compression adds API calls to a fast/cheap model (gpt-4o-mini, haiku, or a self-hosted small model). These calls are parallelizable and each one is processing a relatively small document chunk. At typical chunk sizes of 400 to 800 tokens, the compression call for each chunk is fast and inexpensive.&lt;/p&gt;

&lt;p&gt;The savings come from reducing tokens in the main generation call, which uses a larger, more expensive model. If your main generation model costs 10x more per token than your compression model, and compression reduces the generation context by 60%, the net token cost is lower even accounting for the compression overhead.&lt;/p&gt;

&lt;p&gt;For organizations with high query volumes, the economics are usually favorable. For low-volume deployments where latency matters more than cost, the additional latency from the compression step may not be justified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_compression_economics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;avg_retrieved_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avg_chunk_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected_compression_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# e.g., 0.35 means 35% of tokens retained
&lt;/span&gt;    &lt;span class="n"&gt;compression_model_cost_per_1k_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;generation_model_cost_per_1k_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;daily_queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tokens_before_compression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;avg_retrieved_chunks&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;avg_chunk_tokens&lt;/span&gt;
    &lt;span class="n"&gt;tokens_after_compression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens_before_compression&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;expected_compression_ratio&lt;/span&gt;

    &lt;span class="n"&gt;daily_compression_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;daily_queries&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tokens_before_compression&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;compression_model_cost_per_1k_tokens&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;daily_generation_savings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;daily_queries&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens_before_compression&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tokens_after_compression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
        &lt;span class="n"&gt;generation_model_cost_per_1k_tokens&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_compression_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;daily_compression_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_generation_savings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;daily_generation_savings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_daily_savings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;daily_generation_savings&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;daily_compression_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;break_even_compression_ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;compression_model_cost_per_1k_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;generation_model_cost_per_1k_tokens&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this with your actual model costs and expected compression ratio to determine whether the economics make sense for your deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embedding-based compression as a faster alternative&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For deployments where the latency of LLM-based compression is a constraint, embedding-based compression provides a lighter alternative. Rather than using an LLM to extract relevant sentences, it uses embedding similarity to filter individual sentences within each chunk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.retrievers.document_compressors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EmbeddingsFilter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIEmbeddings&lt;/span&gt;

&lt;span class="c1"&gt;# Or replace with your self-hosted embedding model
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIEmbeddings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;embeddings_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingsFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;similarity_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;  &lt;span class="c1"&gt;# only retain sentences with similarity above this threshold
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;compression_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ContextualCompressionRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_compressor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vectorstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach filters individual sentences within each document based on their embedding similarity to the query. Sentences below the similarity threshold are dropped. It is faster than LLM-based compression because it uses embedding inference rather than generation, but it is less accurate because it cannot use natural language understanding to determine relevance.&lt;/p&gt;

&lt;p&gt;The appropriate choice between LLM-based and embedding-based compression depends on your latency requirements and your tolerance for compression accuracy. For high-stakes queries where answer accuracy matters, LLM-based compression is worth the additional latency. For high-volume, latency-sensitive applications, embedding-based compression provides meaningful context reduction at lower cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combining compression with reranking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a mature retrieval pipeline, compression and reranking address complementary problems and are most effective when combined. The retrieval pipeline order that I use in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Initial retrieval: embedding similarity search, k=20 candidates&lt;/li&gt;
&lt;li&gt;Reranking: cross-encoder reranks to top 8 by relevance to query&lt;/li&gt;
&lt;li&gt;Compression: LLM-based extraction of query-relevant portions from each of the top 8&lt;/li&gt;
&lt;li&gt;Generation: main LLM receives compressed, reranked content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pipeline produces a context that is both more relevant (from reranking) and more dense (from compression) than the baseline retrieve-and-generate approach. The cost is added latency from the additional processing steps. In my experience, the quality improvement justifies the latency cost for most enterprise knowledge retrieval applications, though the specific tradeoff should be measured against your actual query distribution and user requirements.&lt;/p&gt;

&lt;p&gt;The goal is to give the generation model the most useful possible context for each specific query, not the most content. Compression is one of the underused techniques for achieving that goal.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>performance</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building Agentic Workflows That Don't Fall Apart in Production: What I've Learned the Hard Way</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Fri, 03 Jul 2026 11:38:40 +0000</pubDate>
      <link>https://dev.to/nolanvale/building-agentic-workflows-that-dont-fall-apart-in-production-what-ive-learned-the-hard-way-53ig</link>
      <guid>https://dev.to/nolanvale/building-agentic-workflows-that-dont-fall-apart-in-production-what-ive-learned-the-hard-way-53ig</guid>
      <description>&lt;p&gt;The gap between an AI agent that works in a demo and one that works reliably over months of production operation is larger than most teams anticipate. I have built and maintained agentic AI systems across several enterprise deployments, and the failure modes I keep encountering are consistent enough that I want to document them specifically.&lt;/p&gt;

&lt;p&gt;This is not a tutorial on how to build an agent. It is a description of the specific things that break in production that were not obvious during development, and the architectural decisions that prevent or mitigate those failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why agents fail differently from RAG systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A RAG system has a bounded failure mode. It retrieves context, generates a response, and either the response is accurate or it is not. The failure is contained to a single interaction and the blast radius is limited to one user's trust in one answer.&lt;/p&gt;

&lt;p&gt;An agentic system has unbounded failure potential. An agent that can take actions, update records, send communications, trigger processes, or spawn other agents can fail in ways that compound across time and affect people beyond the user who initiated the interaction. A retrieval error produces a wrong answer. An agent error can produce a wrong action, and wrong actions in enterprise systems often have consequences that are difficult or impossible to reverse.&lt;/p&gt;

&lt;p&gt;This fundamental difference in failure surface is the reason that building agents requires a different set of architectural precautions than building RAG systems, and why most RAG-first teams are surprised by the problems they encounter when they add action capabilities to their AI systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The state management problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most common agentic failure I encounter in production is a state management failure: the agent loses track of where it is in a multi-step task and either starts over, gets stuck, or makes decisions based on an incorrect model of what has already happened.&lt;/p&gt;

&lt;p&gt;In development, this problem is rare because tasks run to completion quickly in a controlled environment. In production, tasks get interrupted by network failures, rate limits, user session timeouts, and system restarts. An agent that cannot resume a partially completed task correctly is an agent that will occasionally take the wrong action after a restart, thinking it is in a different position than it actually is.&lt;/p&gt;

&lt;p&gt;The architectural solution is checkpointing with immutable state records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;IN_PROGRESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;AWAITING_APPROVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;awaiting_approval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;COMPLETED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CANCELLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskCheckpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskStatus&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;agent_reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;           &lt;span class="c1"&gt;# what the agent was thinking at this step
&lt;/span&gt;    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;requires_human_approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckpointedAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint_store&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpoint_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkpoint_store&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps_completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskCheckpoint&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_history&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskCheckpoint&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpoint_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps_completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No previous steps. Starting fresh.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;completed_summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_reasoning&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps_completed&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Previously completed steps:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;completed_summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step_inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;checkpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TaskCheckpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps_completed&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;step_inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;agent_reasoning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;requires_human_approval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;requires_approval&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpoint_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requires_approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AWAITING_APPROVAL&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpoint_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AwaitingApprovalException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; requires human approval before proceeding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Execute the actual step here
&lt;/span&gt;        &lt;span class="c1"&gt;# ...
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resume_after_approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approved_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;checkpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps_completed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AWAITING_APPROVAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This step is not awaiting approval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approved_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;approved_by&lt;/span&gt;
        &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IN_PROGRESS&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpoint_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Resume execution from this step
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical property here is that every state transition is logged before it is executed. If the system fails mid-execution, the checkpoint log tells you exactly what was done, what was not done, and what the agent was intending to do next. Resuming from a known state is dramatically safer than attempting to infer state from partial completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approval gate pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every agentic system I build that operates on real enterprise data now includes what I call approval gates: explicit checkpoints where the agent cannot proceed without human confirmation.&lt;/p&gt;

&lt;p&gt;The reasoning is straightforward. Agents make mistakes. Mistakes in action-taking agents affect the real world. The cost of requiring human approval for consequential actions is a friction that slows execution. The cost of an agent taking incorrect consequential actions is a much larger operational problem. For enterprise deployments, the second cost consistently outweighs the first.&lt;/p&gt;

&lt;p&gt;What makes approval gates work in practice is specificity. A vague approval request ("the AI wants to do something, please approve") does not work. Approval requests need to specify: what action is proposed, what the agent's reasoning was, what the expected outcome is, what the reversibility is if the action turns out to be wrong, and what happens if the approver does nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;requesting_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;proposed_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;action_parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;agent_reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expected_outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;reversibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "fully reversible", "partially reversible", "irreversible"
&lt;/span&gt;    &lt;span class="n"&gt;auto_approve_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# seconds until auto-approval, None for manual-only
&lt;/span&gt;    &lt;span class="n"&gt;auto_deny_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# seconds until auto-denial if no response
&lt;/span&gt;    &lt;span class="n"&gt;context_summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;        &lt;span class="c1"&gt;# what has happened so far in this task
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApprovalGateManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify_approvers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# For irreversible actions, never auto-approve
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversibility&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;irreversible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auto_approve_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auto_deny_after&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auto_approve_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Time-based auto-approval for low-risk actions
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Default to denial for safety
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_denial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout_no_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reversibility field drives the strictest control: irreversible actions always require explicit human approval, no exceptions. This is the line I do not move regardless of how much the operational team wants to eliminate the friction for specific workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool call audit problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an agent calls a tool, whether that is querying a database, updating a CRM record, sending an email, or calling an external API, that tool call needs to be logged in a format that satisfies two requirements simultaneously: technical debugging and compliance audit.&lt;/p&gt;

&lt;p&gt;These requirements conflict. Technical debugging logs want raw data, full request and response payloads, timing information, error details. Compliance audit logs want a human-readable description of what happened and why, without the raw data that might contain personal information or proprietary content.&lt;/p&gt;

&lt;p&gt;The architecture that serves both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToolCallRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

    &lt;span class="c1"&gt;# Technical log (internal, full detail)
&lt;/span&gt;    &lt;span class="nb"&gt;raw_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;          &lt;span class="c1"&gt;# full tool input parameters
&lt;/span&gt;    &lt;span class="n"&gt;raw_output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;         &lt;span class="c1"&gt;# full tool output
&lt;/span&gt;    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Compliance log (auditable, human-readable, privacy-safe)
&lt;/span&gt;    &lt;span class="n"&gt;action_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# "Updated contract renewal date for vendor X"
&lt;/span&gt;    &lt;span class="n"&gt;data_accessed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# ["contract_id:12345", "vendor:acme_corp"]
&lt;/span&gt;    &lt;span class="n"&gt;data_modified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# ["renewal_date", "contract_status"]
&lt;/span&gt;    &lt;span class="n"&gt;authorized_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;       &lt;span class="c1"&gt;# user or policy that authorized this action
&lt;/span&gt;    &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;             &lt;span class="c1"&gt;# "success", "failure", "partial"
&lt;/span&gt;
    &lt;span class="c1"&gt;# Immutability controls
&lt;/span&gt;    &lt;span class="n"&gt;logged_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;log_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;            &lt;span class="c1"&gt;# hash of the record content for tamper detection
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compliance log fields are what your legal team and auditors need to see. The technical log fields are what your engineers need when something goes wrong. Both live in the same record. They go to different downstream destinations with different access controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling partial failures gracefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Production agentic systems encounter partial failures constantly. A tool call times out. An API rate limit is hit. A database record was modified by another user between when the agent read it and when it tried to update it. An external service returns an unexpected response format.&lt;/p&gt;

&lt;p&gt;The naive approach is to treat any failure as a task failure and surface an error to the user. This is too conservative. Many partial failures are recoverable without human intervention.&lt;/p&gt;

&lt;p&gt;The approach that works: classify failures by their recoverability at the tool call level rather than the task level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FailureClassifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;TRANSIENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# retry likely to succeed
&lt;/span&gt;    &lt;span class="n"&gt;STALE_DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# re-read and retry
&lt;/span&gt;    &lt;span class="n"&gt;PERMISSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# requires human intervention
&lt;/span&gt;    &lt;span class="n"&gt;DATA_ERROR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# data quality issue, needs investigation
&lt;/span&gt;    &lt;span class="n"&gt;FATAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fatal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# task cannot complete, surface to user
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANSIENT&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optimistic_lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version_conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STALE_DATA&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unauthorized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PERMISSION&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATA_ERROR&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FATAL&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResilientToolExecutor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# exponential backoff
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HumanInterventionRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Permission error on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TaskFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unrecoverable error on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This classification-based retry strategy means that transient infrastructure failures do not surface as user-visible errors, stale data conflicts get automatically refreshed and retried, and only genuinely unrecoverable failures escalate to human attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The monitoring layer that actually matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For agentic systems, the metrics that matter are different from the metrics that matter for RAG systems.&lt;/p&gt;

&lt;p&gt;Queue depth and task age are the early warning metrics. If tasks are sitting in the pending queue for longer than expected, something has stalled. If a task is in-progress for longer than the expected maximum execution time, the agent is probably stuck in a loop or waiting on an approval that was missed.&lt;/p&gt;

&lt;p&gt;Tool call success rates by tool are more useful than overall success rates. An agentic system can have a high task completion rate while masking a specific tool that is failing silently and causing tasks to take workaround paths. Broken down by tool, you see the specific integration that is causing problems rather than a blended metric that looks acceptable.&lt;/p&gt;

&lt;p&gt;Human intervention rate is the metric I watch most closely for agentic systems. It measures what percentage of task executions required human approval or intervention beyond what was expected. Rising intervention rates indicate either that the agent's judgment is not calibrated correctly for the real-world task distribution, or that the task types being handled have evolved beyond what the agent was designed for. Either case requires investigation.&lt;/p&gt;

&lt;p&gt;Building the monitoring layer is not glamorous work. It also does not show up in demos. But the systems that remain trustworthy in production over eighteen months are almost always the systems where the monitoring layer was built with the same care as the execution layer.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>production</category>
    </item>
    <item>
      <title>The Architecture Diagram Looked Perfect. The Operations Team Disagreed.</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Thu, 02 Jul 2026 17:32:14 +0000</pubDate>
      <link>https://dev.to/nolanvale/the-architecture-diagram-looked-perfect-the-operations-team-disagreed-5094</link>
      <guid>https://dev.to/nolanvale/the-architecture-diagram-looked-perfect-the-operations-team-disagreed-5094</guid>
      <description>&lt;p&gt;A few years ago, architecture reviews were mostly about scalability.&lt;/p&gt;

&lt;p&gt;Can the system handle more users?&lt;/p&gt;

&lt;p&gt;Can it survive traffic spikes?&lt;/p&gt;

&lt;p&gt;Can it recover from failures?&lt;/p&gt;

&lt;p&gt;Today, another question has quietly become just as important.&lt;/p&gt;

&lt;p&gt;Can people actually govern what the AI is doing?&lt;/p&gt;

&lt;p&gt;That shift changes the way I evaluate enterprise systems.&lt;/p&gt;

&lt;p&gt;A clean architecture diagram is no longer enough.&lt;/p&gt;

&lt;p&gt;I want to understand how the system behaves after deployment, when different departments start relying on it every day.&lt;/p&gt;

&lt;p&gt;One pattern I've noticed is that AI projects often inherit yesterday's architecture.&lt;/p&gt;

&lt;p&gt;Documents remain in one platform.&lt;/p&gt;

&lt;p&gt;Conversations happen somewhere else.&lt;/p&gt;

&lt;p&gt;Customer data lives in the CRM.&lt;/p&gt;

&lt;p&gt;Internal knowledge sits in another repository.&lt;/p&gt;

&lt;p&gt;Then an AI layer is placed on top and expected to make sense of everything.&lt;/p&gt;

&lt;p&gt;Technically, this works.&lt;/p&gt;

&lt;p&gt;Operationally, it creates a different problem.&lt;/p&gt;

&lt;p&gt;Every additional source introduces another permission model, another synchronization process, and another place where context can become inconsistent.&lt;/p&gt;

&lt;p&gt;The architecture still functions.&lt;/p&gt;

&lt;p&gt;It simply becomes harder to reason about.&lt;/p&gt;

&lt;p&gt;During design reviews, I often stop discussing the language model altogether.&lt;/p&gt;

&lt;p&gt;Instead, I sketch a simple diagram on a whiteboard.&lt;/p&gt;

&lt;p&gt;Data.&lt;/p&gt;

&lt;p&gt;People.&lt;/p&gt;

&lt;p&gt;Agents.&lt;/p&gt;

&lt;p&gt;Approvals.&lt;/p&gt;

&lt;p&gt;If someone cannot explain how information moves between those four elements, the project usually isn't ready for production.&lt;/p&gt;

&lt;p&gt;The interesting part is that these conversations rarely involve machine learning.&lt;/p&gt;

&lt;p&gt;They're conversations about ownership.&lt;/p&gt;

&lt;p&gt;Who is responsible for the data?&lt;/p&gt;

&lt;p&gt;Who decides which agent can access it?&lt;/p&gt;

&lt;p&gt;Who reviews actions before they affect customers?&lt;/p&gt;

&lt;p&gt;Who investigates incidents when something goes wrong?&lt;/p&gt;

&lt;p&gt;Those questions belong to architecture just as much as infrastructure diagrams do.&lt;/p&gt;

&lt;p&gt;I've also become cautious whenever I hear the phrase "the AI has access to everything."&lt;/p&gt;

&lt;p&gt;It sounds convenient.&lt;/p&gt;

&lt;p&gt;It usually isn't.&lt;/p&gt;

&lt;p&gt;Broad access reduces friction during demonstrations.&lt;/p&gt;

&lt;p&gt;It increases risk during operations.&lt;/p&gt;

&lt;p&gt;The strongest enterprise architectures don't maximize visibility.&lt;/p&gt;

&lt;p&gt;They define boundaries intentionally.&lt;/p&gt;

&lt;p&gt;Finance shouldn't need access to HR conversations.&lt;/p&gt;

&lt;p&gt;Support agents shouldn't automatically retrieve executive planning documents.&lt;/p&gt;

&lt;p&gt;An AI agent shouldn't become an exception to those principles simply because it can search faster than a human.&lt;/p&gt;

&lt;p&gt;This is why I find architectures built around isolation more convincing than architectures built around unrestricted connectivity.&lt;/p&gt;

&lt;p&gt;When collaboration spaces, permissions, and AI agents share the same governance model, operational complexity tends to decrease instead of increase.&lt;/p&gt;

&lt;p&gt;That's one aspect I appreciate when looking at platforms like PrivOS.&lt;/p&gt;

&lt;p&gt;Its architecture emphasizes room-level isolation, governed collaboration, and privacy-first deployment rather than assuming every dataset should become universally searchable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://privos.ai/" rel="noopener noreferrer"&gt;https://privos.ai/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architecture is often described as the art of making systems work.&lt;/p&gt;

&lt;p&gt;I think enterprise architecture has become something slightly different.&lt;/p&gt;

&lt;p&gt;It's the art of making complex systems understandable.&lt;/p&gt;

&lt;p&gt;When people understand how a system behaves, they trust it.&lt;/p&gt;

&lt;p&gt;And trust is still the hardest thing to scale.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>devops</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Self-hosted vs external API: an honest comparison table</title>
      <dc:creator>Nolan Vale</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:38:01 +0000</pubDate>
      <link>https://dev.to/nolanvale/self-hosted-vs-external-api-an-honest-comparison-table-npl</link>
      <guid>https://dev.to/nolanvale/self-hosted-vs-external-api-an-honest-comparison-table-npl</guid>
      <description>&lt;p&gt;People keep asking me this. Here is the actual tradeoff matrix I use with clients instead of a generic answer.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;External API&lt;/th&gt;
&lt;th&gt;Self-hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to first working demo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Days to weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to production-ready&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weeks&lt;/td&gt;
&lt;td&gt;Months (or days with a platform like PrivOS at &lt;a href="https://privos.ai/" rel="noopener noreferrer"&gt;https://privos.ai/&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inference quality (frontier tasks)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Slightly lower on complex reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data leaves your network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GDPR / data residency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on DPA&lt;/td&gt;
&lt;td&gt;Fully controlled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost at low volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cheaper&lt;/td&gt;
&lt;td&gt;More expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost at high volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gets expensive fast&lt;/td&gt;
&lt;td&gt;Predictable infra cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vendor lock-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Almost none&lt;/td&gt;
&lt;td&gt;Real and ongoing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Access control granularity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Platform-dependent&lt;/td&gt;
&lt;td&gt;You control it entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audit log completeness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor-defined&lt;/td&gt;
&lt;td&gt;You define it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works behind firewall/VPN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model upgrade control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor decides timing&lt;/td&gt;
&lt;td&gt;You decide timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fine-tuning on your data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data leaves your network&lt;/td&gt;
&lt;td&gt;Stays internal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;When external API wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are moving fast and data sensitivity is low&lt;/li&gt;
&lt;li&gt;You need frontier reasoning quality right now&lt;/li&gt;
&lt;li&gt;You do not have engineering capacity to maintain infrastructure&lt;/li&gt;
&lt;li&gt;Your compliance requirement is "enterprise agreement" not "data residency"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When self-hosted wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any regulated data (health, financial, legal, HR)&lt;/li&gt;
&lt;li&gt;GDPR special category data&lt;/li&gt;
&lt;li&gt;Clients contractually require data not leave your infrastructure&lt;/li&gt;
&lt;li&gt;High query volume where API cost compounds&lt;/li&gt;
&lt;li&gt;You need full audit control for compliance evidence&lt;/li&gt;
&lt;li&gt;The word "subprocessor chain" makes your legal team uncomfortable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The case that's genuinely unclear:&lt;/strong&gt;&lt;br&gt;
Mid-market companies with moderate sensitivity data and limited DevOps capacity. External API with strong enterprise terms is defensible. Self-hosted with a deployment platform (not DIY) is also defensible. Run the 36-month cost model and the compliance scenario and see which one you can actually sleep next to.&lt;/p&gt;

&lt;p&gt;The right answer depends on your threat model, your compliance requirements, and your team's capacity. Anyone who gives you a definitive answer without knowing those three things is selling something.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
