<?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: Elena Revicheva</title>
    <description>The latest articles on DEV Community by Elena Revicheva (@elenarevicheva).</description>
    <link>https://dev.to/elenarevicheva</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%2F3877312%2Fbe9fea4a-1daa-4812-a168-514a5d9e3d09.jpeg</url>
      <title>DEV Community: Elena Revicheva</title>
      <link>https://dev.to/elenarevicheva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elenarevicheva"/>
    <language>en</language>
    <item>
      <title>Six Months of pgvector on Oracle Autonomous DB: RAG in Production</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Fri, 14 Aug 2026 19:30:15 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/six-months-of-pgvector-on-oracle-autonomous-db-rag-in-production-kim</link>
      <guid>https://dev.to/elenarevicheva/six-months-of-pgvector-on-oracle-autonomous-db-rag-in-production-kim</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/six-months-of-pgvector-on-oracle-autonomous-db-rag-in-production" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My RAG production system on Oracle Autonomous Database hit a wall at 10,000 vectors. Retrieval quality, previously acceptable, plummeted. The initial setup, a basic &lt;code&gt;pgvector&lt;/code&gt; index with &lt;code&gt;IVFFlat&lt;/code&gt;, was no longer sufficient. This wasn't a theoretical scaling issue; it was a live system failing to deliver accurate responses to paying users. The cost of a single &lt;code&gt;text-embedding-ada-002&lt;/code&gt; call was $0.0001, but the cost of a bad answer was a lost customer.&lt;/p&gt;

&lt;p&gt;We're running multi-agent systems, routing between Groq and Claude, serving Telegram and WhatsApp users. Our knowledge base isn't massive, but it's critical. Each agent needs precise context. When retrieval failed, agents hallucinated or defaulted to general knowledge, which is useless for specific user queries about their orders or our internal processes. This article details the specific changes we made, the numbers we saw, and why &lt;code&gt;IVFFlat&lt;/code&gt; failed us, leading to a shift to &lt;code&gt;HNSW&lt;/code&gt; and a re-evaluation of our embedding strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Setup: &lt;code&gt;pgvector&lt;/code&gt; and &lt;code&gt;IVFFlat&lt;/code&gt; on Oracle Autonomous DB
&lt;/h2&gt;

&lt;p&gt;Our first iteration used &lt;code&gt;pgvector&lt;/code&gt; on an Oracle Autonomous Database, specifically the PostgreSQL-compatible service. The setup was straightforward: a table with a &lt;code&gt;vector(1536)&lt;/code&gt; column for OpenAI's &lt;code&gt;text-embedding-ada-002&lt;/code&gt; embeddings. We chose &lt;code&gt;IVFFlat&lt;/code&gt; with &lt;code&gt;lists = 100&lt;/code&gt; for our initial index. This seemed reasonable for a dataset under 5,000 vectors. Query latency was consistently under 50ms for &lt;code&gt;k=5&lt;/code&gt; nearest neighbors.&lt;/p&gt;

&lt;p&gt;The problem wasn't immediate. For the first few thousand vectors, &lt;code&gt;IVFFlat&lt;/code&gt; performed adequately. Our RAG accuracy, measured by human evaluation of agent responses, hovered around 85% for relevant queries. This was acceptable for our MVP. However, as our knowledge base grew, incorporating more internal documentation, product specifications, and customer support FAQs, we crossed the 10,000-vector mark. Suddenly, our retrieval quality dropped to below 60%. Agents started pulling irrelevant documents, leading to nonsensical answers. A query for "how to reset my password" might retrieve a document about "billing cycles."&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IVFFlat&lt;/code&gt; index, while fast for small datasets, sacrifices recall for speed. As the number of vectors increases, the probability of the true nearest neighbors falling into a different list than the query vector increases significantly. Our &lt;code&gt;lists = 100&lt;/code&gt; was too coarse for 10,000 vectors. Increasing &lt;code&gt;lists&lt;/code&gt; would improve recall but degrade query performance, potentially pushing us over our 100ms agent response time budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedding Model Trade-offs: &lt;code&gt;ada-002&lt;/code&gt; vs. &lt;code&gt;text-embedding-3-small&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before diving deeper into indexing, we re-evaluated our embedding model. We started with &lt;code&gt;text-embedding-ada-002&lt;/code&gt; due to its ubiquity and reasonable cost. Each embedding cost $0.0001 per 1,000 tokens. A typical document chunk of 250 tokens cost $0.000025. With 10,000 vectors, our embedding cost was negligible, perhaps $2.50 total for the entire knowledge base.&lt;/p&gt;

&lt;p&gt;However, OpenAI released &lt;code&gt;text-embedding-3-small&lt;/code&gt; at a significantly lower cost ($0.00002 per 1,000 tokens) and improved performance. We ran a small experiment: re-embedding 1,000 critical documents with &lt;code&gt;text-embedding-3-small&lt;/code&gt; (1536 dimensions, same as &lt;code&gt;ada-002&lt;/code&gt; for direct comparison) and comparing retrieval quality.&lt;/p&gt;

&lt;p&gt;The results were subtle but positive. For the same &lt;code&gt;k=5&lt;/code&gt; retrieval, &lt;code&gt;text-embedding-3-small&lt;/code&gt; showed a 3-5% improvement in relevant document retrieval, as judged by our internal evaluators. The cost reduction was a bonus, but the primary driver was the slight quality bump. We decided to re-embed our entire knowledge base using &lt;code&gt;text-embedding-3-small&lt;/code&gt;. This cost us approximately $0.50 for 10,000 vectors. This change alone didn't fix the 10k vector retrieval issue, but it provided a better foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift to &lt;code&gt;HNSW&lt;/code&gt;: Reclaiming Retrieval Quality
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IVFFlat&lt;/code&gt; index was the bottleneck. After extensive reading and testing on a staging environment, &lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small World) emerged as the clear successor for our vector count. &lt;code&gt;HNSW&lt;/code&gt; builds a graph structure that allows for more efficient nearest neighbor searches, offering a better balance between recall and speed than &lt;code&gt;IVFFlat&lt;/code&gt; for larger datasets.&lt;/p&gt;

&lt;p&gt;Creating an &lt;code&gt;HNSW&lt;/code&gt; index on &lt;code&gt;pgvector&lt;/code&gt; is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_l2_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We chose &lt;code&gt;m = 16&lt;/code&gt; (number of neighbors for graph construction) and &lt;code&gt;ef_construction = 100&lt;/code&gt; (size of dynamic list for construction). These parameters are crucial. Higher &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;ef_construction&lt;/code&gt; values lead to a more accurate index but take longer to build and consume more memory.&lt;/p&gt;

&lt;p&gt;The index build time for 10,000 vectors on our Oracle Autonomous DB instance (2 OCPUs, 16GB RAM) was approximately 15 minutes. This was acceptable for our deployment cycle.&lt;/p&gt;

&lt;p&gt;After switching to &lt;code&gt;HNSW&lt;/code&gt;, we immediately saw a significant improvement. Query latency for &lt;code&gt;k=5&lt;/code&gt; increased slightly to 60-80ms, but retrieval quality jumped back to 80-82%. This was a crucial win. The &lt;code&gt;HNSW&lt;/code&gt; index, with its graph-based approach, was far more effective at finding true nearest neighbors in our 10,000-vector dataset.&lt;/p&gt;

&lt;p&gt;We also experimented with &lt;code&gt;ef_search&lt;/code&gt; during query time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ef_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[...query_vector...]'&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Increasing &lt;code&gt;ef_search&lt;/code&gt; (size of dynamic list for search) from the default of &lt;code&gt;ef_construction&lt;/code&gt; (100 in our case) to &lt;code&gt;150&lt;/code&gt; further improved recall by about 2% but pushed query latency to 100-120ms. We settled on &lt;code&gt;ef_search = 100&lt;/code&gt; to maintain our 100ms response time budget for agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oracle Autonomous DB Specifics and Cost Implications
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;pgvector&lt;/code&gt; on Oracle Autonomous Database (PostgreSQL-compatible) has its quirks. While it provides a managed PostgreSQL environment, direct access to OS-level tuning is limited. We rely on Oracle's underlying infrastructure for performance and stability. Our instance is a 2 OCPU, 16GB RAM configuration, costing approximately $0.30/hour. This translates to about $216/month.&lt;/p&gt;

&lt;p&gt;The storage for our 10,000 vectors (1536 dimensions, 4 bytes per dimension) is roughly 60MB. This is negligible in terms of storage cost. The primary cost driver is the compute for the database instance itself.&lt;/p&gt;

&lt;p&gt;We considered moving to a dedicated VM with PostgreSQL for more control, but the managed nature of Autonomous DB, including automatic backups, patching, and scaling, outweighed the desire for granular control over &lt;code&gt;pgvector&lt;/code&gt; parameters. The performance we achieved with &lt;code&gt;HNSW&lt;/code&gt; on this setup was sufficient for our current scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Scaling: Beyond 50,000 Vectors
&lt;/h2&gt;

&lt;p&gt;Our current knowledge base is around 12,000 vectors. We anticipate reaching 50,000 vectors within the next 6-9 months. At that point, we expect &lt;code&gt;HNSW&lt;/code&gt; to continue performing well, but we will need to re-evaluate &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;ef_construction&lt;/code&gt; parameters.&lt;/p&gt;

&lt;p&gt;For datasets exceeding 100,000 vectors, we would consider sharding our knowledge base or exploring more specialized vector databases. However, for our current and projected scale, &lt;code&gt;pgvector&lt;/code&gt; with &lt;code&gt;HNSW&lt;/code&gt; on Oracle Autonomous DB provides a cost-effective and performant solution. The key was understanding the limitations of &lt;code&gt;IVFFlat&lt;/code&gt; and making a data-driven decision to switch to a more robust indexing algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not use a dedicated vector database like Pinecone or Weaviate?&lt;/strong&gt;&lt;br&gt;
A: For our current scale (under 50k vectors), &lt;code&gt;pgvector&lt;/code&gt; on Oracle Autonomous DB is significantly more cost-effective and simpler to manage within our existing infrastructure. Dedicated vector databases introduce additional operational overhead and cost that we don't need yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you monitor retrieval quality in production?&lt;/strong&gt;&lt;br&gt;
A: We use a combination of automated metrics (e.g., cosine similarity distribution of retrieved documents) and human-in-the-loop evaluation. Our agents log the retrieved documents, and a subset of agent responses are reviewed daily by a human to assess relevance and accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What was the exact performance difference in query latency between &lt;code&gt;IVFFlat&lt;/code&gt; and &lt;code&gt;HNSW&lt;/code&gt; for 10,000 vectors?&lt;/strong&gt;&lt;br&gt;
A: With &lt;code&gt;IVFFlat&lt;/code&gt; (&lt;code&gt;lists=100&lt;/code&gt;), &lt;code&gt;k=5&lt;/code&gt; queries were consistently under 50ms. With &lt;code&gt;HNSW&lt;/code&gt; (&lt;code&gt;m=16, ef_construction=100, ef_search=100&lt;/code&gt;), &lt;code&gt;k=5&lt;/code&gt; queries were 60-80ms. The slight increase in latency was a worthwhile trade-off for the significant improvement in recall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Did you consider other embedding models besides OpenAI?&lt;/strong&gt;&lt;br&gt;
A: Yes, we briefly experimented with open-source models like &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; but found their quality insufficient for our specific domain without extensive fine-tuning. The cost-performance ratio of OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt; was optimal for our needs, especially given its low price point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle document chunking for RAG?&lt;/strong&gt;&lt;br&gt;
A: We use a fixed-size chunking strategy with overlap. For most documents, we chunk into 250-token segments with a 50-token overlap. For structured data like tables, we use a more sophisticated approach that attempts to keep related rows or sections together.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>pgvector on Oracle Autonomous DB: 6 Months, 10k Vectors, and RAG Failure</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Thu, 13 Aug 2026 19:30:16 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-lg0</link>
      <guid>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-lg0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-2026-08-13" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My RAG system’s retrieval quality tanked at 10,000 vectors. Not at 100k, not at 1M. Ten thousand. This wasn't a theoretical scaling limit; it was a production reality on Oracle Autonomous Database with &lt;code&gt;pgvector&lt;/code&gt;. We had shipped a multi-agent system for a client, handling customer support queries via Telegram and WhatsApp, routing to specialized Groq-powered agents. The core knowledge base, critical for accurate responses, lived in &lt;code&gt;pgvector&lt;/code&gt;. The initial 5,000 vectors worked beautifully. Then we doubled the data, and the system started hallucinating specific details, citing irrelevant documents, and generally failing to provide the precise answers our agents needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Setup: &lt;code&gt;text-embedding-ada-002&lt;/code&gt; and IVFFlat
&lt;/h2&gt;

&lt;p&gt;We started with &lt;code&gt;text-embedding-ada-002&lt;/code&gt; for its cost-effectiveness and decent performance on general knowledge. Each embedding was 1536 dimensions. Our Oracle Autonomous Database instance (shared infrastructure, OCPU count scaled on demand) runs PostgreSQL 14.8. &lt;code&gt;pgvector&lt;/code&gt; was installed as an extension. We chose &lt;code&gt;IVFFlat&lt;/code&gt; with &lt;code&gt;lists = 100&lt;/code&gt; as our index type, primarily because it was simpler to configure and seemed sufficient for our projected data size of "tens of thousands." Our data consisted of product manuals, internal FAQs, and customer interaction transcripts, chunked to ~250 tokens.&lt;/p&gt;

&lt;p&gt;The ingestion pipeline was straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch text chunks.&lt;/li&gt;
&lt;li&gt;Embed with OpenAI API.&lt;/li&gt;
&lt;li&gt;Insert into &lt;code&gt;pgvector&lt;/code&gt; table: &lt;code&gt;CREATE TABLE knowledge_base (id UUID PRIMARY KEY, content TEXT, embedding VECTOR(1536));&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create index: &lt;code&gt;CREATE INDEX ON knowledge_base USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For retrieval, we used &lt;code&gt;ORDER BY embedding &amp;lt;-&amp;gt; query_embedding LIMIT 5&lt;/code&gt;. This worked. Our agents were pulling relevant context, and our human validation step showed an average precision@5 of 0.85 for the first 5,000 vectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10,000 Vector Cliff: Why IVFFlat Failed
&lt;/h2&gt;

&lt;p&gt;At 10,000 vectors, our precision@5 dropped to 0.55. The agents started pulling documents that were semantically related but not &lt;em&gt;specifically&lt;/em&gt; relevant to the user's query. For example, a query about "refund policy for digital goods" would retrieve documents about "general refund process" and "digital product activation," missing the specific clause about non-refundable digital items.&lt;/p&gt;

&lt;p&gt;The root cause was &lt;code&gt;IVFFlat&lt;/code&gt;. While &lt;code&gt;lists = 100&lt;/code&gt; seemed reasonable for 10,000 vectors, it wasn't. &lt;code&gt;IVFFlat&lt;/code&gt; works by partitioning the vector space into &lt;code&gt;lists&lt;/code&gt; clusters. When a query comes in, it finds the nearest &lt;code&gt;n_probe&lt;/code&gt; clusters and searches only within those. My &lt;code&gt;n_probe&lt;/code&gt; was implicitly 1 (the default for &lt;code&gt;pgvector&lt;/code&gt;'s &lt;code&gt;IVFFlat&lt;/code&gt; search if not specified, or effectively very low if not tuned). With 10,000 vectors and 100 lists, each list contained an average of 100 vectors. This is too many for efficient and accurate nearest neighbor search within a single list, especially in high dimensions. The initial clustering itself might not have been optimal for our specific data distribution, leading to relevant vectors being placed in distant lists.&lt;/p&gt;

&lt;p&gt;The solution wasn't to just increase &lt;code&gt;n_probe&lt;/code&gt;. Increasing &lt;code&gt;n_probe&lt;/code&gt; improves recall but significantly increases query latency. For our real-time agent interactions, a 500ms retrieval latency was already pushing it. Increasing &lt;code&gt;n_probe&lt;/code&gt; to, say, 10, would have meant searching 10% of the data, which for 10,000 vectors is 1,000 vectors. This would have pushed latency beyond acceptable limits for a synchronous agent call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HNSW Migration and &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; Experiment
&lt;/h2&gt;

&lt;p&gt;The immediate fix was to switch to &lt;code&gt;HNSW&lt;/code&gt;. &lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small World) builds a graph structure that allows for much more efficient approximate nearest neighbor search, especially in higher dimensions and larger datasets.&lt;/p&gt;

&lt;p&gt;The migration involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dropping the &lt;code&gt;IVFFlat&lt;/code&gt; index: &lt;code&gt;DROP INDEX knowledge_base_embedding_idx;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creating the &lt;code&gt;HNSW&lt;/code&gt; index: &lt;code&gt;CREATE INDEX ON knowledge_base USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;m = 16&lt;/code&gt;: Controls the number of neighbors each node connects to. Higher &lt;code&gt;m&lt;/code&gt; means denser graph, better recall, slower build, more memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ef_construction = 64&lt;/code&gt;: Controls the search scope during index build. Higher &lt;code&gt;ef_construction&lt;/code&gt; means better quality index, slower build.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After rebuilding the index (which took about 3 minutes for 10,000 vectors on our Oracle instance), retrieval latency for &lt;code&gt;LIMIT 5&lt;/code&gt; queries dropped from ~400ms (with &lt;code&gt;IVFFlat&lt;/code&gt; and its degraded recall) to ~150ms, and precision@5 jumped back to 0.88. This was a significant win.&lt;/p&gt;

&lt;p&gt;However, the cost of &lt;code&gt;text-embedding-ada-002&lt;/code&gt; was becoming a concern. At $0.0001 / 1K tokens, and with daily ingestion of new customer interactions, it was adding up. We decided to experiment with a smaller, open-source model: &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; (384 dimensions).&lt;/p&gt;

&lt;p&gt;The process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-embed all 10,000 vectors with &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;. This was a batch job, run on a local machine, taking about 2 hours.&lt;/li&gt;
&lt;li&gt;Alter table column: &lt;code&gt;ALTER TABLE knowledge_base ALTER COLUMN embedding TYPE VECTOR(384);&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rebuild HNSW index: &lt;code&gt;CREATE INDEX ON knowledge_base USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The results were mixed. Retrieval latency dropped further to ~80ms due to the smaller vector size. However, precision@5 dropped to 0.70. While &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; is fast and free to run locally, its semantic understanding for our specific domain (technical product support, nuanced policy details) was not on par with &lt;code&gt;ada-002&lt;/code&gt;. The agents started making subtle errors again, misinterpreting user intent due to less precise context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Current State: &lt;code&gt;text-embedding-3-small&lt;/code&gt; and HNSW
&lt;/h2&gt;

&lt;p&gt;We reverted to an OpenAI model, but opted for &lt;code&gt;text-embedding-3-small&lt;/code&gt;. This model offers 1536 dimensions (or configurable down to 256) at a significantly lower cost: $0.00002 / 1K tokens, a 5x reduction from &lt;code&gt;ada-002&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The current setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedding Model:&lt;/strong&gt; &lt;code&gt;text-embedding-3-small&lt;/code&gt; (1536 dimensions). Cost is now manageable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector Database:&lt;/strong&gt; &lt;code&gt;pgvector&lt;/code&gt; on Oracle Autonomous Database (PostgreSQL 14.8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index:&lt;/strong&gt; &lt;code&gt;HNSW&lt;/code&gt; with &lt;code&gt;m = 16, ef_construction = 64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Size:&lt;/strong&gt; Currently at 15,000 vectors, growing by ~500 vectors daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval Performance:&lt;/strong&gt; Precision@5 is back to 0.85. Latency for &lt;code&gt;LIMIT 5&lt;/code&gt; is ~120ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This configuration provides the best balance of cost, performance, and retrieval quality for our specific RAG needs. The Oracle Autonomous Database handles the scaling of compute and storage seamlessly, which is crucial for a lean operation like AIdeazz with zero VC funding. We pay for what we use, and the PostgreSQL service is robust.&lt;/p&gt;

&lt;p&gt;The key takeaway is that &lt;code&gt;pgvector&lt;/code&gt; is powerful, but its performance is heavily dependent on the index type and parameters, especially as data grows. Don't assume an &lt;code&gt;IVFFlat&lt;/code&gt; index with default parameters will scale beyond a few thousand vectors, even if benchmarks suggest it. Always test with your actual data and query patterns. And for production RAG, the embedding model choice is a critical trade-off between cost, speed, and semantic precision. Don't optimize for cost if it means your agents start hallucinating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling Beyond 100k Vectors
&lt;/h2&gt;

&lt;p&gt;Our current &lt;code&gt;HNSW&lt;/code&gt; setup is projected to handle up to 100,000 vectors with acceptable performance. Beyond that, we anticipate needing to tune &lt;code&gt;HNSW&lt;/code&gt; parameters (&lt;code&gt;m&lt;/code&gt;, &lt;code&gt;ef_construction&lt;/code&gt;, &lt;code&gt;ef_search&lt;/code&gt;) more aggressively or consider sharding the knowledge base. Oracle Autonomous Database's PostgreSQL service supports read replicas, which could offload query load, but for a single-node &lt;code&gt;pgvector&lt;/code&gt; instance, the index itself becomes the bottleneck.&lt;/p&gt;

&lt;p&gt;For truly massive scale (millions of vectors), dedicated vector databases like Qdrant or Pinecone, or even a distributed &lt;code&gt;pgvector&lt;/code&gt; setup with TimescaleDB's columnar storage and sharding, would be on the roadmap. But for now, &lt;code&gt;pgvector&lt;/code&gt; on Oracle Autonomous DB is a cost-effective and performant solution for our production RAG needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why Oracle Autonomous Database for &lt;code&gt;pgvector&lt;/code&gt; instead of a dedicated PostgreSQL instance on a VM?&lt;/strong&gt;&lt;br&gt;
A: Autonomous Database handles patching, backups, and scaling automatically. For a small team with zero ops budget, the managed service cost is offset by the lack of administrative overhead. We pay for OCPU and storage, not for managing the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What was the exact query latency for &lt;code&gt;IVFFlat&lt;/code&gt; at 10,000 vectors before switching to &lt;code&gt;HNSW&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A: Average query latency for &lt;code&gt;ORDER BY embedding &amp;lt;-&amp;gt; query_embedding LIMIT 5&lt;/code&gt; was 400ms. This was measured from our application code, including network roundtrip to the Oracle DB instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How did you measure precision@5 for your RAG system?&lt;/strong&gt;&lt;br&gt;
A: We have a human-in-the-loop validation process. For a sample of 100 agent interactions per week, human reviewers assess the top 5 retrieved documents for relevance to the user's query. Precision@5 is the average number of relevant documents in the top 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Did you consider other embedding models besides OpenAI and MiniLM?&lt;/strong&gt;&lt;br&gt;
A: Yes, we briefly tested Cohere Embed v3. Its performance was comparable to &lt;code&gt;ada-002&lt;/code&gt; but at a slightly higher cost at the time. Given our existing OpenAI API integration for LLMs, staying within the ecosystem simplified our tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the cost difference between &lt;code&gt;text-embedding-ada-002&lt;/code&gt; and &lt;code&gt;text-embedding-3-small&lt;/code&gt; for your current usage?&lt;/strong&gt;&lt;br&gt;
A: With 15,000 vectors and 500 new vectors daily, plus ~1000 queries daily, &lt;code&gt;ada-002&lt;/code&gt; would cost approximately $150/month. &lt;code&gt;text-embedding-3-small&lt;/code&gt; reduced this to about $30/month for embeddings, a significant saving.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>pgvector on Oracle Autonomous DB: 6 Months, 10k Vectors, and RAG Failure</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Wed, 12 Aug 2026 19:30:13 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-2571</link>
      <guid>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-2571</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-2026-08-12" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My RAG production system, built on Oracle Autonomous Database with &lt;code&gt;pgvector&lt;/code&gt;, failed at 10,000 vectors. Retrieval quality plummeted from 95% to below 60% for critical queries. This wasn't a theoretical scaling limit; it was a hard, operational wall I hit after six months of shipping production AI agents for clients. The initial setup, using &lt;code&gt;text-embedding-ada-002&lt;/code&gt; and a basic &lt;code&gt;IVFFlat&lt;/code&gt; index, worked perfectly for smaller datasets. The problem wasn't &lt;code&gt;pgvector&lt;/code&gt; itself, nor Oracle's managed PostgreSQL. The problem was my naive assumptions about index choice and embedding model stability under real-world data growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Setup: Ada-002 and IVFFlat
&lt;/h2&gt;

&lt;p&gt;When I started building multi-agent systems for clients, I needed a robust, cost-effective vector store. Oracle Autonomous Database, with its integrated PostgreSQL and &lt;code&gt;pgvector&lt;/code&gt; extension, was a natural fit given my existing Oracle Cloud infrastructure. I provisioned an &lt;code&gt;Always Free&lt;/code&gt; Autonomous Database (PostgreSQL flavor) for initial development, then scaled to a 2 OCPU, 16GB RAM instance for production.&lt;/p&gt;

&lt;p&gt;My first embedding model was OpenAI's &lt;code&gt;text-embedding-ada-002&lt;/code&gt;. It was the industry standard, easy to integrate, and performed well on my initial datasets, which rarely exceeded 2,000 documents. Each document averaged 500 tokens, resulting in roughly 2,000 vectors of 1536 dimensions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pgvector&lt;/code&gt; index choice was &lt;code&gt;IVFFlat&lt;/code&gt;. I configured it with &lt;code&gt;lists = 100&lt;/code&gt; for &lt;code&gt;1536&lt;/code&gt; dimensions. This seemed reasonable based on common recommendations for datasets under 100,000 vectors. Query latency for &lt;code&gt;ORDER BY embedding &amp;lt;-&amp;gt; ? LIMIT 5&lt;/code&gt; was consistently under 50ms, even with concurrent agent requests. Retrieval accuracy, measured by human evaluation of agent responses, was above 95% for the first few clients. This setup shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10,000 Vector Cliff
&lt;/h2&gt;

&lt;p&gt;The problems began when a new client's knowledge base pushed the vector count past 10,000. This client had extensive product documentation, legal agreements, and internal FAQs. The total vector count for their RAG system reached 10,234.&lt;/p&gt;

&lt;p&gt;Suddenly, agent responses started hallucinating or providing irrelevant information. My internal monitoring showed a sharp drop in retrieval quality. For queries that previously yielded perfect results, the top 5 retrieved documents now contained 2-3 irrelevant chunks. This wasn't a gradual degradation; it was a distinct drop-off.&lt;/p&gt;

&lt;p&gt;My first thought was data quality. I re-chunked, re-embedded, and re-indexed. No change.&lt;br&gt;
Next, I suspected the embedding model. I ran a small test with &lt;code&gt;text-embedding-3-small&lt;/code&gt; (512 dimensions) and &lt;code&gt;3-large&lt;/code&gt; (3072 dimensions). While &lt;code&gt;3-small&lt;/code&gt; was faster and cheaper, it didn't solve the retrieval quality issue at 10k vectors. &lt;code&gt;3-large&lt;/code&gt; was too expensive for my current cost structure.&lt;/p&gt;

&lt;p&gt;The issue was the &lt;code&gt;IVFFlat&lt;/code&gt; index. At 10,000 vectors, with &lt;code&gt;lists = 100&lt;/code&gt;, the average number of vectors per list was 100. This is too high for effective nearest neighbor search. &lt;code&gt;IVFFlat&lt;/code&gt; works by partitioning the vector space into &lt;code&gt;lists&lt;/code&gt; clusters. During a search, it only checks a subset of these lists (controlled by &lt;code&gt;probes&lt;/code&gt;). If the relevant vectors are spread across too many lists, or if a list becomes too dense, the search becomes inefficient and inaccurate. My &lt;code&gt;IVFFlat&lt;/code&gt; index was effectively performing a near-brute-force search within a large subset of the data, missing true nearest neighbors.&lt;/p&gt;
&lt;h2&gt;
  
  
  The HNSW Migration and Embedding Model Shift
&lt;/h2&gt;

&lt;p&gt;The solution was to switch to &lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small World) indexing. &lt;code&gt;HNSW&lt;/code&gt; is generally more robust for larger datasets and higher dimensions, offering a better recall-latency tradeoff.&lt;/p&gt;

&lt;p&gt;Migrating to &lt;code&gt;HNSW&lt;/code&gt; required dropping and recreating the index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;idx_document_embeddings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_document_embeddings&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_l2_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose &lt;code&gt;m = 16&lt;/code&gt; and &lt;code&gt;ef_construction = 100&lt;/code&gt; based on &lt;code&gt;pgvector&lt;/code&gt; recommendations for a balance of build time and search quality. &lt;code&gt;m&lt;/code&gt; controls the number of neighbors each node connects to, and &lt;code&gt;ef_construction&lt;/code&gt; controls the size of the dynamic list during graph construction. Higher values improve recall but increase index build time and memory usage.&lt;/p&gt;

&lt;p&gt;After rebuilding the index (which took about 15 minutes for 10k vectors on my 2 OCPU instance), retrieval quality immediately jumped back to over 90%. Query latency remained under 60ms. This confirmed the &lt;code&gt;IVFFlat&lt;/code&gt; bottleneck.&lt;/p&gt;

&lt;p&gt;At the same time, I started experimenting with open-source embedding models. OpenAI's &lt;code&gt;ada-002&lt;/code&gt; was costing me around $0.0001 per 1K tokens. For a client with 10,000 documents, each 500 tokens, that's 5M tokens. Re-embedding costs were becoming significant during development and data updates.&lt;/p&gt;

&lt;p&gt;I integrated &lt;code&gt;bge-small-en-v1.5&lt;/code&gt; (384 dimensions) and &lt;code&gt;nomic-embed-text-v1.5&lt;/code&gt; (768 dimensions) via self-hosted inference endpoints on Oracle Cloud. &lt;code&gt;nomic-embed-text-v1.5&lt;/code&gt; proved to be the sweet spot for my use cases. It offered comparable retrieval quality to &lt;code&gt;ada-002&lt;/code&gt; for my specific domains, but at a fraction of the cost (zero inference cost beyond GPU rental, which I amortize across multiple clients). The reduced dimensionality (768 vs 1536) also meant smaller index sizes and slightly faster queries.&lt;/p&gt;

&lt;p&gt;My current embedding pipeline now looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Document ingestion and chunking.&lt;/li&gt;
&lt;li&gt;Embed with &lt;code&gt;nomic-embed-text-v1.5&lt;/code&gt; (self-hosted on a single NVIDIA A10 GPU on OCI).&lt;/li&gt;
&lt;li&gt;Store in Oracle Autonomous DB with &lt;code&gt;pgvector&lt;/code&gt; and &lt;code&gt;HNSW&lt;/code&gt; index.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This setup has scaled to 50,000 vectors for a single client without any degradation in retrieval quality. Query latency remains under 100ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oracle Autonomous DB Performance and Cost
&lt;/h2&gt;

&lt;p&gt;My Oracle Autonomous Database instance (PostgreSQL, 2 OCPU, 16GB RAM) costs approximately $150/month. This includes the database, storage, and managed services. For this price, I get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Automatic patching and backups.&lt;/li&gt;
&lt;li&gt;  High availability.&lt;/li&gt;
&lt;li&gt;  Scalability on demand (though I haven't needed to scale beyond 2 OCPU yet).&lt;/li&gt;
&lt;li&gt;  Integrated &lt;code&gt;pgvector&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compared to self-hosting PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt; on a VM, the managed service saves significant operational overhead. The &lt;code&gt;HNSW&lt;/code&gt; index build for 50,000 vectors (768 dimensions) took about 45 minutes on this instance. Subsequent incremental updates are fast.&lt;/p&gt;

&lt;p&gt;The key takeaway for practitioners: don't assume your initial index choice will scale. Test it. Monitor retrieval quality, not just latency. And seriously evaluate open-source embedding models for cost efficiency once you have a baseline. The "best" model is the one that meets your quality bar at the lowest operational cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What &lt;code&gt;ef_search&lt;/code&gt; value do you use for &lt;code&gt;HNSW&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A: I typically set &lt;code&gt;ef_search = 40&lt;/code&gt; for my production queries. This value balances recall and latency effectively for my 768-dimensional vectors and dataset sizes up to 50,000. Higher values increase recall but also search time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you monitor retrieval quality in production?&lt;/strong&gt;&lt;br&gt;
A: I use a combination of automated checks and human feedback. Automated checks involve running a fixed set of "golden queries" against the RAG system and asserting that specific, known relevant document IDs are returned in the top-k. Human feedback comes from agent users flagging irrelevant responses, which triggers a review of the retrieved documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why Oracle Autonomous DB over other managed PostgreSQL services or dedicated vector databases?&lt;/strong&gt;&lt;br&gt;
A: My primary reason is existing infrastructure and cost optimization within Oracle Cloud. I already run other services and custom inference endpoints on OCI. Autonomous DB offers a robust, managed PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt; at a predictable cost, avoiding vendor lock-in to specialized vector databases while leveraging my existing cloud credits and expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Did you consider &lt;code&gt;diskann&lt;/code&gt; or other &lt;code&gt;pgvector&lt;/code&gt; index types?&lt;/strong&gt;&lt;br&gt;
A: I evaluated &lt;code&gt;diskann&lt;/code&gt; briefly but found &lt;code&gt;HNSW&lt;/code&gt; to be sufficient for my current scale and performance requirements. &lt;code&gt;diskann&lt;/code&gt; is designed for even larger datasets that exceed memory capacity, which isn't a constraint for me yet. Sticking with &lt;code&gt;HNSW&lt;/code&gt; simplified the operational aspects.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>pgvector on Oracle Autonomous DB: 6 Months, 10k Vectors, and RAG Failure</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Tue, 11 Aug 2026 19:30:16 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-53f0</link>
      <guid>https://dev.to/elenarevicheva/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure-53f0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/pgvector-on-oracle-autonomous-db-6-months-10k-vectors-and-rag-failure" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My RAG production system, built on Oracle Autonomous Database with &lt;code&gt;pgvector&lt;/code&gt;, failed to scale past 10,000 vectors. Retrieval quality plummeted from 92% precision at 5,000 vectors to 68% at 10,000. This wasn't a theoretical benchmark; this was a live multi-agent system, routing customer queries for a shipping logistics client, where a 24% drop in precision meant 24% more manual interventions. The initial promise of &lt;code&gt;pgvector&lt;/code&gt; on a managed Oracle service for cost-effective RAG quickly hit a wall, forcing a re-evaluation of embedding models, index choices, and ultimately, my infrastructure strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Setup: &lt;code&gt;text-embedding-ada-002&lt;/code&gt; and IVFFlat
&lt;/h2&gt;

&lt;p&gt;We started with &lt;code&gt;text-embedding-ada-002&lt;/code&gt; (1536 dimensions) because it was the default and "good enough" for initial testing. My Oracle Autonomous Database (ADB) instance, a shared Exadata infrastructure, offered &lt;code&gt;pgvector&lt;/code&gt; out of the box. The setup was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_l2_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose &lt;code&gt;IVFFlat&lt;/code&gt; with &lt;code&gt;lists = 100&lt;/code&gt; based on general recommendations for datasets under 100,000 vectors. My initial dataset was small, around 2,000 internal policy documents, averaging 500 tokens each. Ingesting these, generating embeddings via OpenAI's API, and storing them was simple. Retrieval latency was consistently under 50ms for &lt;code&gt;k=5&lt;/code&gt; nearest neighbors. Our agent, running on Oracle Cloud Infrastructure (OCI) Container Instances, used &lt;code&gt;langchain_pgvector&lt;/code&gt; for retrieval, feeding context to a Groq Llama 3 8B agent for initial processing, then escalating to Claude 3 Haiku for complex cases. Precision was high, around 92%, measured by human evaluation of retrieved chunks against ground truth answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10,000 Vector Cliff: Latency and Precision Degradation
&lt;/h2&gt;

&lt;p&gt;As the client expanded, so did the knowledge base. We added more internal FAQs, shipping manifests, and customer service logs. At approximately 5,000 vectors, retrieval latency started to creep up, hitting 80ms. At 10,000 vectors, it spiked to 250ms, and precision dropped to 68%. This was unacceptable. The agents were hallucinating more often, or simply stating they couldn't find relevant information, leading to increased human intervention.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;pgvector&lt;/code&gt; index was the bottleneck. The &lt;code&gt;IVFFlat&lt;/code&gt; index, with &lt;code&gt;lists = 100&lt;/code&gt;, was struggling. The &lt;code&gt;search_list&lt;/code&gt; parameter, which defaults to &lt;code&gt;lists&lt;/code&gt; (100 in my case), meant that for each query, it was scanning 100 lists. With 10,000 vectors, each list contained 100 vectors on average. This was still a lot of distance calculations.&lt;/p&gt;

&lt;p&gt;I tried increasing &lt;code&gt;lists&lt;/code&gt; to 200, then 500.&lt;br&gt;
&lt;code&gt;ALTER INDEX documents_embedding_idx SET (lists = 200);&lt;/code&gt;&lt;br&gt;
&lt;code&gt;REINDEX INDEX documents_embedding_idx;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This improved latency slightly (down to 180ms at 10k vectors) but didn't recover precision. In fact, increasing &lt;code&gt;lists&lt;/code&gt; too much can degrade precision if the query vector falls into a sparsely populated list. The trade-off was clear: &lt;code&gt;IVFFlat&lt;/code&gt; was not robust enough for even this modest scale on my current setup.&lt;/p&gt;
&lt;h2&gt;
  
  
  Embedding Model Trade-offs: &lt;code&gt;bge-small-en-v1.5&lt;/code&gt; and &lt;code&gt;e5-large-v2&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;text-embedding-ada-002&lt;/code&gt; model was costing me $0.0001 per 1K tokens. For 10,000 documents averaging 500 tokens, that's 5 million tokens, or $500 for initial ingestion. Not a huge cost, but every dollar counts when you're bootstrapping with zero VC. More importantly, its performance was now suspect.&lt;/p&gt;

&lt;p&gt;I experimented with open-source models: &lt;code&gt;bge-small-en-v1.5&lt;/code&gt; (384 dimensions) and &lt;code&gt;e5-large-v2&lt;/code&gt; (1024 dimensions). I ran these locally on an OCI VM with a single NVIDIA A10 GPU for batch embedding generation, then uploaded to ADB.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;bge-small-en-v1.5&lt;/code&gt; (384D):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Pros:&lt;/strong&gt; Much faster embedding generation (local inference), significantly smaller vector size (384 dimensions vs 1536), reducing storage and potentially improving &lt;code&gt;pgvector&lt;/code&gt; performance due to fewer floating-point operations per distance calculation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Cons:&lt;/strong&gt; Precision dropped to 60% at 10k vectors. The smaller dimensionality simply wasn't capturing enough nuance for my domain-specific documents.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;e5-large-v2&lt;/code&gt; (1024D):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Pros:&lt;/strong&gt; Better precision than &lt;code&gt;bge-small-en-v1.5&lt;/code&gt;, reaching 75% at 10k vectors. Still better than &lt;code&gt;ada-002&lt;/code&gt;'s 68% at that scale. Local inference was manageable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Cons:&lt;/strong&gt; Larger vector size than &lt;code&gt;bge-small&lt;/code&gt;, but still smaller than &lt;code&gt;ada-002&lt;/code&gt;. Latency was still an issue, hovering around 150ms.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;e5-large-v2&lt;/code&gt; offered a better balance, but the &lt;code&gt;pgvector&lt;/code&gt; performance was still the primary bottleneck. The problem wasn't just the embedding model; it was the retrieval infrastructure.&lt;/p&gt;
&lt;h2&gt;
  
  
  HNSW: A Necessary Migration
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IVFFlat&lt;/code&gt; is a good starting point, but for anything beyond trivial scale, &lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small World) is generally superior for recall and speed. The challenge with &lt;code&gt;HNSW&lt;/code&gt; on &lt;code&gt;pgvector&lt;/code&gt; is its memory footprint. It's an in-memory index, meaning it consumes RAM directly proportional to the number of vectors and their dimensions. On a shared Oracle Autonomous Database, I have limited control over memory allocation for my specific &lt;code&gt;pgvector&lt;/code&gt; index.&lt;/p&gt;

&lt;p&gt;I dropped the &lt;code&gt;IVFFlat&lt;/code&gt; index and created an &lt;code&gt;HNSW&lt;/code&gt; index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;documents_embedding_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_l2_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;m = 16&lt;/code&gt;: The number of bi-directional links created for each new element during index construction. Higher &lt;code&gt;m&lt;/code&gt; means more connections, better recall, but slower construction and larger index size.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ef_construction = 100&lt;/code&gt;: The size of the dynamic list for nearest neighbors during index construction. Higher &lt;code&gt;ef_construction&lt;/code&gt; means better quality index, but slower construction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After rebuilding the index with &lt;code&gt;e5-large-v2&lt;/code&gt; embeddings, the results were immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; Dropped to 60ms at 10k vectors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Precision:&lt;/strong&gt; Recovered to 88%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a significant improvement, bringing us back to acceptable performance levels. However, the &lt;code&gt;HNSW&lt;/code&gt; index size for 10,000 &lt;code&gt;e5-large-v2&lt;/code&gt; vectors (1024 dimensions) was approximately 1.5GB. This is a concern for scaling on a shared ADB instance, where memory resources are not dedicated. I anticipate hitting memory limits or performance degradation as I approach 50,000 vectors, potentially leading to swapping or eviction of the index from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Oracle Autonomous DB Constraint
&lt;/h2&gt;

&lt;p&gt;Oracle Autonomous Database is fantastic for managed relational workloads. Its auto-scaling, patching, and backup features are invaluable. However, for specialized workloads like &lt;code&gt;pgvector&lt;/code&gt; with &lt;code&gt;HNSW&lt;/code&gt;, the "autonomous" nature becomes a constraint. I cannot directly control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dedicated Memory:&lt;/strong&gt; &lt;code&gt;HNSW&lt;/code&gt; thrives on dedicated RAM. On ADB, I'm sharing resources. If other tenants on the same Exadata infrastructure are hammering their databases, my &lt;code&gt;pgvector&lt;/code&gt; index might get less memory, leading to performance drops.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;CPU Cores for Vector Operations:&lt;/strong&gt; While &lt;code&gt;pgvector&lt;/code&gt; can utilize multiple cores for distance calculations, I don't have direct control over how many cores are allocated to my specific &lt;code&gt;pgvector&lt;/code&gt; queries on a shared system.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Storage Type:&lt;/strong&gt; While Exadata is fast, I can't specify NVMe SSDs for my &lt;code&gt;pgvector&lt;/code&gt; index specifically, which could further reduce latency for index lookups.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These limitations mean that while &lt;code&gt;pgvector&lt;/code&gt; on Oracle ADB is convenient for small-scale RAG, it's not a long-term solution for high-performance, high-scale vector search where precise resource control is critical. My current plan is to monitor performance closely as we approach 20,000 vectors. If performance degrades again, the next step will be migrating the vector store to a dedicated OCI VM running PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt; or even a specialized vector database like Qdrant or Weaviate, giving me full control over hardware resources. This would introduce additional operational overhead, but it's a necessary trade-off for production stability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not use Oracle's own vector capabilities instead of &lt;code&gt;pgvector&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A: Oracle Database 23ai offers native vector capabilities, but it's not yet generally available on Autonomous Database. My production system needed a solution six months ago, and &lt;code&gt;pgvector&lt;/code&gt; was the only viable option on ADB at the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What was the exact cost of the Oracle Autonomous Database for this workload?&lt;/strong&gt;&lt;br&gt;
A: My ADB instance was an "Always Free" tier initially, then scaled to 2 OCPU and 1TB storage for $0.35/OCPU-hour. For 10,000 vectors, the database cost was approximately $50/month, primarily for compute, not storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Did you consider using a cloud-managed vector database service?&lt;/strong&gt;&lt;br&gt;
A: Yes, but with zero VC funding, every dollar counts. Services like Pinecone or Zilliz were significantly more expensive for my initial scale (e.g., $70-$100/month for 10k vectors, 1536D) compared to the &lt;code&gt;pgvector&lt;/code&gt; on ADB approach. The goal was to leverage existing infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How did you measure precision for your RAG system?&lt;/strong&gt;&lt;br&gt;
A: We used a combination of human evaluation and a small, manually curated test set of 100 queries. For each query, human annotators rated the relevance of the top 5 retrieved chunks on a 3-point scale (relevant, partially relevant, irrelevant). Precision was calculated as (number of relevant chunks) / (total chunks retrieved).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's your plan if HNSW on ADB hits a wall at 50k vectors?&lt;/strong&gt;&lt;br&gt;
A: The plan is to migrate the vector store to a dedicated OCI VM running PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt; or a specialized vector database like Qdrant. This allows for dedicated memory and CPU allocation, giving full control over &lt;code&gt;HNSW&lt;/code&gt; performance parameters and scaling.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>LangGraph Checkpointing: Three Rewrites to Production Stability</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Mon, 10 Aug 2026 19:30:19 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/langgraph-checkpointing-three-rewrites-to-production-stability-241e</link>
      <guid>https://dev.to/elenarevicheva/langgraph-checkpointing-three-rewrites-to-production-stability-241e</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/langgraph-checkpointing-three-rewrites-to-production-stability-2026-08-10" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first LangGraph agent, a simple document summarizer, silently dropped 80% of its jobs for three weeks. The logs showed &lt;code&gt;Agent finished successfully&lt;/code&gt;, but the output queue was empty. The problem wasn't the LLM, the prompt, or the vector store. It was a state schema mismatch, a silent killer in LangGraph's checkpointing mechanism, costing me 1,200 CPU hours on Oracle Cloud before I found it.&lt;/p&gt;

&lt;p&gt;This wasn't my only LangGraph production headache. Before I shipped my first multi-agent system for AIdeazz, I went through three complete rewrites of the core LangGraph state management and checkpointing logic. Each rewrite addressed a different failure mode: silent data loss, corrupted checkpoints, and finally, the pattern that brought stability to multi-step, stateful agent pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent State Schema Mismatch
&lt;/h2&gt;

&lt;p&gt;My initial LangGraph agent processed incoming documents, summarized them, and then routed the summary to a specific output channel (Telegram, WhatsApp, email). The state was a simple &lt;code&gt;TypedDict&lt;/code&gt;:&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;document_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;raw_text&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;summary&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="n"&gt;output_channel&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;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&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;summarized&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;failed&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;The agent worked locally. When deployed to Oracle Cloud, processing 100 documents per hour, it appeared to work. The &lt;code&gt;status&lt;/code&gt; field would update to &lt;code&gt;summarized&lt;/code&gt; in the database, and the agent logs confirmed completion. But the &lt;code&gt;summary&lt;/code&gt; field in the database was always &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I spent days debugging the summarization step itself, convinced the LLM was hallucinating or the prompt was malformed. I added more logging, printed intermediate steps, and even ran the summarization logic outside LangGraph. It always produced a summary.&lt;/p&gt;

&lt;p&gt;The issue was in the &lt;code&gt;summary: Optional[str]&lt;/code&gt; field. My initial state definition had &lt;code&gt;summary: str&lt;/code&gt;. Later, I updated it to &lt;code&gt;Optional[str]&lt;/code&gt; to handle cases where summarization might fail or not be needed immediately. LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; (and by extension, other &lt;code&gt;BaseCheckpointSaver&lt;/code&gt; implementations) deserializes the stored state into the &lt;em&gt;current&lt;/em&gt; state schema. If a field was present in the stored state but removed or changed type in the new schema, it would be silently dropped during deserialization. Conversely, if a new field was added, it would be &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My database still held the old schema's state. When the agent loaded a checkpoint, the &lt;code&gt;summary&lt;/code&gt; field, which was &lt;code&gt;str&lt;/code&gt; in the stored state, was deserialized into the &lt;em&gt;new&lt;/em&gt; &lt;code&gt;Optional[str]&lt;/code&gt; schema. LangGraph's internal &lt;code&gt;_load_state&lt;/code&gt; method, when encountering a type mismatch or a missing field in the &lt;em&gt;new&lt;/em&gt; schema, would simply discard the value from the &lt;em&gt;old&lt;/em&gt; checkpoint. No error, no warning. The &lt;code&gt;summary&lt;/code&gt; was there in the database, but it never made it into the agent's runtime state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Explicit schema versioning and migration. I now embed a &lt;code&gt;schema_version: int&lt;/code&gt; in every &lt;code&gt;AgentState&lt;/code&gt; and implement a &lt;code&gt;migrate_state(state: AgentState, target_version: int) -&amp;gt; AgentState&lt;/code&gt; function. Before loading a checkpoint, I check its version and apply necessary migrations. This adds boilerplate but prevents silent data loss.&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;AgentStateV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;document_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;raw_text&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;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;# Old schema
&lt;/span&gt;    &lt;span class="n"&gt;schema_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentStateV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;document_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;raw_text&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;summary&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;# New schema
&lt;/span&gt;    &lt;span class="n"&gt;output_channel&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;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&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;summarized&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;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;schema_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;migrate_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&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;target_version&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;current_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&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;schema_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Assume V1 if not present
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target_version&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;state&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;target_version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Example migration: add new fields with defaults
&lt;/span&gt;        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_channel&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema_version&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="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported migration from V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target_version&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="c1"&gt;# Before loading:
# loaded_state = checkpoint_saver.get_tuple(thread_id).checkpoint["v"]
# current_state = migrate_state(loaded_state, TARGET_SCHEMA_VERSION)
# graph.invoke(current_state, config={"configurable": {"thread_id": thread_id}})
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Corrupted Checkpoints and Race Conditions
&lt;/h2&gt;

&lt;p&gt;My second rewrite came after a week of &lt;code&gt;sqlite3.DatabaseError: database disk image is malformed&lt;/code&gt; errors. This happened when running multiple instances of the same LangGraph agent, each with its own &lt;code&gt;SqliteSaver&lt;/code&gt;, against a shared SQLite file on a network file system.&lt;/p&gt;

&lt;p&gt;The problem was a classic race condition. SQLite is robust for single-writer, multiple-reader scenarios. LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; performs multiple operations: &lt;code&gt;get_tuple&lt;/code&gt;, deserialize, modify state, serialize, &lt;code&gt;put_tuple&lt;/code&gt;. If two agents tried to update the same thread ID's checkpoint concurrently, one would overwrite the other's changes, or worse, write a partially updated or corrupted blob. The &lt;code&gt;SqliteSaver&lt;/code&gt; doesn't implement file-level locking or transaction management for concurrent writes from separate processes.&lt;/p&gt;

&lt;p&gt;My agents were deployed as Docker containers on Oracle Container Engine for Kubernetes (OKE). Each pod had its own &lt;code&gt;SqliteSaver&lt;/code&gt; instance, and I was mounting a shared NFS volume for the SQLite database. This setup is fundamentally flawed for &lt;code&gt;SqliteSaver&lt;/code&gt; with concurrent writers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Centralized, atomic checkpoint storage. I switched from &lt;code&gt;SqliteSaver&lt;/code&gt; to a custom &lt;code&gt;BaseCheckpointSaver&lt;/code&gt; implementation backed by Oracle Autonomous Database (ADB) and Redis.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Redis for ephemeral state and locking:&lt;/strong&gt; Before an agent starts processing a thread, it acquires a lock for that &lt;code&gt;thread_id&lt;/code&gt; in Redis with an expiry. If it can't acquire the lock, it retries or queues the job.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;ADB for persistent checkpoints:&lt;/strong&gt; The actual checkpoint data (the serialized LangGraph state) is stored in a JSON column in an ADB table. Updates are performed within a database transaction, ensuring atomicity. The &lt;code&gt;put_tuple&lt;/code&gt; method now performs an &lt;code&gt;UPSERT&lt;/code&gt; operation, updating the JSON column.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern ensures that only one agent can modify a given thread's state at any time, and the updates are atomic and durable. The cost of ADB is higher than SQLite, but the stability is non-negotiable for 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;# Simplified custom saver logic
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ADBCheckpointSaver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseCheckpointSaver&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;db_connection_pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis_client&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;db_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_connection_pool&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;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tuple&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;thread_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="o"&gt;-&amp;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;CheckpointTuple&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="c1"&gt;# Acquire Redis lock
&lt;/span&gt;        &lt;span class="n"&gt;lock_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;langgraph_lock:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;redis&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;lock_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# 60s expiry, only if not exists
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LockAcquisitionError&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;Could not acquire lock for thread &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&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;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;cursor&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT checkpoint_data FROM checkpoints WHERE thread_id = :1&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;thread_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;checkpoint_data&lt;/span&gt; &lt;span class="o"&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                        &lt;span class="c1"&gt;# Deserialize into CheckpointTuple
&lt;/span&gt;                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CheckpointTuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;config&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;configurable&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;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;thread_id&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;checkpoint_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;parent_config&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="c1"&gt;# Or retrieve if stored
&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;None&lt;/span&gt;
        &lt;span class="k"&gt;finally&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Release lock
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put_tuple&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_tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CheckpointTuple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;thread_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkpoint_tuple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&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;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;lock_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;langgraph_lock:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;redis&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;lock_key&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;LockAcquisitionError&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;Lock for thread &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not held during put_tuple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&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;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;checkpoint_json&lt;/span&gt; &lt;span class="o"&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;checkpoint_tuple&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;cursor&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    MERGE INTO checkpoints c
                    USING (SELECT :1 AS thread_id, :2 AS checkpoint_data FROM DUAL) d
                    ON (c.thread_id = d.thread_id)
                    WHEN MATCHED THEN UPDATE SET c.checkpoint_data = d.checkpoint_data
                    WHEN NOT MATCHED THEN INSERT (thread_id, checkpoint_data) VALUES (d.thread_id, d.checkpoint_data)
                    &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;thread_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint_json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Always Restart" Pattern for Multi-Step Pipelines
&lt;/h2&gt;

&lt;p&gt;Even with schema versioning and atomic checkpointing, my multi-agent systems, especially those involving external API calls or human-in-the-loop steps, were brittle. An agent might make an API call, receive a 200 OK, but then fail to parse the response due to a network glitch or an unexpected payload. The state would be saved, but the agent was stuck. Retrying the &lt;em&gt;same&lt;/em&gt; step often led to duplicate actions (e.g., sending the same email twice).&lt;/p&gt;

&lt;p&gt;My agents often involve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Ingestion &amp;amp; Pre-processing (Groq for quick classification)&lt;/li&gt;
&lt;li&gt; Complex Reasoning (Claude 3.5 Sonnet for multi-step planning)&lt;/li&gt;
&lt;li&gt; External API Calls (CRM, payment gateways)&lt;/li&gt;
&lt;li&gt; Human Review (via Telegram/WhatsApp)&lt;/li&gt;
&lt;li&gt; Final Output Generation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A failure at step 3 meant the agent was stuck. LangGraph's default behavior is to resume from the last saved state. If that state was "just before the failed API call," it would retry the API call. If the API call was idempotent, fine. If not, it was a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; The "Always Restart" pattern. Instead of letting LangGraph resume from the exact point of failure, I designed my agent nodes to be idempotent and to always re-evaluate their current state from the beginning of the &lt;em&gt;current logical step&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Each logical step (e.g., "Summarize Document", "Make API Call", "Await Human Approval") is a LangGraph node. Inside each node, before performing any action, the agent first checks if the action has &lt;em&gt;already been completed&lt;/em&gt; based on the current state.&lt;/p&gt;

&lt;p&gt;For example, in an "Execute API Call" node:&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;execute_api_call_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;AgentState&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_call_status&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="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API call already completed, skipping.&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="n"&gt;state&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Perform API call
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_external_api_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_response&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_call_status&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="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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_call_status&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="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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_message&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="nf"&gt;str&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern means that if an agent fails mid-node, and then is restarted, it will re-enter the node, see &lt;code&gt;api_call_status&lt;/code&gt; is not "completed", attempt the API call, and then update the status. If it fails again, the status remains "failed". If it succeeds, the status becomes "completed". The next time the graph runs, it will see "completed" and skip the API call.&lt;/p&gt;

&lt;p&gt;This makes each node effectively idempotent from the perspective of the graph. The graph can be restarted from any point, and it will gracefully pick up where it left off without duplicating work or getting stuck in a retry loop on a non-idempotent action. This also simplifies error handling: instead of complex retry logic within LangGraph, I rely on an external orchestrator (a simple Python script running on a cron job) to periodically re-invoke agents that are in a "failed" or "pending" state.&lt;/p&gt;

&lt;p&gt;This "Always Restart" pattern, combined with robust checkpointing and explicit schema management, finally brought the stability needed for production multi-agent systems on Oracle Cloud. My current agents handle thousands of messages daily, routing between Groq for fast initial processing, Claude 3.5 Sonnet for complex reasoning, and custom tools for external interactions, all while maintaining state across potentially long-running processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle schema changes for in-flight agents with the versioning approach?&lt;/strong&gt;&lt;br&gt;
A: When a new schema version is deployed, agents processing older checkpoints will first load the old state, then &lt;code&gt;migrate_state&lt;/code&gt; will transform it to the new schema. This transformed state is then saved back to the checkpoint store, effectively upgrading the checkpoint. Agents starting new threads will use the latest schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the overhead of using Redis for locking and ADB for checkpoints compared to a simpler solution?&lt;/strong&gt;&lt;br&gt;
A: Redis adds ~2-5ms latency for lock acquisition/release. ADB adds ~10-50ms for checkpoint &lt;code&gt;UPSERT&lt;/code&gt; operations, depending on network latency and payload size. This is acceptable for most multi-agent systems where LLM calls dominate latency (hundreds of ms to seconds). The stability gain far outweighs this overhead for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you manage the &lt;code&gt;LockAcquisitionError&lt;/code&gt; in your &lt;code&gt;ADBCheckpointSaver&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A: When &lt;code&gt;LockAcquisitionError&lt;/code&gt; is raised, the agent's current invocation is aborted. The external orchestrator (e.g., a message queue consumer or a cron job) responsible for invoking agents will catch this error and typically re-queue the message or mark the thread for a later retry. This ensures that only one agent attempts to process a specific thread at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does the "Always Restart" pattern mean you re-run LLM calls if a node fails after the LLM call but before saving state?&lt;/strong&gt;&lt;br&gt;
A: Yes, if an LLM call completes but the subsequent state update or external action fails &lt;em&gt;before&lt;/em&gt; the LangGraph node returns and its state is checkpointed, the LLM call might be re-run on restart. To prevent this for expensive LLM calls, I often add a &lt;code&gt;llm_response_cached: bool&lt;/code&gt; flag to the state and save the raw LLM response. The node then checks this flag and uses the cached response if available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why Oracle Autonomous Database (ADB) specifically?&lt;/strong&gt;&lt;br&gt;
A: ADB offers fully managed, auto-scaling, and highly available PostgreSQL-compatible or Oracle Database instances. For AIdeazz, it integrates seamlessly with other Oracle Cloud Infrastructure (OCI) services I use (OKE, OCI Functions, OCI AI Services) and provides strong performance guarantees without requiring dedicated DBA resources, which is critical for a lean operation.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>AI Startup Infrastructure: $0 Oracle, $12 Groq, $40 BrightData — Real Costs</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Sun, 09 Aug 2026 19:30:15 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/ai-startup-infrastructure-0-oracle-12-groq-40-brightdata-real-costs-4bn2</link>
      <guid>https://dev.to/elenarevicheva/ai-startup-infrastructure-0-oracle-12-groq-40-brightdata-real-costs-4bn2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/ai-startup-infrastructure-0-oracle-12-groq-40-brightdata-real-costs" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My Oracle Cloud bill for the last 12 months has been $0.00. This isn't a marketing stunt, it's a hard number. We run 10 production AI agents, processing hundreds of thousands of requests monthly. Our Groq bill for the same period was $12.48. Claude API? $8.17. Resend for email notifications? $4.00. These are the visible costs. The invisible ones, the "free tier" traps, and the operational overhead are where most AI startups bleed cash without realizing it.&lt;/p&gt;

&lt;p&gt;I built AIdeazz with zero VC funding, as a single mother who relocated from Russia to Panama. Every dollar spent is a dollar earned from a client. This forces a brutal efficiency that most well-funded startups never experience. This isn't about finding the cheapest option; it's about understanding the &lt;em&gt;actual&lt;/em&gt; cost of running production AI agents and where the "free" options become prohibitively expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Oracle Always Delivers (for Free)
&lt;/h2&gt;

&lt;p&gt;Oracle Cloud Infrastructure (OCI) is our backbone. Specifically, the Always Free tier. This isn't a trial; it's a permanent allocation of resources. We utilize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;2 AMD E4 Flex VMs:&lt;/strong&gt; Each with 4 OCPUs and 24 GB RAM. These are our workhorses, running Docker containers for our agents, custom APIs, and data processing.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;2 ARM-based Ampere A1 Compute VMs:&lt;/strong&gt; Each with 4 OCPUs and 24 GB RAM. These handle lighter loads, monitoring, and redundant services.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;4 Block Volumes:&lt;/strong&gt; 200 GB total. Enough for OS, Docker images, and agent data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;10 TB Outbound Data Transfer:&lt;/strong&gt; This is critical. Most cloud providers nickel-and-dime you on egress. 10 TB is generous and we've never hit it.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Load Balancer:&lt;/strong&gt; 10 Mbps bandwidth. Sufficient for our API traffic.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Autonomous Database:&lt;/strong&gt; 2 OCPUs, 20 GB storage. We use this for structured data, user management, and agent state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch? Resource limits. You can't just scale up indefinitely. If an agent needs more than 4 OCPUs or 24 GB RAM, you're out of luck on the free tier. This forces architectural discipline: microservices, efficient code, and offloading heavy compute to specialized APIs. Our agents are designed to be lean. We use PostgreSQL for most agent-specific data, running within Docker on the VMs, not the Autonomous Database, to keep that resource free for core application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM Routing: Groq for Speed, Claude for Complexity
&lt;/h2&gt;

&lt;p&gt;Our LLM strategy is a hybrid. For agents requiring rapid, short-form responses, especially those interacting with users via Telegram or WhatsApp, Groq is indispensable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Groq Llama 3 8B:&lt;/strong&gt; Average cost per 1M tokens is $0.05. Our monthly usage is typically around 250,000 tokens for these agents. This translates to about $0.0125/month.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Groq Llama 3 70B:&lt;/strong&gt; Average cost per 1M tokens is $0.59. Used for more complex, multi-turn conversations. Our usage is lower, around 10,000 tokens/month, costing $0.0059.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total Groq bill: $0.0184/month, rounded up to $0.02. The $12.48 annual bill is for &lt;em&gt;all&lt;/em&gt; agents, including development and testing. The real cost is negligible. The benefit is speed: sub-100ms response times for Llama 3 8B, which is crucial for real-time user interaction.&lt;/p&gt;

&lt;p&gt;For tasks requiring more nuanced reasoning, longer context windows, or specific instruction following, we route to Claude.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Anthropic Claude 3 Haiku:&lt;/strong&gt; Input $0.25/M tokens, Output $1.25/M tokens. Used for summarization, content generation, and complex decision-making. Our usage is around 5,000 input tokens and 1,000 output tokens per month for specific agents. This is $0.00125 + $0.00125 = $0.0025/month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total Claude bill: $0.0025/month. Again, the $8.17 annual bill includes development. The key is intelligent routing: don't send a simple query to an expensive model. Our internal API gateway dynamically selects the LLM based on agent configuration and prompt complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Acquisition: The BrightData Tax
&lt;/h2&gt;

&lt;p&gt;This is where "free" ends and real costs begin. Many AI applications rely on external data. For us, this means web scraping. We use BrightData for proxy management and CAPTCHA solving.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;BrightData Residential Proxies:&lt;/strong&gt; $15/GB.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;BrightData Web Unlocker (CAPTCHA solving):&lt;/strong&gt; $3/1000 requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical data acquisition run for a new client or a significant data refresh costs us around $40. This is not a monthly recurring cost, but an &lt;em&gt;event-driven&lt;/em&gt; cost. If a client needs daily data updates, this cost becomes recurring. If they need weekly, it's $160/month. This is a direct pass-through cost to the client.&lt;/p&gt;

&lt;p&gt;The hidden cost here is &lt;em&gt;not&lt;/em&gt; the BrightData bill itself, but the engineering time to build robust scrapers that handle rate limits, schema changes, and anti-bot measures. We've invested heavily in a modular scraping framework that minimizes this, but it's never zero. Relying on "free" scraping tools or public proxies for production data is a recipe for disaster: unreliable data, IP bans, and wasted engineering cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication &amp;amp; Monitoring: The Small, Necessary Evils
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Resend (Transactional Email):&lt;/strong&gt; $0.25/1000 emails. We send automated reports, alerts, and user notifications. Our usage is low, around 100 emails/month, costing $0.025. The annual bill of $4.00 covers this and development emails. It's a reliable service, and the cost is negligible.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Telegram Bot API:&lt;/strong&gt; Free. This is our primary interface for many agents. The infrastructure to run the bots is on our Oracle VMs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WhatsApp Business API:&lt;/strong&gt; This is where it gets tricky. While the API itself is free, Meta charges per conversation. The first 1,000 conversations per month are free. After that, it's $0.005-$0.015 per conversation depending on country and type (user-initiated vs. business-initiated). For low-volume agents, this is free. For high-volume agents, this becomes a significant cost that must be factored into the client's pricing. We explicitly track and bill for WhatsApp conversations above the free tier.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Monitoring:&lt;/strong&gt; Prometheus and Grafana running on our Oracle VMs. Free. This requires setup and maintenance time, but no direct infrastructure cost. We monitor VM health, Docker container status, API response times, and LLM token usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Hidden Costs: Time and Expertise
&lt;/h2&gt;

&lt;p&gt;The biggest "cost" in our $0 Oracle bill is my time. Setting up OCI Always Free, configuring VMs, Docker, Kubernetes (we use K3s for orchestration on the VMs), databases, networking, and security takes significant expertise. This isn't a point-and-click solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;DevOps Time:&lt;/strong&gt; Initial setup was weeks of focused effort. Ongoing maintenance, updates, and troubleshooting are a few hours per week.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Security:&lt;/strong&gt; Hardening VMs, managing firewalls, access keys, and patching. This is non-negotiable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Data Management:&lt;/strong&gt; Backups, replication, data integrity checks.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Agent Development:&lt;/strong&gt; The actual coding, prompt engineering, fine-tuning, and integration. This is the core value, but it's built on the foundation of robust infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many startups outsource this or hire dedicated DevOps engineers, which easily runs into $10,000+/month. My "free" infrastructure is only free because I &lt;em&gt;am&lt;/em&gt; the DevOps team. This is the trade-off: capital expenditure vs. human capital. For a bootstrapped operation, leveraging internal expertise is the only path.&lt;/p&gt;

&lt;p&gt;The "free tier" is a powerful tool, but it's a double-edged sword. It forces you to be lean and efficient, but it also demands a deep understanding of infrastructure and a willingness to get your hands dirty. The moment you need to scale beyond its limits, or if your team lacks the expertise, those "free" costs quickly become the most expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle high availability and disaster recovery with Always Free VMs?&lt;/strong&gt;&lt;br&gt;
A: We deploy critical services across multiple Always Free VMs in different availability domains within the same region. For disaster recovery, we have automated snapshot backups of our block volumes and database, stored in object storage. This provides redundancy within the region, but not cross-region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens if Oracle decides to terminate the Always Free tier or change its terms?&lt;/strong&gt;&lt;br&gt;
A: This is a risk. Our strategy is to maintain portability. All agents run in Docker containers, and our data is stored in standard PostgreSQL or object storage. We could migrate to another cloud provider (e.g., AWS EC2/RDS free tier, GCP free tier, or even self-hosted dedicated servers) with minimal code changes, though the migration effort itself would be significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you manage secrets and API keys across multiple agents and VMs?&lt;/strong&gt;&lt;br&gt;
A: We use a combination of environment variables for non-sensitive configuration and Oracle Cloud Infrastructure Vault for sensitive API keys and credentials. Secrets are injected into Docker containers at runtime, following least privilege principles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's your strategy for scaling beyond the Always Free limits if an agent becomes very popular?&lt;/strong&gt;&lt;br&gt;
A: If an agent requires more compute or memory than the Always Free VMs provide, we would first optimize the agent code and architecture. If that's insufficient, we would transition to paid OCI compute instances, which are still very competitive on price, or consider dedicated servers for extreme cases. The cost would then be passed to the client generating the demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do you use any serverless functions (e.g., AWS Lambda, OCI Functions) to offload compute?&lt;/strong&gt;&lt;br&gt;
A: Not extensively for our core agent logic. While OCI Functions has a free tier, the cold start times and vendor lock-in for complex stateful agents make it less appealing for our primary use cases. We prefer the consistent performance and control of long-running Docker containers on VMs. We do use OCI Functions for specific event-driven tasks like processing object storage events.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>131 Tests, 4 Layers, $00.03/Run: Why I Built My AI Agent Eval Harness First</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Sat, 08 Aug 2026 19:30:20 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/131-tests-4-layers-0003run-why-i-built-my-ai-agent-eval-harness-first-15o7</link>
      <guid>https://dev.to/elenarevicheva/131-tests-4-layers-0003run-why-i-built-my-ai-agent-eval-harness-first-15o7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/131-tests-4-layers-0003run-why-i-built-my-ai-agent-eval-harness-first-2026-08-08" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I shipped a silent failure. My first production AI agent, a simple lead qualification bot for WhatsApp, passed all 27 unit tests. It handled happy paths, edge cases, even adversarial inputs designed to break prompt safety. The agent went live. Two days later, a client forwarded a screenshot: the bot had hallucinated a 15% discount on a product that didn't exist. Revenue impact: $0. But trust impact: significant. The root cause wasn't a bug in my code, nor a prompt injection. It was a subtle shift in the LLM's internal reasoning, a drift that unit tests, by their very nature, cannot detect.&lt;/p&gt;

&lt;p&gt;This experience led to a hard decision: no new AI agent feature ships without first integrating into my evaluation harness. I now have 131 tests across four distinct layers, costing me $0.03 per full run. This harness isn't a luxury; it's the bedrock of shipping production AI agents with zero VC funding and a single developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fundamental Flaw of Unit Tests for AI Agents
&lt;/h2&gt;

&lt;p&gt;Unit tests verify &lt;em&gt;my code&lt;/em&gt;. They assert that &lt;code&gt;my_function(input)&lt;/code&gt; returns &lt;code&gt;expected_output&lt;/code&gt;. For traditional software, this is sufficient. For AI agents, it's dangerously incomplete. An AI agent's core logic isn't deterministic code I wrote; it's emergent behavior from an LLM interacting with tools and external systems.&lt;/p&gt;

&lt;p&gt;Consider a multi-agent system designed to process customer inquiries.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Router Agent:&lt;/strong&gt; Classifies incoming messages (e.g., "sales," "support," "technical").&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Sales Agent:&lt;/strong&gt; Qualifies leads, retrieves product info from a database, generates a personalized offer.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Support Agent:&lt;/strong&gt; Accesses knowledge base, schedules appointments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A unit test might verify: &lt;code&gt;router_agent.classify("I need a new laptop")&lt;/code&gt; returns &lt;code&gt;"sales"&lt;/code&gt;. But what if the LLM behind the router agent, after a model update or a subtle change in its training data, starts classifying "I need help configuring my new laptop" as &lt;code&gt;"sales"&lt;/code&gt; instead of &lt;code&gt;"support"&lt;/code&gt;? My unit test still passes because the &lt;em&gt;function&lt;/em&gt; &lt;code&gt;classify&lt;/code&gt; executed without error. The &lt;em&gt;semantic intent&lt;/em&gt; changed. This is where an AI agent evaluation harness 131 tests production becomes indispensable.&lt;/p&gt;

&lt;h2&gt;
  
  
  My 4-Layer Evaluation Harness
&lt;/h2&gt;

&lt;p&gt;My harness runs on Oracle Cloud Infrastructure (OCI) Functions, triggered by new code commits. Each layer targets a different aspect of agent reliability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Core Functionality (38 Tests)
&lt;/h3&gt;

&lt;p&gt;These are the closest to traditional unit tests, but they operate at the agent's public API level. They verify that the agent can perform its primary tasks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Example:&lt;/strong&gt; For a lead qualification agent, tests include:

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;agent.process_message("I need 100 widgets")&lt;/code&gt; -&amp;gt; asserts &lt;code&gt;response.includes("quote")&lt;/code&gt; and &lt;code&gt;response.includes("delivery time")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;agent.process_message("Tell me about product X")&lt;/code&gt; -&amp;gt; asserts &lt;code&gt;response.includes("features of X")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;agent.process_message("I want to speak to a human")&lt;/code&gt; -&amp;gt; asserts &lt;code&gt;response.includes("transferring to human")&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests use specific, deterministic inputs and check for expected keywords or structured outputs. They catch regressions in tool calls, API integrations, or basic prompt adherence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Semantic Intent &amp;amp; Reasoning (52 Tests)
&lt;/h3&gt;

&lt;p&gt;This is where the harness starts to diverge significantly from unit testing. These tests focus on whether the agent &lt;em&gt;understands&lt;/em&gt; and &lt;em&gt;acts appropriately&lt;/em&gt; based on the user's intent, even with varied phrasing. This layer is crucial for catching the "silent failures" I experienced.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Methodology:&lt;/strong&gt; Each test case has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;user_input_variations&lt;/code&gt;: An array of 3-5 semantically similar phrases (e.g., "I want a quote," "How much does it cost?", "Pricing for X").&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;expected_intent&lt;/code&gt;: A categorical label (e.g., "request_quote").&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;expected_action&lt;/code&gt;: The tool or internal function the agent &lt;em&gt;should&lt;/em&gt; invoke (e.g., &lt;code&gt;call_pricing_api&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;expected_output_keywords&lt;/code&gt;: Keywords that &lt;em&gt;must&lt;/em&gt; appear in the final response.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;unexpected_output_keywords&lt;/code&gt;: Keywords that &lt;em&gt;must not&lt;/em&gt; appear.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Example Test Case:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing Inquiry - Product A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_input_variations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"How much is Product A?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Cost of Product A please."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Give me a quote for Product A."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"What's the price tag on Product A?"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"request_pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_product_pricing_tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_output_keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unexpected_output_keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"discount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipping cost"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The harness runs each variation through the agent. It then uses a small, fine-tuned classification model (or a separate LLM call with a strict prompt) to verify the &lt;code&gt;expected_intent&lt;/code&gt; from the agent's internal logs and the &lt;code&gt;expected_action&lt;/code&gt; from tool call logs. Finally, it checks the final output against &lt;code&gt;expected_output_keywords&lt;/code&gt; and &lt;code&gt;unexpected_output_keywords&lt;/code&gt;. This catches hallucination of discounts or incorrect routing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 3: Robustness &amp;amp; Edge Cases (29 Tests)
&lt;/h3&gt;

&lt;p&gt;This layer pushes the agent with malformed inputs, ambiguous requests, and high-load scenarios.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Ambiguity:&lt;/strong&gt; "I need help." (Should trigger clarification or human transfer).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Missing Info:&lt;/strong&gt; "Quote for widgets." (Should ask for quantity).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Out-of-Scope:&lt;/strong&gt; "Tell me a joke." (Should politely decline or redirect).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Long Inputs:&lt;/strong&gt; Messages exceeding typical character limits.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Rapid-fire:&lt;/strong&gt; Sending 5 messages in 2 seconds to simulate burst traffic (tested with a dedicated load testing script, not part of the $0.03/run cost).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each of these, the harness asserts specific fallback behaviors or error messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Safety &amp;amp; Guardrails (12 Tests)
&lt;/h3&gt;

&lt;p&gt;These tests focus on preventing harmful or inappropriate responses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;PII Evasion:&lt;/strong&gt; Inputs designed to trick the agent into revealing personal data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Harmful Content:&lt;/strong&gt; Prompts asking for illegal activities or hate speech.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sensitive Topics:&lt;/strong&gt; Inputs related to politics, religion, or medical advice (for non-specialized agents).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harness expects a refusal, a redirection, or a canned safety response. I use Groq for its speed in these checks, as the response time is critical for real-time moderation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost: $0.03 Per Full Run
&lt;/h2&gt;

&lt;p&gt;Running 131 tests across four layers isn't free, but it's cheap enough to run on every commit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LLM Invocations:&lt;/strong&gt; The primary cost driver. I dynamically route between Groq (for speed-critical, simple classification/refusal checks) and Claude 3 Haiku (for more complex reasoning and output generation).

&lt;ul&gt;
&lt;li&gt;  Groq: ~$0.000008 / 1k tokens.&lt;/li&gt;
&lt;li&gt;  Claude 3 Haiku: ~$0.00025 / 1k input tokens, ~$0.00125 / 1k output tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Functions:&lt;/strong&gt; Serverless execution for the harness logic. Billed per invocation and GB-second. Negligible for my scale.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Object Storage:&lt;/strong&gt; Storing test cases, results, and logs. Also negligible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical full run involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ~100 Claude 3 Haiku calls (avg 200 input tokens, 100 output tokens) = $0.005 + $0.0125 = $0.0175&lt;/li&gt;
&lt;li&gt;  ~30 Groq calls (avg 50 input tokens, 20 output tokens) = $0.000012&lt;/li&gt;
&lt;li&gt;  OCI Function execution time: &amp;lt;1 second total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~$0.0175 + negligible. I round up to $0.03 to account for occasional longer responses or additional internal logging. This cost is a fraction of the potential revenue loss or reputational damage from a single silent failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Failure It Caught
&lt;/h2&gt;

&lt;p&gt;Last month, I was developing a new feature for a customer support agent: dynamic FAQ generation based on user queries. The idea was to use an RAG system to pull relevant knowledge base articles and summarize them.&lt;/p&gt;

&lt;p&gt;My unit tests passed. The RAG system retrieved correct articles. The summarization prompt worked on isolated examples. I pushed the code.&lt;/p&gt;

&lt;p&gt;The eval harness ran. Layer 2, Semantic Intent &amp;amp; Reasoning, failed one test: "Query about refund policy." The expected output was a summary of the refund policy. The actual output included a sentence: "Please note, all refunds are subject to a 10% processing fee."&lt;/p&gt;

&lt;p&gt;This was a hallucination. Our refund policy has no processing fee. The RAG system had correctly retrieved the policy. The summarization LLM (Claude 3 Haiku, at the time) had &lt;em&gt;added&lt;/em&gt; this detail, likely from its general training data about refunds, despite the explicit instruction in the prompt to only use provided context.&lt;/p&gt;

&lt;p&gt;Without the harness, this would have shipped. A customer asking about a refund would have been told about a non-existent fee, leading to confusion, frustration, and a direct support ticket. The $0.03 cost of that eval run saved me a customer interaction and preserved trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building an AI agent evaluation harness 131 tests production isn't optional for serious AI development. It's a critical infrastructure component that catches emergent failures unit tests cannot. It's the difference between shipping robust, reliable agents and constantly firefighting silent, trust-eroding bugs. My $0.03 per run is the cheapest insurance policy I've ever bought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you manage the test data for 131 tests? Is it all hardcoded JSON?&lt;/strong&gt;&lt;br&gt;
A: The core test cases are JSON files stored in OCI Object Storage. For variations and negative tests, I use a small Python script that programmatically generates additional inputs based on templates, ensuring coverage without manually writing every permutation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What if the LLM itself changes its behavior, making existing tests fail even if my code is correct?&lt;/strong&gt;&lt;br&gt;
A: This is precisely what the harness is designed to detect. If an LLM update causes a test failure, it's a signal to either adjust the prompt (to guide the LLM back to desired behavior) or accept the new behavior and update the test's expected output. It's a continuous calibration process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle non-deterministic LLM outputs in your assertions?&lt;/strong&gt;&lt;br&gt;
A: I avoid strict string equality. Instead, I use keyword presence/absence checks, regex patterns, and sometimes a secondary, smaller LLM (like Groq) to classify the agent's output against expected intent or sentiment. This allows for variability while ensuring core requirements are met.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is $0.03 per run sustainable for larger teams or more complex agents?&lt;/strong&gt;&lt;br&gt;
A: Yes. The cost scales with LLM usage, not linearly with the number of tests if tests are designed efficiently. For larger teams, the cost per run might increase, but the value of catching critical errors early far outweighs it. My current setup is optimized for minimal token usage per test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you integrate this with CI/CD?&lt;/strong&gt;&lt;br&gt;
A: My OCI Functions are triggered by new commits to my Git repository. If any test fails, the CI pipeline breaks, preventing the new code from being deployed to production. This ensures that no untested or failing agent code ever reaches users.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>131 Tests, 4 Layers, $00.03/Run: My AI Agent Eval Harness</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Fri, 07 Aug 2026 19:30:18 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/131-tests-4-layers-0003run-my-ai-agent-eval-harness-2e2o</link>
      <guid>https://dev.to/elenarevicheva/131-tests-4-layers-0003run-my-ai-agent-eval-harness-2e2o</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/131-tests-4-layers-0003run-my-ai-agent-eval-harness-2026-08-07" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I shipped a production AI agent that silently failed 17% of the time. The agent was designed to process user requests from Telegram, route them to a specialized Groq-powered summarizer, then to a Claude-powered content generator, and finally format the output for WhatsApp. My unit tests passed. My integration tests passed. My end-to-end tests, which checked the final output format, also passed. The problem was not &lt;em&gt;if&lt;/em&gt; it produced output, but &lt;em&gt;what&lt;/em&gt; output.&lt;/p&gt;

&lt;p&gt;The agent was supposed to summarize a technical document and then generate a social media post. Without an AI agent evaluation harness, I would have continued shipping a system that generated social media posts based on &lt;em&gt;irrelevant sections&lt;/em&gt; of the document, or worse, hallucinated key facts. This wasn't a bug in my Python code; it was a failure in the AI's reasoning, its ability to follow complex, multi-step instructions, and its robustness to varied inputs. This is why I stopped all feature development and spent two weeks building an AI agent evaluation harness with 131 tests across four distinct layers, costing me $0.03 per full run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Failure: Why Unit Tests Are Blind
&lt;/h2&gt;

&lt;p&gt;My initial test suite for the multi-agent system included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Unit tests:&lt;/strong&gt; Checked individual Python functions, e.g., &lt;code&gt;parse_telegram_input()&lt;/code&gt;, &lt;code&gt;format_whatsapp_output()&lt;/code&gt;. (100% coverage)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Integration tests:&lt;/strong&gt; Verified API calls to Groq and Claude, ensuring correct request/response structure. (Passed)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;End-to-end tests:&lt;/strong&gt; Confirmed the final WhatsApp message structure and presence of expected keywords. (Passed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent’s core task was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Receive a document URL and a target social media platform from Telegram.&lt;/li&gt;
&lt;li&gt; Fetch the document.&lt;/li&gt;
&lt;li&gt; Summarize the document using Groq (for speed).&lt;/li&gt;
&lt;li&gt; Generate a social media post using Claude (for nuance and creativity).&lt;/li&gt;
&lt;li&gt; Send the post to WhatsApp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The silent failure occurred in step 3 and 4. A user uploaded a PDF of a research paper. The Groq summarizer, under certain prompt variations, would focus on the "Acknowledgements" section or the "References" instead of the "Abstract" and "Methodology." Claude, receiving this skewed summary, would then generate a social media post about the &lt;em&gt;authors' funding sources&lt;/em&gt; or &lt;em&gt;related works&lt;/em&gt;, not the paper's findings. The final WhatsApp message &lt;em&gt;looked&lt;/em&gt; correct structurally, but the content was fundamentally wrong.&lt;/p&gt;

&lt;p&gt;This is a class of failure that traditional software tests cannot catch. Unit tests verify code logic. Integration tests verify component communication. End-to-end tests verify system flow and final output &lt;em&gt;format&lt;/em&gt;. None of them verify &lt;em&gt;semantic correctness&lt;/em&gt; or &lt;em&gt;reasoning fidelity&lt;/em&gt; of an AI agent. This requires a dedicated AI agent evaluation harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Input Robustness (35 Tests)
&lt;/h2&gt;

&lt;p&gt;The first layer of my evaluation harness focuses on how well the agent handles diverse and challenging inputs. My agents operate on Telegram and WhatsApp, meaning inputs are often unstructured, misspelled, or incomplete.&lt;/p&gt;

&lt;p&gt;I built 35 test cases covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Malformed URLs:&lt;/strong&gt; &lt;code&gt;htps://example.com&lt;/code&gt;, &lt;code&gt;example.com&lt;/code&gt;, &lt;code&gt;ftp://example.com/doc.pdf&lt;/code&gt;. Expected: graceful error, prompt for correct URL.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Non-existent URLs:&lt;/strong&gt; &lt;code&gt;https://nonexistent-domain-12345.com/doc.pdf&lt;/code&gt;. Expected: network error handling, user notification.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unsupported document types:&lt;/strong&gt; &lt;code&gt;https://example.com/image.jpg&lt;/code&gt;, &lt;code&gt;https://example.com/video.mp4&lt;/code&gt;. Expected: "unsupported format" message.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Empty messages:&lt;/strong&gt; User sends nothing. Expected: prompt for input.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Gibberish:&lt;/strong&gt; "asdfghjkl;" Expected: "I don't understand" message.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Language variations:&lt;/strong&gt; Requests in Spanish, French (my agents are English-only for now). Expected: "English only" message.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Long inputs:&lt;/strong&gt; A 10,000-word text pasted directly into Telegram. Expected: truncation or "too long" message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each test case is a JSON object defining the input, expected output (regex or specific string), and a pass/fail condition. The harness simulates a Telegram message, injects it into the agent's entry point, and captures the final response. This layer alone caught 8 critical failures related to URL parsing and content fetching that my previous tests missed. For example, a malformed URL like &lt;code&gt;example.com&lt;/code&gt; would previously crash the document fetching service, leading to a silent failure for the user. Now, it triggers a specific "invalid URL" response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Core Reasoning &amp;amp; Prompt Adherence (50 Tests)
&lt;/h2&gt;

&lt;p&gt;This is the most critical layer, designed to catch the semantic failures I described. It focuses on the agent's ability to understand and execute complex instructions, especially across multiple LLM calls.&lt;/p&gt;

&lt;p&gt;I developed 50 test cases, each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;A specific document:&lt;/strong&gt; (e.g., a research paper, a news article, a product manual).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A specific prompt/instruction:&lt;/strong&gt; (e.g., "Summarize this for a 10-year-old," "Extract key takeaways for investors," "Generate a LinkedIn post highlighting the challenges.")&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Evaluation criteria:&lt;/strong&gt; These are not simple string matches. They are a combination of:

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Keyword presence/absence:&lt;/strong&gt; "Must contain 'AI' and 'Panama', must NOT contain 'Acknowledgements'."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Semantic similarity:&lt;/strong&gt; Using an embedding model (e.g., &lt;code&gt;text-embedding-ada-002&lt;/code&gt; via Oracle OCI Generative AI service) to compare the generated output against a human-written "gold standard" answer. A cosine similarity score below 0.7 triggers a failure.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Factuality check:&lt;/strong&gt; For specific factual extraction tasks, I use a small, fine-tuned LLM (running on Oracle Cloud Infrastructure's GPU instances) to act as a "critic," comparing generated facts against the source document. This critic model is prompted with "Given the document X, is statement Y true? Answer only 'Yes' or 'No'."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Constraint adherence:&lt;/strong&gt; "Output must be under 280 characters," "Must use bullet points."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Document:&lt;/strong&gt; A 5-page PDF on "Quantum Computing Advancements."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Prompt:&lt;/strong&gt; "Generate a tweet summarizing the main breakthrough for a general audience. Keep it under 280 characters. Focus on the practical implications."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Evaluation:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Length check: &lt;code&gt;&amp;lt; 280 chars&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Keyword check: &lt;code&gt;contains "quantum"&lt;/code&gt;, &lt;code&gt;contains "breakthrough"&lt;/code&gt;, &lt;code&gt;NOT contains "Schrödinger equation"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Semantic similarity: Compare generated tweet embedding to a human-written tweet embedding (cosine similarity &amp;gt; 0.75).&lt;/li&gt;
&lt;li&gt;  Factuality: Critic model checks if the "practical implications" mentioned are actually present in the document.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layer revealed that my Groq summarizer, while fast, sometimes over-simplified or omitted crucial context, leading Claude to generate misleading posts. I adjusted Groq's system prompt to explicitly emphasize "main findings and their implications" and added a post-processing step to re-rank summary sentences based on their embedding similarity to the initial user query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Multi-Turn &amp;amp; State Management (26 Tests)
&lt;/h2&gt;

&lt;p&gt;My agents are designed for conversational interfaces. This means they need to maintain context and respond appropriately in multi-turn interactions.&lt;/p&gt;

&lt;p&gt;26 tests cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Follow-up questions:&lt;/strong&gt; User asks "What about X?" after a summary. Expected: agent refers to the previous summary and answers X if relevant.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Clarification requests:&lt;/strong&gt; Agent asks "Which document are you referring to?" if context is ambiguous.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Context switching:&lt;/strong&gt; User asks for a summary of Document A, then immediately asks for a social media post for Document B. Expected: agent correctly switches context and processes Document B.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Interrupted flows:&lt;/strong&gt; User starts a task, then sends "stop" or "cancel." Expected: agent gracefully terminates the current task.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Error recovery:&lt;/strong&gt; Agent encounters an error (e.g., API timeout), then user retries. Expected: agent attempts to restart or guide the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests simulate a sequence of messages, checking the agent's state at each step. For instance, after a user provides a document URL, the harness checks if the agent's internal state correctly stores this URL before the next message. This layer helped me refine my state management logic, moving from a simple dictionary to a more robust, session-based context store backed by Oracle Autonomous Database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Performance &amp;amp; Cost (20 Tests)
&lt;/h2&gt;

&lt;p&gt;While not directly about correctness, performance and cost are critical for production systems, especially with LLM APIs.&lt;/p&gt;

&lt;p&gt;20 tests monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; Time taken for specific operations (e.g., summarization, generation). I set thresholds (e.g., Groq summarization &amp;lt; 2 seconds, Claude generation &amp;lt; 10 seconds).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Token usage:&lt;/strong&gt; Number of input/output tokens for each LLM call. This directly impacts cost. I monitor for unexpected spikes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;API call count:&lt;/strong&gt; Number of external API calls per request. An unexpected increase could indicate a loop or inefficient prompting.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Error rates:&lt;/strong&gt; Percentage of API calls returning non-200 status codes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Total cost per run:&lt;/strong&gt; The harness aggregates token usage and API calls, then calculates the estimated cost based on current provider rates (Groq: $0.0002/1K tokens, Claude 3 Sonnet: $3/M input tokens, $15/M output tokens). My target is $0.03 per full agent interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harness runs these 131 tests nightly on Oracle Cloud Infrastructure's always-free tier compute instances. The total cost for a full run, including LLM calls, is consistently around $0.03. This low cost allows me to run it frequently, catching regressions quickly. The performance tests revealed that certain complex prompts for Claude were pushing generation times beyond acceptable limits, leading me to optimize prompt structure and explore smaller Claude models for specific tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Not Evaluating
&lt;/h2&gt;

&lt;p&gt;Building this AI agent evaluation harness took me two weeks. This was time I wasn't building new features, wasn't acquiring new users. But without it, I would have continued shipping a product that was fundamentally broken for a significant portion of its use cases. The cost of debugging production issues, losing user trust, and rebuilding features would have been far higher.&lt;/p&gt;

&lt;p&gt;My 131 tests across four layers provide a safety net that traditional software testing cannot. They ensure my multi-agent system not only functions technically but also reasons correctly, adheres to instructions, and provides valuable, accurate outputs. This is non-negotiable for any AI agent moving into production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you manage the "gold standard" answers for semantic similarity tests?&lt;/strong&gt;&lt;br&gt;
A: For critical paths, I manually create 2-3 "gold standard" answers per test case. For less critical paths, I use a stronger LLM (like Claude 3 Opus) to generate a reference answer, which is then manually reviewed and approved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What if the LLM models change or update, invalidating my semantic similarity scores?&lt;/strong&gt;&lt;br&gt;
A: This is a real risk. I pin to specific model versions where possible (e.g., &lt;code&gt;claude-3-sonnet-20240229&lt;/code&gt;). When models update, I re-run the entire eval harness. If a significant number of semantic similarity tests fail, it indicates a model drift, and I either adjust the gold standards or fine-tune my prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle the cost of running 131 tests, especially with expensive LLMs?&lt;/strong&gt;&lt;br&gt;
A: I optimize by using cheaper, faster models (like Groq) for summarization and initial routing within the harness itself where possible. For the actual generation tests, I use the production models but keep the input documents and desired outputs concise to minimize token usage. The $0.03/run is a calculated average, and I monitor it closely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's your strategy for evaluating agents that generate creative content, where there's no single "correct" answer?&lt;/strong&gt;&lt;br&gt;
A: For creative content, I rely more on constraint adherence (e.g., tone, style, length) and negative constraints (e.g., "must not be offensive," "must not hallucinate facts"). Semantic similarity is used to check for relevance to the prompt, not exact phrasing. Human review of a subset of creative outputs is also essential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you integrate this harness into your CI/CD pipeline on Oracle Cloud?&lt;/strong&gt;&lt;br&gt;
A: The harness is a separate Python application. I use Oracle Cloud Infrastructure (OCI) DevOps to trigger a nightly run. The results (pass/fail, latency, cost) are stored in an OCI Object Storage bucket and visualized via OCI Logging Analytics dashboards. A critical failure triggers a notification via OCI Notifications.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Automated Publishing: From GSC Gap to Live Post in 11 Minutes</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Thu, 06 Aug 2026 19:30:14 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/automated-publishing-from-gsc-gap-to-live-post-in-11-minutes-14j5</link>
      <guid>https://dev.to/elenarevicheva/automated-publishing-from-gsc-gap-to-live-post-in-11-minutes-14j5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/automated-publishing-from-gsc-gap-to-live-post-in-11-minutes" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first attempt at an AI content pipeline failed to generate a single useful article for three weeks. The problem wasn't the LLM, the orchestrator, or the publishing API. It was the definition of "content gap." Google Search Console (GSC) showed 15 queries for &lt;code&gt;aideazz.xyz&lt;/code&gt; with impressions but zero clicks. My initial thought: these are the gaps. I built an agent to pick the highest impression/zero-click query, send it to Claude 3.5 Sonnet, and publish the result. The articles were technically correct, but nobody clicked. Why? Because a "gap" isn't just a missing page; it's a missing &lt;em&gt;intent match&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GSC Zero-Click Trap: Why "Gap" Isn't Just Missing Content
&lt;/h2&gt;

&lt;p&gt;My GSC data for &lt;code&gt;aideazz.xyz&lt;/code&gt; showed queries like "aideazz pricing," "aideazz contact," "aideazz reviews." These are navigational or transactional queries. A blog post titled "Aideazz Pricing Explained" isn't what a user searching "aideazz pricing" wants. They want a pricing page, not an article. The zero-click rate wasn't a content gap; it was a &lt;em&gt;format gap&lt;/em&gt; or a &lt;em&gt;conversion gap&lt;/em&gt;. My agent was filling a non-existent content void with irrelevant blog posts.&lt;/p&gt;

&lt;p&gt;The real content gap for a technical audience lies in informational queries where your site &lt;em&gt;could&lt;/em&gt; rank, but doesn't, or where existing content is weak. This requires a more nuanced GSC analysis than simply filtering for zero-click queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redefining "Gap": From Zero-Click to Intent Mismatch
&lt;/h2&gt;

&lt;p&gt;I pivoted the GSC analysis agent. Instead of focusing on zero-click queries, it now looks for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Low-CTR informational queries:&lt;/strong&gt; Queries with impressions &amp;gt; 500, average position &amp;lt; 20, and CTR &amp;lt; 1%. These indicate potential ranking opportunities where existing content isn't compelling enough, or where a new, targeted article could perform better.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Queries with high impressions and no relevant page:&lt;/strong&gt; This is harder to automate. It requires a semantic search over existing content to confirm no page addresses the query directly. My current agent uses a vector database of existing article embeddings. If a query's embedding similarity to all existing articles is below a threshold (e.g., cosine similarity &amp;lt; 0.7), it's flagged as a potential gap.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Competitor keyword overlap (manual step):&lt;/strong&gt; This is still a manual process. I use Ahrefs to find keywords my competitors rank for where &lt;code&gt;aideazz.xyz&lt;/code&gt; has no presence. This informs the GSC agent's filtering criteria.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent now pulls GSC data via the API, filters by these criteria, and prioritizes based on a weighted score: &lt;code&gt;(impressions * (1 - CTR)) + (position_score * 0.5)&lt;/code&gt;. &lt;code&gt;position_score&lt;/code&gt; is &lt;code&gt;(20 - average_position) / 20&lt;/code&gt; to give higher priority to queries closer to page one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Agent Content Generation Pipeline: Oracle Cloud to Dev.to
&lt;/h2&gt;

&lt;p&gt;Once a topic is identified by the GSC analysis agent, it's passed to the content generation pipeline. This is a multi-agent system running on Oracle Cloud Infrastructure (OCI) using OCI Functions and Container Instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent 1: The Research &amp;amp; Outline Agent
&lt;/h3&gt;

&lt;p&gt;This agent receives the prioritized query. Its first task is to generate a detailed outline. It uses a combination of techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;SERP analysis:&lt;/strong&gt; It performs a real-time Google search for the target query, scrapes the top 10 results, and extracts headings, common themes, and entities. This is crucial for understanding current ranking content and identifying sub-topics.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Audience persona:&lt;/strong&gt; It's pre-configured with a persona for "skeptical technical founder/developer." This influences the tone and depth of the outline.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Constraint injection:&lt;/strong&gt; It's explicitly told to include specific constraints relevant to AIdeazz (e.g., "mention Oracle Cloud," "discuss zero VC funding," "emphasize shipping production agents").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The outline is structured with specific instructions for the drafting agent: "Lead with failure/constraint," "use numbers," "avoid clichés." This outline is then reviewed by a human (me) for 5-10 minutes. This is the only human in the loop for content creation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent 2: The Drafting Agent (Claude 3.5 Sonnet)
&lt;/h3&gt;

&lt;p&gt;The approved outline goes to the drafting agent. I've experimented with various LLMs (GPT-4o, Llama 3, Mixtral), but Claude 3.5 Sonnet consistently produces the best first drafts for technical content, especially when given strict formatting and tone instructions. Its ability to follow complex constraints without hallucinating or becoming overly verbose is superior for this use case.&lt;/p&gt;

&lt;p&gt;The prompt for Claude includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The full outline.&lt;/li&gt;
&lt;li&gt;  Instructions for tone: "Skeptical, direct, technical, no fluff."&lt;/li&gt;
&lt;li&gt;  Hard rules: "Lead with failure/constraint/number," "no clichés," "use specific numbers/error messages/costs," "first person or neutral technical," "Markdown only, ## sections, no H1."&lt;/li&gt;
&lt;li&gt;  Target length: 1400-2400 words.&lt;/li&gt;
&lt;li&gt;  Specific instructions for the FAQ section and byline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The drafting agent runs on a dedicated OCI Container Instance, making API calls to Anthropic. The average generation time for a 2000-word article is 90-120 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent 3: The Publishing Agent (Dev.to &amp;amp; Aideazz.xyz Cache)
&lt;/h3&gt;

&lt;p&gt;Once the draft is generated, it's passed to the publishing agent. This agent has two primary functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dev.to API integration:&lt;/strong&gt; It uses the Dev.to API to create a new post. The article content (Markdown), title, and relevant tags are extracted from the draft. It also sets &lt;code&gt;published: false&lt;/code&gt; initially, allowing for a final review on Dev.to's platform before going live.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Aideazz.xyz caching:&lt;/strong&gt; A copy of the Markdown content is stored in an OCI Object Storage bucket, which acts as a cache for &lt;code&gt;aideazz.xyz&lt;/code&gt;. This ensures the content is immediately available on my own domain, even before Dev.to publishes it. A separate OCI Function triggers a static site regeneration for &lt;code&gt;aideazz.xyz&lt;/code&gt; to pull this new content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire process, from GSC query identification to the article being available on &lt;code&gt;aideazz.xyz&lt;/code&gt; and staged on Dev.to, takes approximately 11 minutes, including the 5-10 minute human review of the outline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Metrics: Before and After
&lt;/h2&gt;

&lt;p&gt;Before this refined &lt;code&gt;AI content pipeline GSC gap analysis automated publishing&lt;/code&gt; system, my blog posts were sporadic and often missed the mark. I was publishing 1-2 articles per month, with an average CTR of 0.8% from organic search.&lt;/p&gt;

&lt;p&gt;Since implementing this system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Publishing frequency:&lt;/strong&gt; 3-4 articles per week.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Average organic CTR:&lt;/strong&gt; Increased to 2.1% across new articles.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Time to publish:&lt;/strong&gt; Reduced from 4-8 hours (manual) to 11 minutes (automated + human outline review).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;GSC impressions for new articles:&lt;/strong&gt; Average 1,200 impressions in the first 30 days, up from 350.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key improvement wasn't just speed or volume, but &lt;em&gt;relevance&lt;/em&gt;. By focusing on true intent gaps identified through a more sophisticated GSC analysis, the content now directly addresses what my target audience is searching for, leading to higher engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure: Why Oracle Cloud and Groq/Claude Routing
&lt;/h2&gt;

&lt;p&gt;I run AIdeazz on Oracle Cloud Infrastructure (OCI) for several reasons, primarily cost-effectiveness and performance for GPU-intensive workloads. My multi-agent system leverages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;OCI Functions:&lt;/strong&gt; Serverless compute for orchestrating agents, GSC API calls, and publishing logic. Cost is near-zero for this usage.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Container Instances:&lt;/strong&gt; For the drafting agent (Claude API calls) and SERP scraping. Provides isolated environments and scales efficiently.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Object Storage:&lt;/strong&gt; For caching articles and storing GSC data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Generative AI Service:&lt;/strong&gt; While I use Anthropic's Claude directly for drafting, OCI's Gen AI service is used for other internal tasks, like summarization and data extraction from unstructured documents, providing a unified platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I route LLM calls dynamically. For drafting, Claude 3.5 Sonnet is the default due to its quality. For faster, less critical tasks (e.g., quick rephrasing, simple data extraction), I use Groq's Llama 3 8B or 70B via their API. This routing decision is made by an OCI Function based on the &lt;code&gt;task_type&lt;/code&gt; parameter passed to the LLM orchestration layer. Groq offers significantly lower latency (tens of milliseconds) for smaller models, which is critical for interactive agents or high-throughput, low-complexity tasks. For a 2000-word article, Claude's 90-120 second generation time is acceptable, but for real-time chat agents, Groq is indispensable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you prevent AI-generated content from sounding generic or repetitive?&lt;/strong&gt;&lt;br&gt;
A: The key is the detailed outline and strict prompt engineering. The outline agent performs real-time SERP analysis to ensure novelty, and the drafting agent's prompt explicitly forbids clichés and demands specific examples/numbers, forcing it to generate unique content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the cost of running this pipeline?&lt;/strong&gt;&lt;br&gt;
A: Excluding the LLM API costs (which vary based on usage, but average $50-100/month for content generation), the OCI infrastructure for the agents, functions, and storage costs less than $10/month. This is due to OCI's generous free tier and efficient serverless billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle factual accuracy and potential hallucinations?&lt;/strong&gt;&lt;br&gt;
A: The research agent's SERP analysis provides a factual grounding. For highly sensitive topics, a human review of the &lt;em&gt;draft&lt;/em&gt; (not just the outline) is added. For this blog, the outline review is sufficient, as the topics are within my domain expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not use a single, larger LLM for everything?&lt;/strong&gt;&lt;br&gt;
A: Cost and performance. A smaller, specialized agent for GSC analysis is cheaper and faster than using a large LLM. Routing to Groq for specific tasks optimizes for latency where needed, while Claude handles the heavy lifting of drafting. This multi-agent, multi-LLM approach is more efficient.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>LangGraph Checkpointing: Three Production Rewrites to Stop Losing State</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Wed, 05 Aug 2026 19:30:18 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/langgraph-checkpointing-three-production-rewrites-to-stop-losing-state-1561</link>
      <guid>https://dev.to/elenarevicheva/langgraph-checkpointing-three-production-rewrites-to-stop-losing-state-1561</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/langgraph-checkpointing-three-production-rewrites-to-stop-losing-state-2026-08-05" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first LangGraph production agent silently discarded every job for weeks. The agent was supposed to process user requests from Telegram, break them down into sub-tasks, and execute them across multiple steps, storing intermediate results. Instead, it would process the first step, then restart from scratch on the next invocation, losing all prior context. The problem wasn't in the agent logic itself, but in how I was attempting to persist its state.&lt;/p&gt;

&lt;p&gt;I burned through three distinct LangGraph checkpointing strategies before I found one that reliably worked for stateful agents in production. Each failure taught me a critical lesson about the assumptions LangGraph makes and the realities of deploying multi-step AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent State Schema Mismatch
&lt;/h2&gt;

&lt;p&gt;My initial approach was simple: use LangGraph's built-in &lt;code&gt;SqliteSaver&lt;/code&gt;. It seemed robust enough for a single-instance deployment on an Oracle Cloud VM. The agent's graph defined a state, let's call it &lt;code&gt;AgentState&lt;/code&gt;, with fields like &lt;code&gt;user_id: str&lt;/code&gt;, &lt;code&gt;request_id: str&lt;/code&gt;, &lt;code&gt;task_list: list[str]&lt;/code&gt;, and &lt;code&gt;current_step: int&lt;/code&gt;.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_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;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_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;current_step&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;# ... more fields added later
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent would receive a message, initialize &lt;code&gt;AgentState&lt;/code&gt;, and kick off the graph. Subsequent messages from the same &lt;code&gt;user_id&lt;/code&gt; were supposed to resume the existing state.&lt;/p&gt;

&lt;p&gt;The problem started when I needed to add a new field to &lt;code&gt;AgentState&lt;/code&gt;, say &lt;code&gt;tool_output: dict&lt;/code&gt;. I updated the &lt;code&gt;TypedDict&lt;/code&gt;, redeployed the agent, and expected it to pick up where it left off. It didn't. Existing conversations would restart. New conversations worked fine.&lt;/p&gt;

&lt;p&gt;I debugged for days, tracing &lt;code&gt;SqliteSaver&lt;/code&gt; calls, checking the database directly. The &lt;code&gt;checkpoint&lt;/code&gt; table in SQLite stores a JSON blob of the state. What I found was insidious: LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; does not perform schema migration or even warn about schema mismatches. When an older checkpoint (without &lt;code&gt;tool_output&lt;/code&gt;) was loaded into an agent expecting the new &lt;code&gt;AgentState&lt;/code&gt; schema, the &lt;code&gt;TypedDict&lt;/code&gt; instantiation would silently drop any fields not present in the loaded JSON, and also fail to initialize new fields with default values if they weren't explicitly handled.&lt;/p&gt;

&lt;p&gt;The agent would load a partial state, proceed as if it were complete, and then fail downstream because &lt;code&gt;tool_output&lt;/code&gt; was missing. Or, worse, it would just restart the entire process because a critical flag like &lt;code&gt;current_step&lt;/code&gt; was reset due to the partial load. This wasn't an error; it was a silent data loss. My solution was a manual, painful process: dump the SQLite checkpoints, manually migrate the JSON, and re-insert. This was not scalable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checkpoint Corruption Lottery
&lt;/h2&gt;

&lt;p&gt;After the &lt;code&gt;SqliteSaver&lt;/code&gt; debacle, I moved to a custom &lt;code&gt;OracleCloudObjectStorageSaver&lt;/code&gt;. My agents run on Oracle Cloud Infrastructure (OCI), and object storage is cheap and highly available. I implemented a &lt;code&gt;BaseCheckpointSaver&lt;/code&gt; subclass that would serialize the state to JSON and upload it to an OCI object storage bucket, using the &lt;code&gt;thread_id&lt;/code&gt; as the object name.&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;OracleCloudObjectStorageSaver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseCheckpointSaver&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;bucket_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;namespace&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;object_storage_client&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;bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket_name&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;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namespace&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object_storage_client&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;thread_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="o"&gt;-&amp;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;Checkpoint&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="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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&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;namespace&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;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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;loads&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&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="nc"&gt;Checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&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="c1"&gt;# Log and return None if object not found or corrupted
&lt;/span&gt;            &lt;span class="nf"&gt;print&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;Error loading checkpoint &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&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;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put&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;thread_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&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="o"&gt;-&amp;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;data&lt;/span&gt; &lt;span class="o"&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;checkpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&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;# default=str handles datetime objects
&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&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;namespace&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;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seemed more robust. I had full control over serialization and deserialization. I could add versioning to my state objects and handle migrations explicitly.&lt;/p&gt;

&lt;p&gt;Then came the checkpoint corruption. Occasionally, an agent would fail to load its state, reporting a JSON decoding error. The &lt;code&gt;get_object&lt;/code&gt; call would return valid data, but &lt;code&gt;json.loads&lt;/code&gt; would throw. Upon inspection, the JSON files in OCI object storage were truncated or malformed.&lt;/p&gt;

&lt;p&gt;The root cause was concurrency. My agents are stateless in themselves, running as serverless functions or on VMs that can scale. Multiple invocations for the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;thread_id&lt;/code&gt; could happen almost simultaneously, especially if a user sent rapid-fire messages. If two &lt;code&gt;put&lt;/code&gt; operations happened concurrently, one might overwrite the other partially, or a read might occur while a write was in progress, leading to corrupted JSON. OCI Object Storage provides eventual consistency, but not strong consistency for overwrites.&lt;/p&gt;

&lt;p&gt;My initial fix was to add a retry mechanism with exponential backoff for &lt;code&gt;put&lt;/code&gt; operations. This reduced the frequency of corruption but didn't eliminate it. The problem was fundamental: a simple overwrite model for state in a highly concurrent environment is a race condition waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Atomic Update Pattern: Versioning and Conditional Writes
&lt;/h2&gt;

&lt;p&gt;The solution that finally stabilized LangGraph stateful agents in production involved two key components: explicit state versioning and conditional writes.&lt;/p&gt;

&lt;p&gt;Instead of just storing the &lt;code&gt;Checkpoint&lt;/code&gt; object, I wrapped it in a custom envelope that included a version number.&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;VersionedCheckpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;version&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;checkpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Checkpoint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When loading a checkpoint, I would read the &lt;code&gt;version&lt;/code&gt; field. When writing, I would increment it. The crucial part was the &lt;em&gt;conditional write&lt;/em&gt;. OCI Object Storage, like S3, supports conditional requests using &lt;code&gt;If-Match&lt;/code&gt; or &lt;code&gt;If-None-Match&lt;/code&gt; headers, based on the ETag of the object. This allows for optimistic locking.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;put&lt;/code&gt; operation was modified to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Read the current object (if it exists) to get its ETag and current version.&lt;/li&gt;
&lt;li&gt; Increment the version number.&lt;/li&gt;
&lt;li&gt; Attempt to write the new object, including the new version, &lt;em&gt;conditionally&lt;/em&gt;. If the ETag of the object on the server doesn't match the ETag I read in step 1, it means another process modified the object. The write fails.&lt;/li&gt;
&lt;li&gt; If the write fails due to an ETag mismatch, retry the entire process (read, increment, write) a few times.
&lt;/li&gt;
&lt;/ol&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;OracleCloudObjectStorageAtomicSaver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseCheckpointSaver&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;bucket_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;namespace&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;object_storage_client&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;bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket_name&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;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namespace&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object_storage_client&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_retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;thread_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="o"&gt;-&amp;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;Checkpoint&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="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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&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;namespace&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;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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;loads&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="c1"&gt;# Expecting VersionedCheckpoint structure
&lt;/span&gt;            &lt;span class="n"&gt;versioned_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VersionedCheckpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;versioned_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checkpoint&lt;/span&gt;&lt;span class="sh"&gt;'&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="nf"&gt;print&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;Error loading checkpoint &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&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;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;put&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;thread_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&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;self&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="n"&gt;current_etag&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;current_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

            &lt;span class="c1"&gt;# Try to get current object and ETag for conditional write
&lt;/span&gt;            &lt;span class="k"&gt;try&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&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;namespace&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;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;current_etag&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;headers&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;etag&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;existing_data&lt;/span&gt; &lt;span class="o"&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;loads&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;current_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing_data&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;version&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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="c1"&gt;# Object might not exist, or other transient error. Proceed with no ETag.
&lt;/span&gt;                &lt;span class="nf"&gt;print&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;No existing object or error getting ETag for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&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="n"&gt;new_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;versioned_checkpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VersionedCheckpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_version&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;checkpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;data_to_write&lt;/span&gt; &lt;span class="o"&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;versioned_checkpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;headers&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;If-Match&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current_etag&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;current_etag&lt;/span&gt; &lt;span class="k"&gt;else&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&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;namespace&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;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_to_write&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;opc_meta&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;version&lt;/span&gt;&lt;span class="sh"&gt;'&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;new_version&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="c1"&gt;# Store version in metadata too
&lt;/span&gt;                    &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;# Success
&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;412 Precondition Failed&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;e&lt;/span&gt;&lt;span class="p"&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;self&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;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&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;Precondition failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, retrying (attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&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="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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="c1"&gt;# Re-raise if max retries reached or other error
&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&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;Failed to save checkpoint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; after &lt;/span&gt;&lt;span class="si"&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_retries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts.&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 pattern ensures that only one write operation succeeds at a time for a given &lt;code&gt;thread_id&lt;/code&gt;. If multiple agents try to update the same state concurrently, only the one whose &lt;code&gt;If-Match&lt;/code&gt; header correctly identifies the &lt;em&gt;current&lt;/em&gt; state's ETag will succeed. Others will fail and retry, eventually picking up the newly written state and applying their updates on top of it. This effectively serializes concurrent updates to the same checkpoint.&lt;/p&gt;

&lt;p&gt;This atomic update pattern, combined with explicit state versioning and schema management, finally provided the stability needed for production LangGraph agents. My agents now reliably maintain state across multiple steps, even under concurrent load, whether they're routing requests to Groq for fast initial responses or to Claude for complex reasoning. The cost of OCI Object Storage for this is negligible, typically under $5/month for hundreds of thousands of checkpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned for Stateful AI Agents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; is for single-process, non-evolving state.&lt;/strong&gt; It's fine for demos, but not for production where state schemas change or concurrency is a factor.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Explicitly manage your state schema.&lt;/strong&gt; &lt;code&gt;TypedDict&lt;/code&gt; is a compile-time hint, not a runtime validator or migrator. Implement your own versioning and migration logic for your state objects.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Concurrency is a state killer.&lt;/strong&gt; Any shared state in a distributed or concurrent system needs an atomic update mechanism. Simple overwrites lead to silent data corruption.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Leverage cloud primitives.&lt;/strong&gt; Object storage with conditional writes (&lt;code&gt;If-Match&lt;/code&gt; / &lt;code&gt;ETag&lt;/code&gt;) is a powerful, cost-effective primitive for optimistic locking. Don't reinvent the wheel with complex distributed locks unless absolutely necessary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Monitor and log everything.&lt;/strong&gt; The silent failures were the hardest to debug. Extensive logging of checkpoint loads, saves, versions, and retries is crucial.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My current agents, handling everything from customer support routing to internal data analysis, now run with this pattern. The initial pain of three rewrites was worth the stability and confidence it brought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not use a proper database like PostgreSQL with row-level locking for checkpointing?&lt;/strong&gt;&lt;br&gt;
A: A full relational database adds operational overhead (management, backups, scaling) and cost. For simple key-value state, object storage with conditional writes provides sufficient atomicity and is orders of magnitude cheaper and simpler to operate at scale for this specific use case. My current setup costs under $5/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle schema migrations for &lt;code&gt;AgentState&lt;/code&gt; with this approach?&lt;/strong&gt;&lt;br&gt;
A: When loading a &lt;code&gt;VersionedCheckpoint&lt;/code&gt;, I check the &lt;code&gt;version&lt;/code&gt; field. If the loaded version is older than the agent's current expected schema version, I apply explicit migration functions (e.g., adding default values for new fields, transforming old field names) before instantiating the &lt;code&gt;AgentState&lt;/code&gt; &lt;code&gt;TypedDict&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What if an agent crashes &lt;em&gt;during&lt;/em&gt; a &lt;code&gt;put&lt;/code&gt; operation, leaving a corrupted checkpoint?&lt;/strong&gt;&lt;br&gt;
A: The &lt;code&gt;put&lt;/code&gt; operation is designed to be idempotent and resilient. If a crash occurs mid-write, the next &lt;code&gt;get&lt;/code&gt; operation will either retrieve the last &lt;em&gt;successfully&lt;/em&gt; written checkpoint (if the partial write didn't overwrite the ETag) or fail to parse the JSON, triggering a retry or a fresh start. The conditional write helps prevent partial writes from corrupting a valid previous state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does this approach add significant latency to each agent step?&lt;/strong&gt;&lt;br&gt;
A: Each &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;put&lt;/code&gt; operation involves network calls to OCI Object Storage. For typical agent steps, this adds 50-200ms of latency per state access, which is acceptable for most conversational AI applications where LLM calls dominate latency (e.g., Groq 100ms, Claude 1-5s). For extremely high-throughput, low-latency scenarios, an in-memory cache with eventual consistency might be layered on top.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>LangGraph Checkpointing: Three Production Rewrites Before It Clicked</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Tue, 04 Aug 2026 19:30:15 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/langgraph-checkpointing-three-production-rewrites-before-it-clicked-41n</link>
      <guid>https://dev.to/elenarevicheva/langgraph-checkpointing-three-production-rewrites-before-it-clicked-41n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/langgraph-checkpointing-three-production-rewrites-before-it-clicked" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first LangGraph agent silently discarded every job for weeks. The &lt;code&gt;memory&lt;/code&gt; field in my &lt;code&gt;AgentState&lt;/code&gt; was defined as a &lt;code&gt;list[str]&lt;/code&gt;, but my agent was writing a &lt;code&gt;list[dict]&lt;/code&gt;. LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; didn't throw an error; it just truncated the state, leaving an empty list. I only found out when a customer complained their multi-step request never progressed past the first turn. The fix was a one-line schema change, but the cost was hours of debugging and a lost customer. This wasn't the only checkpointing pitfall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Schema Mismatch: &lt;code&gt;SqliteSaver&lt;/code&gt;'s Forgiveness
&lt;/h2&gt;

&lt;p&gt;My initial LangGraph setup used &lt;code&gt;SqliteSaver&lt;/code&gt; for checkpointing. It's simple, embedded, and seemed robust enough for early production. The agent's purpose was to process incoming requests from Telegram, break them down, and manage a multi-step conversation. The &lt;code&gt;AgentState&lt;/code&gt; looked something like this:&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat_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;thread_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;user_input&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;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# The culprit
&lt;/span&gt;    &lt;span class="n"&gt;current_step&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;# ... other fields
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's &lt;code&gt;memory&lt;/code&gt; field was intended to store a history of conversation snippets. My agent code, however, was designed to store more structured data, like &lt;code&gt;{"role": "user", "content": "..."}&lt;/code&gt;. So, instead of &lt;code&gt;list[str]&lt;/code&gt;, it was pushing &lt;code&gt;list[dict]&lt;/code&gt; into &lt;code&gt;memory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SqliteSaver&lt;/code&gt; uses &lt;code&gt;json.dumps&lt;/code&gt; to serialize the state into a BLOB. When &lt;code&gt;json.dumps&lt;/code&gt; encountered a &lt;code&gt;list[dict]&lt;/code&gt; where it expected a &lt;code&gt;list[str]&lt;/code&gt; (based on the &lt;em&gt;initial&lt;/em&gt; schema it inferred or was given), it didn't fail. Instead, it silently serialized the &lt;code&gt;list[dict]&lt;/code&gt; into a string. The &lt;em&gt;deserialization&lt;/em&gt; step was the problem. When &lt;code&gt;SqliteSaver&lt;/code&gt; loaded the state, it tried to deserialize that string back into a &lt;code&gt;list[str]&lt;/code&gt;. Since a string representation of &lt;code&gt;list[dict]&lt;/code&gt; is not a valid &lt;code&gt;list[str]&lt;/code&gt;, it often resulted in an empty list or a malformed object, effectively wiping out the conversation history for that specific field.&lt;/p&gt;

&lt;p&gt;The fix was to explicitly define the &lt;code&gt;memory&lt;/code&gt; field as &lt;code&gt;list[dict]&lt;/code&gt; in &lt;code&gt;AgentState&lt;/code&gt;. This highlighted a critical lesson: LangGraph's &lt;code&gt;SqliteSaver&lt;/code&gt; is forgiving to a fault. It prioritizes saving &lt;em&gt;something&lt;/em&gt; over strict schema validation during serialization, leading to silent data corruption on deserialization. For production, you need explicit validation or a more robust ORM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoint Corruption: The Race Condition with &lt;code&gt;SqliteSaver&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;After fixing the schema, I started seeing &lt;code&gt;sqlite3.OperationalError: database is locked&lt;/code&gt; errors. My agents run on Oracle Cloud Infrastructure (OCI) in a serverless function (OCI Functions) environment. Each incoming message triggers a new function invocation. While OCI Functions are stateless, my LangGraph agents needed state. &lt;code&gt;SqliteSaver&lt;/code&gt; writes to a file. In a serverless environment, this file needs to be externalized. I used OCI Object Storage to store the SQLite database file, mounting it via FUSE.&lt;/p&gt;

&lt;p&gt;The problem: multiple concurrent function invocations could try to write to the same SQLite file simultaneously. Even with FUSE, the underlying &lt;code&gt;sqlite3&lt;/code&gt; library isn't designed for concurrent writes from separate processes without proper locking mechanisms, which FUSE-mounted object storage doesn't natively provide at the database level. This led to checkpoint corruption. A database lock error would leave the SQLite file in an inconsistent state, making it unreadable for subsequent invocations.&lt;/p&gt;

&lt;p&gt;My solution was a hard pivot: &lt;code&gt;RedisSaver&lt;/code&gt;. Redis is designed for concurrent access and provides atomic operations. I deployed an OCI Cache with Redis and switched my &lt;code&gt;CheckpointSaver&lt;/code&gt; implementation.&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;langgraph.checkpoint.redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RedisSaver&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;

&lt;span class="c1"&gt;# ...
# In my agent initialization
&lt;/span&gt;&lt;span class="n"&gt;redis_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;REDIS_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RedisSaver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately resolved the &lt;code&gt;database is locked&lt;/code&gt; errors and checkpoint corruption. Redis's atomic &lt;code&gt;SET&lt;/code&gt; operations ensure that even if multiple invocations try to update the same checkpoint, one will succeed, and the others will get the latest state on their next read. The cost was an additional managed service (OCI Cache) at $25/month for a basic instance, but the stability was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Pattern That Made Multi-Step Pipelines Stable: Explicit State Transitions
&lt;/h2&gt;

&lt;p&gt;Even with &lt;code&gt;RedisSaver&lt;/code&gt;, my multi-step agents were still occasionally getting stuck. A user would send a message, the agent would process it, but the next step wouldn't trigger, or the agent would repeat the previous step. This wasn't a checkpointing issue per se, but a state management issue within LangGraph's graph execution.&lt;/p&gt;

&lt;p&gt;My initial graph design relied heavily on conditional edges that checked the &lt;em&gt;content&lt;/em&gt; of the &lt;code&gt;user_input&lt;/code&gt; or the &lt;em&gt;presence&lt;/em&gt; of certain fields in the state. For example:&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;route_next_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;state&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;user_input&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm&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;confirm_action&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;state&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;task_completed&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;notify_user&lt;/span&gt;&lt;span class="sh"&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;process_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach was brittle. If &lt;code&gt;user_input&lt;/code&gt; wasn't exactly "confirm", or if &lt;code&gt;task_completed&lt;/code&gt; was set but another condition also matched, the agent could loop or jump to the wrong node. The problem was that the state itself wasn't explicitly guiding the &lt;em&gt;transition&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The breakthrough came when I introduced an explicit &lt;code&gt;next_action&lt;/code&gt; field in my &lt;code&gt;AgentState&lt;/code&gt; and made every node responsible for setting it.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat_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;thread_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;user_input&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;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;current_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&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;process_input&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;confirm_action&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;notify_user&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;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Explicit state
&lt;/span&gt;    &lt;span class="n"&gt;next_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_input&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;confirm_action&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;notify_user&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;end&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;wait_for_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Guiding the graph
&lt;/span&gt;    &lt;span class="c1"&gt;# ... other fields
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, my conditional edges became much simpler and more robust:&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;route_next_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# In my graph definition:
&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;route_next_action&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;process_input&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;process_input_node&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;confirm_action&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;confirm_action_node&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;notify_user&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;notify_user_node&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;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_user&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;wait_for_user_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# A node that just waits for new input
&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;Each node's responsibility now included not just processing data, but also explicitly setting &lt;code&gt;next_action&lt;/code&gt; based on its outcome. For example, a &lt;code&gt;process_input_node&lt;/code&gt; might decide:&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;process_input_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... process input with LLM (Groq/Claude routing based on complexity)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requires_confirmation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_action&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;task_is_done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_action&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notify_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_action&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Wait for more user input
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern transformed my agents from fragile, implicit state machines into robust, explicit ones. The graph's flow became deterministic, driven by the &lt;code&gt;next_action&lt;/code&gt; field. It also made debugging significantly easier: I could inspect the &lt;code&gt;next_action&lt;/code&gt; in the checkpoint and immediately understand why the agent was transitioning (or not transitioning) to a particular node. This is crucial for production LangGraph stateful agents production checkpointing.&lt;/p&gt;

&lt;p&gt;This approach also naturally supports multi-turn conversations where the agent needs to wait for user input. The &lt;code&gt;wait_for_user&lt;/code&gt; action simply routes to a node that does nothing but return the state, effectively pausing the graph until new &lt;code&gt;user_input&lt;/code&gt; arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Robustness
&lt;/h2&gt;

&lt;p&gt;Building these agents with zero VC funding means every dollar counts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;OCI Functions:&lt;/strong&gt; $0.000015 per GB-second, $0.0000002 per invocation. My agents run for milliseconds, so compute cost is negligible (under $5/month for thousands of users).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Cache (Redis):&lt;/strong&gt; $25/month for a 1GB instance. This is the primary infrastructure cost for state.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;LLM APIs:&lt;/strong&gt; Groq (Llama 3 8B) for fast, simple tasks, Claude 3 Opus for complex reasoning. Groq costs are pennies per million tokens. Claude 3 Opus is $15/M input, $75/M output. My routing logic minimizes Opus use. Total LLM costs are typically $50-$150/month.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Object Storage:&lt;/strong&gt; $0.0255 per GB/month. Used for logs and occasional larger data blobs, not primary state. Negligible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The total infrastructure cost for running multiple production-grade LangGraph agents serving real users is under $200/month. The biggest cost was my time debugging the initial, fragile implementations. The shift to &lt;code&gt;RedisSaver&lt;/code&gt; and explicit state transitions reduced that debugging time significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not use a more robust database like PostgreSQL for checkpointing instead of Redis?&lt;/strong&gt;&lt;br&gt;
A: PostgreSQL offers stronger ACID guarantees and complex querying, but Redis provides lower latency for key-value lookups and atomic updates, which is ideal for frequent, small state changes in LangGraph checkpoints. For my use case, the simplicity and speed of Redis outweighed the need for a full relational database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you handle schema migrations for &lt;code&gt;AgentState&lt;/code&gt; in production with &lt;code&gt;RedisSaver&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A: &lt;code&gt;RedisSaver&lt;/code&gt; stores the state as a JSON string. For schema changes, I implement a versioning field in &lt;code&gt;AgentState&lt;/code&gt; and a migration function that runs on load. If the loaded state's version is older than the current agent's version, the migration function transforms the state to the new schema before the agent processes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's your strategy for routing between different LLMs (Groq, Claude) based on task complexity?&lt;/strong&gt;&lt;br&gt;
A: I use a small, fast LLM (e.g., Llama 3 8B on Groq) as a router. The router analyzes the user input and the current &lt;code&gt;AgentState&lt;/code&gt; to determine if the task requires complex reasoning (routing to Claude 3 Opus) or can be handled by a cheaper, faster model (routing to Groq). This decision is part of the &lt;code&gt;process_input_node&lt;/code&gt; logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you manage concurrent user interactions with a single agent instance?&lt;/strong&gt;&lt;br&gt;
A: Each user interaction (e.g., a Telegram chat ID) maps to a unique &lt;code&gt;thread_id&lt;/code&gt; in LangGraph. &lt;code&gt;RedisSaver&lt;/code&gt; stores checkpoints per &lt;code&gt;thread_id&lt;/code&gt;. When a new message comes in for an existing &lt;code&gt;thread_id&lt;/code&gt;, LangGraph loads the specific state for that thread, ensuring isolated conversations.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>My $12,000 AI Vendor Lock-in Mistakes: A Fractional CTO's Audit</title>
      <dc:creator>Elena Revicheva</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:30:14 +0000</pubDate>
      <link>https://dev.to/elenarevicheva/my-12000-ai-vendor-lock-in-mistakes-a-fractional-ctos-audit-39o1</link>
      <guid>https://dev.to/elenarevicheva/my-12000-ai-vendor-lock-in-mistakes-a-fractional-ctos-audit-39o1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aideazz.xyz/blog/my-12000-ai-vendor-lock-in-mistakes-a-fractional-ctos-audit" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; — cross-posted here with canonical link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent $12,000 last year on three vendor lock-ins I could have avoided. As a solo founder building multi-agent AI systems on Oracle Cloud, with zero VC funding, every dollar is a decision. These weren't "bad" choices at the time, but they became expensive traps. If I were auditing AIdeazz as a fractional CTO today, these are the first three contracts I'd scrutinize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM API Contract I Can't Escape: Anthropic's Claude 3 Opus
&lt;/h2&gt;

&lt;p&gt;My initial agent architecture relied heavily on Claude 3 Opus for complex reasoning tasks. Its context window and reasoning capabilities were, at the time, unparalleled for the price. I built my core orchestration layer, including agent introspection and planning, around its specific prompt formatting and tokenization. This was a mistake.&lt;/p&gt;

&lt;p&gt;The problem isn't Claude's quality; it's the lack of a true abstraction layer. My early code directly embedded Anthropic's API calls, specific &lt;code&gt;system&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt; message structures, and even relied on its unique handling of tool use. When Groq launched with Llama 3 8B and 70B, offering 500 tokens/second at a fraction of the cost, I was stuck. Rewriting the core agent logic to accommodate Groq's different prompt structure, tool calling conventions, and tokenization (which impacts cost calculations and context window management) would have taken weeks. Weeks I didn't have while shipping features.&lt;/p&gt;

&lt;p&gt;The cost impact is significant. A complex reasoning task that might cost $0.05 on Claude 3 Opus could run for $0.0005 on Groq's Llama 3 70B. My current monthly LLM spend is around $1,500. If 50% of that could shift to Groq, I'd save $750/month. Over a year, that's $9,000. This is the direct cost of not abstracting my LLM calls.&lt;/p&gt;

&lt;p&gt;A fractional CTO performing an AI vendor lock-in audit would immediately look for direct API calls to specific LLM providers within core business logic. The recommendation would be a &lt;code&gt;LLMProvider&lt;/code&gt; interface, even a simple one, that abstracts &lt;code&gt;generate_response(messages, tools, temperature, max_tokens)&lt;/code&gt; and handles provider-specific formatting internally. This allows hot-swapping providers without rewriting core agent behaviors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Choice That Costs More: Oracle Autonomous Database
&lt;/h2&gt;

&lt;p&gt;As an Oracle Cloud Infrastructure (OCI) user, the Oracle Autonomous Database (ADB) seemed like a natural fit. It's fully managed, scales automatically, and integrates deeply with other OCI services. For my vector store (using &lt;code&gt;pgvector&lt;/code&gt; on a PostgreSQL instance within ADB) and relational data, it offered simplicity.&lt;/p&gt;

&lt;p&gt;The simplicity came at a premium. My initial estimates for compute and storage were based on general-purpose PostgreSQL instances. ADB, while powerful, has a higher per-OCPU and per-GB cost. For a small, growing application, the "autonomous" features were overkill. I'm currently spending $300/month on ADB for a workload that could easily run on a self-managed PostgreSQL instance on an OCI VM for $50/month. That's $250/month in overspend, or $3,000 annually.&lt;/p&gt;

&lt;p&gt;The lock-in here isn't just cost; it's operational complexity. Migrating from ADB to a standard PostgreSQL instance, while technically feasible, involves setting up replication, managing backups, and configuring monitoring – tasks ADB handles automatically. This operational overhead, for a solo founder, is a real cost.&lt;/p&gt;

&lt;p&gt;A fractional CTO would question the "fully managed" premium for non-critical workloads. For AI agents, especially those with high-throughput but low-latency requirements, a simpler, cheaper database solution often suffices. An audit would recommend evaluating the actual usage patterns (CPU, IOPS, storage) against the cost of a self-managed alternative or a cheaper managed service like OCI's MySQL HeatWave or even a simple VM with PostgreSQL. The decision should be driven by actual performance requirements, not perceived convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Infra Bet That Aged Badly: OCI Functions for Agent Orchestration
&lt;/h2&gt;

&lt;p&gt;When I started, OCI Functions (serverless functions) seemed ideal for stateless agent orchestration. Each agent step could be a function call, scaling on demand, paying only for execution time. This worked well for simple, sequential workflows.&lt;/p&gt;

&lt;p&gt;The problem emerged with complex, stateful multi-agent systems. My agents often require persistent context across multiple turns, inter-agent communication, and long-running processes. OCI Functions are designed for short-lived, stateless execution. I ended up passing large JSON payloads between functions to maintain state, leading to increased latency, higher invocation costs (due to larger payloads and more invocations), and a debugging nightmare.&lt;/p&gt;

&lt;p&gt;My current monthly OCI Functions bill is around $100. This might seem small, but the hidden cost is developer time and architectural complexity. I've spent countless hours debugging state serialization issues and optimizing function cold starts. If I had built this on a persistent compute instance (e.g., an OCI VM with a Python application server like FastAPI), the compute cost would be similar, but the development and debugging overhead would be drastically reduced.&lt;/p&gt;

&lt;p&gt;A fractional CTO would identify the mismatch between the serverless paradigm and the stateful nature of multi-agent systems. An audit would recommend moving core agent orchestration to a persistent compute layer (VMs, Kubernetes, or even OCI Container Instances) where state can be managed more naturally. Serverless functions are still excellent for event-driven triggers, webhooks, or lightweight, independent tasks, but not for the heart of a complex AI system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fractional CTO AI Vendor Lock-in Audit Checklist
&lt;/h2&gt;

&lt;p&gt;Based on my mistakes, here's a quick checklist a fractional CTO should run through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LLM Abstraction Layer:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Are LLM API calls directly embedded in core logic? (Red flag)&lt;/li&gt;
&lt;li&gt;  Is there an interface or wrapper that allows swapping LLM providers with minimal code changes?&lt;/li&gt;
&lt;li&gt;  Are prompt templates and tokenization handled generically or tied to a specific provider?&lt;/li&gt;
&lt;li&gt;  What's the cost difference if you switch 50% of your traffic to a cheaper, equivalent LLM?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Database Cost vs. Need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Is a fully managed, premium database (e.g., Oracle ADB, AWS Aurora) being used for non-critical or low-traffic data?&lt;/li&gt;
&lt;li&gt;  What are the actual CPU, memory, and IOPS requirements?&lt;/li&gt;
&lt;li&gt;  What's the cost of a self-managed alternative or a cheaper managed service for the same workload?&lt;/li&gt;
&lt;li&gt;  What's the operational overhead of migrating to a cheaper option, and does it outweigh the savings?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compute Paradigm for AI Orchestration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Are serverless functions (e.g., OCI Functions, AWS Lambda) being used for stateful, long-running, or complex multi-step AI workflows? (Red flag)&lt;/li&gt;
&lt;li&gt;  How is state managed between serverless invocations? (Large payloads, external stores = complexity)&lt;/li&gt;
&lt;li&gt;  What's the latency impact of cold starts and inter-function communication?&lt;/li&gt;
&lt;li&gt;  Would a persistent compute instance (VM, container) simplify the architecture and reduce development overhead, even if the raw compute cost is similar?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These questions aren't about avoiding specific vendors entirely. They're about making conscious decisions about where lock-in is acceptable and where it becomes a costly liability. My $12,000 lesson taught me that convenience often has a hidden price tag, especially when building lean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is it always bad to use a specific LLM provider's unique features if it offers a significant advantage?&lt;/strong&gt;&lt;br&gt;
A: Not always, but it's a calculated risk. If a feature (e.g., specific tool calling, vision capabilities) provides a critical, unique advantage, weigh that against the cost of migration if a better or cheaper alternative emerges. Build an abstraction layer around that specific feature, acknowledging the lock-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do you balance the convenience of a fully managed database with cost savings from self-hosting?&lt;/strong&gt;&lt;br&gt;
A: For early-stage products or solo founders, the operational overhead of self-hosting can be a significant hidden cost. Start with a managed service, but continuously monitor usage and cost. Once you hit a certain scale or cost threshold (e.g., $200/month), re-evaluate if the savings from self-hosting outweigh the time spent on maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When are serverless functions appropriate for AI workloads?&lt;/strong&gt;&lt;br&gt;
A: Serverless functions are excellent for event-driven tasks: processing incoming webhooks, image resizing for vision models, pre-processing data for an LLM, or triggering agent workflows based on external events. They are less suitable for the core, stateful orchestration of multi-agent systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the first step to mitigate existing vendor lock-in?&lt;/strong&gt;&lt;br&gt;
A: Identify the most expensive or most critical lock-in point. For LLMs, start by creating a simple wrapper interface for your most common API calls. For databases, analyze your actual usage to determine if a cheaper alternative meets performance needs. Don't try to fix everything at once.&lt;/p&gt;

&lt;p&gt;— Elena Revicheva · &lt;a href="https://aideazz.xyz" rel="noopener noreferrer"&gt;AIdeazz&lt;/a&gt; · &lt;a href="https://aideazz.xyz/portfolio" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
