<?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: Kartik N V J K</title>
    <description>The latest articles on DEV Community by Kartik N V J K (@kartik-nvjk).</description>
    <link>https://dev.to/kartik-nvjk</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%2F3977738%2F99b8e069-3afe-49cf-a409-0f52b82c22b6.jpg</url>
      <title>DEV Community: Kartik N V J K</title>
      <link>https://dev.to/kartik-nvjk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kartik-nvjk"/>
    <language>en</language>
    <item>
      <title>LlamaIndex makes RAG easy to build and hard to debug. Here is how I evaluate it.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:34:35 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/llamaindex-makes-rag-easy-to-build-and-hard-to-debug-here-is-how-i-evaluate-it-188b</link>
      <guid>https://dev.to/kartik-nvjk/llamaindex-makes-rag-easy-to-build-and-hard-to-debug-here-is-how-i-evaluate-it-188b</guid>
      <description>&lt;p&gt;I built a RAG app on LlamaIndex in about four lines. Wire an index to a query engine, point it at my documents, ask a question, get an answer. The first hundred queries were great. I was impressed with how little code it took.&lt;/p&gt;

&lt;p&gt;Then a user asked something my little setup had never been tested on. The answer came back confident and wrong. I checked my eval, which reported a single faithfulness score, and all it told me was that faithfulness had dropped. Not which part of the pipeline broke. Not why. Just a number going down.&lt;/p&gt;

&lt;p&gt;That was the moment I understood the trap. LlamaIndex makes RAG incredibly easy to assemble, and that ease hides how many separate moving parts you just wired together. One quality score cannot debug a thing made of five different pieces. Here is how I evaluate it now, layer by layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one RAG score is not enough here
&lt;/h2&gt;

&lt;p&gt;Plain RAG is basically two steps: retrieve some chunks, write an answer from them. Two steps means two things to check, and one score almost covers it.&lt;/p&gt;

&lt;p&gt;LlamaIndex is rarely that flat. The interesting apps compose. You add a router that picks between several engines. You add a step that breaks a big question into smaller ones. You pick a synthesizer mode that decides how the final answer gets assembled. You maybe turn the whole thing into an agent that calls tools. Every one of those pieces is its own little component with its own way of failing.&lt;/p&gt;

&lt;p&gt;So when a composed pipeline gives a wrong answer, the single faithfulness score is measuring the very end of a long chain and telling you nothing about where the chain broke. The fix is to stop scoring the program as one blob and start scoring each piece against its own job. I think of it as four layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer one: score the retrieval on its own
&lt;/h2&gt;

&lt;p&gt;The first layer is the retrieval, scored completely independently of the answer that comes after it. This is the upstream signal. If retrieval is broken, every later score will look bad too, so this is where the debugging should start.&lt;/p&gt;

&lt;p&gt;Three things I check on the retrieved chunks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Are the chunks actually relevant to the question?&lt;/strong&gt; This catches the classic case where the search grabbed a passage that shares words with the query but means something different. You asked about Section 12, it handed you Section 9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the chunks actually support the claims in the answer?&lt;/strong&gt; This catches invented citations, where the answer points at a chunk that does not say what the answer claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the answer even use the chunks it fetched?&lt;/strong&gt; This one surprised me. If you fetch ten chunks and the answer uses two, you are over-fetching, and that is both wasteful and a sign your retrieval settings are loose. The fix is usually a reranker before the answer step, or just pulling fewer chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that made this useful: score retrieval per retriever, not per app. Different retrieval methods fail in different shapes, and tagging the score by which retriever ran means a regression points straight at the one that moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer two: score the routing and the decomposition
&lt;/h2&gt;

&lt;p&gt;This is the layer generic RAG eval completely misses, and it is where two of my nastiest bugs lived.&lt;/p&gt;

&lt;p&gt;If you use a router that picks between engines, you have to score the routing decision on its own. Here is why it is sneaky: if the router sends a question to the wrong engine, the chunks that engine retrieves are still relevant to whatever it retrieved, and the answer is still grounded in them. Both your normal scores look fine. The answer is wrong purely because the wrong engine ran. So I score routing as its own question: given the query and the choices, did it pick the right engine? A right answer from the wrong engine is luck, and the next harder question will expose it.&lt;/p&gt;

&lt;p&gt;If you use a step that breaks a big question into smaller ones, score the decomposition too. The failure I hit here was a dropped condition. A user asked for X under a specific condition Y. The decomposer split it into "tell me about X" and "tell me about Y" as two separate questions, and the final answer lost the "under condition Y" part entirely. Each sub-question was answered perfectly. The combined answer was still wrong, because the join between them got lost.&lt;/p&gt;

&lt;p&gt;So for a decomposing engine I score each sub-question on its own, then score the final merged answer on completeness against all of them together. If the sub-questions score well but the final answer does not, the merge step is dropping something. If a single sub-question scores badly, that branch is your problem. Same data, two clearly different bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer three: watch which synthesizer mode you are using
&lt;/h2&gt;

&lt;p&gt;LlamaIndex gives you a few ways to assemble the final answer from the retrieved chunks, and they genuinely behave differently. One mode stuffs everything into a single call. Another refines the answer chunk by chunk. Another summarizes in a tree.&lt;/p&gt;

&lt;p&gt;The one that bit me: the tree-style summarizer drops citations on long contexts that the simpler mode handled fine. If you switch modes in a deploy and only score the final answer, all you see is that quality dropped after Tuesday, with no hint that the synthesizer mode was the cause. So I treat the synthesizer mode as a thing worth scoring on its own, especially citation validity, and I re-check it whenever the mode or the context length changes. Long-context behavior is exactly where these modes diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer four: run the same checks in production, not just in CI
&lt;/h2&gt;

&lt;p&gt;Your offline eval catches the failures you thought to test. Production catches the ones you did not. So the same four layers of checks that run before release should also run against real traffic after release.&lt;/p&gt;

&lt;p&gt;You do not have to score everything. The cheap checks, like whether a citation actually exists, can run on every request. The expensive judge-based ones can run on a small sample of live traffic. Then you watch for a sustained drop in any one of them per route.&lt;/p&gt;

&lt;p&gt;The most useful signal turned out to be the gap between my offline scores and my live scores. When they agree, my test set still looks like reality. When they drift apart, my test set has gone stale and is no longer testing what users are actually asking. That gap is worth watching on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on sensitive data
&lt;/h2&gt;

&lt;p&gt;If your app retrieves over anything sensitive, like medical, legal, or financial documents, the same checks that score offline should also gate at request time. Run them before the answer goes out, and if the answer is not grounded in what was retrieved, fall back to a safe response instead of shipping the confident wrong one. It is the same rubric, just used as a gate instead of a report. For high-stakes paths I make it strict, every check has to pass. For casual ones, a majority is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes that ship LlamaIndex regressions quietly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One faithfulness score for the whole app.&lt;/strong&gt; It tells you something broke, never which piece.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not scoring the router.&lt;/strong&gt; A wrong-engine answer looks perfectly grounded, so it hides in plain sight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not scoring the decomposition.&lt;/strong&gt; The dropped-condition bug passes every per-sub-question check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the synthesizer mode.&lt;/strong&gt; Switching modes can drop citations with no other visible cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-fetching and never checking.&lt;/strong&gt; If the answer ignores most of what you retrieved, you are paying for chunks that do nothing and loosening your relevance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-only eval.&lt;/strong&gt; Production drifts past a frozen test set within a quarter. Score live traffic too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I keep coming back to is that LlamaIndex's biggest strength, how easily it composes primitives, is also what makes it hard to debug. Every primitive you add is another place a failure can hide, and a single score averages all of them into one useless number. Once I scored each layer on its own, the bug that had been invisible for a week was obvious in an afternoon.&lt;/p&gt;

&lt;p&gt;If you want the deeper version, with the exact rubric for each layer and how to wire routing and decomposition checks specifically, &lt;a href="https://futureagi.com/blog/evaluating-llamaindex-rag-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; walks through all four layers in detail.&lt;/p&gt;

&lt;p&gt;If you run LlamaIndex in production, I am curious which primitive broke on you first. For me it was the router, an answer that was perfectly grounded in evidence the wrong engine went and fetched.&lt;/p&gt;

</description>
      <category>llamaindex</category>
      <category>rag</category>
      <category>llm</category>
      <category>testing</category>
    </item>
    <item>
      <title>My DSPy pipeline compiled beautifully and got worse in production</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:16:15 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/my-dspy-pipeline-compiled-beautifully-and-got-worse-in-production-1hk7</link>
      <guid>https://dev.to/kartik-nvjk/my-dspy-pipeline-compiled-beautifully-and-got-worse-in-production-1hk7</guid>
      <description>&lt;p&gt;I love the DSPy idea. You stop hand-editing prompts and let the compiler optimize them for you against a metric and a training set. I built a RAG pipeline that way, the compile score came out high, the hold-out looked fine, and I shipped it.&lt;/p&gt;

&lt;p&gt;Live traffic quality was worse than what I had before. Nothing errored. The numbers I had been watching just did not match what users were getting.&lt;/p&gt;

&lt;p&gt;The problem was not DSPy. It was what I was optimizing against and what I was measuring. Here is the short version of what I learned.&lt;/p&gt;

&lt;p&gt;The metric you compile against is not your production rubric&lt;/p&gt;

&lt;p&gt;DSPy optimizes your prompts against a metric you give the compiler. That metric has to be cheap, because the compiler scores thousands of trial prompts in a single pass. So in practice it is something thin: does the final answer contain the expected string, a single yes-or-no judge call, that kind of thing.&lt;/p&gt;

&lt;p&gt;But your product is not judged on a thin metric. A real RAG answer needs to be grounded in what was retrieved, complete on multi-part questions, and willing to refuse when the answer is not there. None of that fits into the cheap check the compiler runs five thousand times.&lt;/p&gt;

&lt;p&gt;So when the cheap compile metric and the real rubric disagree, the compiler happily overfits the cheap one. It gets very good at the thing you told it to measure, which was never quite the thing you actually cared about. That is exactly how a pipeline scores high at compile time and ships worse.&lt;/p&gt;

&lt;p&gt;The fix is not a cleverer cheap metric. It is to keep the cheap metric where it belongs, inside the compile loop, and run the real, rich rubric separately, on your hold-out set and on live traffic.&lt;/p&gt;

&lt;p&gt;Score the Signature, not the whole program&lt;/p&gt;

&lt;p&gt;The second thing I got wrong: I scored the pipeline end to end, one number for the whole thing.&lt;/p&gt;

&lt;p&gt;A DSPy program is a few modules chained together, each with its own little job. Mine had a retrieval step and an answer step. When the single end-to-end score dropped, it told me the program got worse and absolutely nothing about which step caused it. I was left guessing, and I guessed wrong for a while.&lt;/p&gt;

&lt;p&gt;What actually works is scoring each module against its own job:&lt;/p&gt;

&lt;p&gt;The retrieval step gets judged on whether it fetched the right material.&lt;br&gt;
The answer step gets judged on whether it stayed grounded in that material and answered the whole question.&lt;/p&gt;

&lt;p&gt;Now when the program regresses, the module scores point straight at the culprit. If retrieval tanks while the answer step holds, I know the compiler produced a bad retrieval prompt and I fix that one. The end-to-end number could never tell me that.&lt;/p&gt;

&lt;p&gt;Every module can pass and the program still fail&lt;/p&gt;

&lt;p&gt;Here is the sneaky one. Sometimes each module scores fine on its own and the program is still wrong, because the composition lost something between the steps. Retrieval fetched the right passages. The answer step reasoned fine over what it got. The final answer was still off.&lt;/p&gt;

&lt;p&gt;So I also keep one check that looks at the whole run and asks, when the final answer is wrong, which step was the proximate cause. Run that across a handful of failing cases and you get an actual distribution instead of a hunch. If most of the failures trace back to retrieval, you fix retrieval first. If they trace back to the answer step despite it scoring well in isolation, that is your real weak link.&lt;/p&gt;

&lt;p&gt;What I do now, in one line&lt;/p&gt;

&lt;p&gt;Compile with the cheap metric, but never trust it as the verdict. Judge each module on its own job with the real rubric, keep one check for where the cascade breaks, and run all of it on a fresh hold-out and a slice of live traffic, not just on the set the compiler already saw. The moment I split the score up that way, the regression that had been invisible was obvious.&lt;/p&gt;

&lt;p&gt;If you want the deeper version, with the exact per-module rubrics and how to wire the compile-versus-production comparison into CI, this piece goes through it step by step.&lt;/p&gt;

&lt;p&gt;If you run DSPy in production, I am curious whether your compile scores and your live scores ever drifted apart. Mine did, quietly, and the gap was the whole lesson.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>rag</category>
    </item>
    <item>
      <title>AWS Bedrock's built-in eval graded my agent green. It only looks at a quarter of it.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/aws-bedrocks-built-in-eval-graded-my-agent-green-it-only-looks-at-a-quarter-of-it-52d0</link>
      <guid>https://dev.to/kartik-nvjk/aws-bedrocks-built-in-eval-graded-my-agent-green-it-only-looks-at-a-quarter-of-it-52d0</guid>
      <description>&lt;p&gt;I built an agent on AWS Bedrock the low-code way. Pick a foundation model, wire an action group to a Lambda, attach a Knowledge Base, add a Guardrail. Then I ran Bedrock's built-in Model Evaluation job, got good scores, and shipped.&lt;/p&gt;

&lt;p&gt;Production disagreed almost immediately. The agent was dropping a chunk of its tool calls on a task the built-in eval had passed. The model was fine. Everything wrapped around the model was not, and the built-in check could not see any of it.&lt;/p&gt;

&lt;p&gt;Here is what I learned about actually evaluating a Bedrock agent, in plain terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in eval scores the model, and your agent is four things
&lt;/h2&gt;

&lt;p&gt;This is the core mistake, and it is baked into the tool. Bedrock's Model Evaluation job runs a dataset against one foundation model and scores it for accuracy and safety. That is a fine sanity check on the raw model.&lt;/p&gt;

&lt;p&gt;But a Bedrock agent is not just a model. It is a model, plus action groups (the tool-call layer that runs your Lambdas), plus a Knowledge Base (the retrieval layer), plus Guardrails (the safety layer). The built-in eval sees the first piece and is blind to the other three. So three-quarters of what your user actually experiences never gets graded.&lt;/p&gt;

&lt;p&gt;It also runs once, as a one-shot job. There is no path from "I re-indexed the Knowledge Base" or "I tweaked the Guardrail" back into a check that gates your next deploy. Production agents need the eval to run on every change, not once at the start.&lt;/p&gt;

&lt;p&gt;So the fix is to stop grading the model and start grading the three layers around it. Three axes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis one: did it call the right tool, correctly
&lt;/h2&gt;

&lt;p&gt;Action groups are where my agent broke first. Getting this right means three separate things, and all three matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Did it pick the right tool, including picking none?&lt;/strong&gt; This is the one people forget. You need test cases where the correct answer is to not call any tool at all. Without those, the failure where a prompt tweak makes the agent call tools too eagerly stays invisible until users complain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Were the arguments actually right?&lt;/strong&gt; Not just valid against the schema, but semantically correct. A date like "2026-01-01" can be perfectly valid and still wrong if the user said "next Friday." Schema-valid and correct are two different checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did it use what the tool returned?&lt;/strong&gt; I hit exactly this. A tool returned a real account balance, and the model answered without reading it, just making up a plausible number. If the agent ignores its own tool output and falls back on invented knowledge, every earlier check can pass and the answer is still wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Axis two: did it retrieve the right documents
&lt;/h2&gt;

&lt;p&gt;If you use a Bedrock Knowledge Base, the retrieval happens inside the agent run, which means a bad-retrieval bug looks exactly like a bad-model bug unless you score retrieval on its own.&lt;/p&gt;

&lt;p&gt;So I score the retrieval step separately from the final answer. Build a small set of questions with the specific documents that should come back for each, and check how often the right ones actually land in the results. Then check the answer separately. Now you can tell the two apart: if retrieval is bad, fix the chunking or the embeddings; if retrieval is good but the answer is ungrounded, fix the prompt or the model. Without the split you spend a week tuning the wrong layer.&lt;/p&gt;

&lt;p&gt;Two Bedrock gotchas worth knowing before they bite you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changing the embedding model quietly drops your recall.&lt;/strong&gt; Swapping the embeddings behind a Knowledge Base reshuffles which chunks come back, and I have seen it drop retrieval quality by roughly ten points with nothing else changed. Re-run the retrieval check after any embedding change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-English content degrades silently.&lt;/strong&gt; If your docs are multilingual, tag each test case by language and watch the non-English subsets on their own. The regression where German or Hindi retrieval quietly falls behind English is one of the most common and least noticed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Axis three: are the guardrails catching attacks without blocking real users
&lt;/h2&gt;

&lt;p&gt;Bedrock Guardrails fire on word lists and rules. They are fast and predictable, and they fail in two opposite directions at once. They miss attacks that are phrased around the filter, and they block legitimate queries you forgot to test.&lt;/p&gt;

&lt;p&gt;So I score them on two labelled sets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A benign set&lt;/strong&gt; of normal queries that should pass. What fraction did the Guardrail wrongly block? That is your false-block rate, and it is quietly making your support queue longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An adversarial set&lt;/strong&gt; of jailbreaks, injections, and real policy violations that should be blocked. What fraction did it actually catch?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Report both, per Guardrail version. A policy that catches almost every attack but blocks a tenth of real users is not obviously better than one that lets a few more through but rarely annoys anyone. The eval just shows you the trade-off honestly so you can pick the operating point on purpose instead of by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate on each axis, not on one number
&lt;/h2&gt;

&lt;p&gt;The tempting move is to average everything into one agent score and gate on that. Do not. An aggregate of 0.85 can easily hide a 0.62 on parameter correctness behind a 0.97 on tool selection, and the production failure rides on the weak one.&lt;/p&gt;

&lt;p&gt;So I set a separate threshold per axis: tool selection, parameter correctness, retrieval quality, groundedness, guardrail precision, guardrail recall. When the gate fails, the name of the failing check is the root cause. One bisect instead of three days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that is unique to Bedrock: the model swap
&lt;/h2&gt;

&lt;p&gt;Bedrock makes changing the model behind your agent a one-line change. That is convenient and it is a trap. A pass on one model is not a pass on another. I have watched one model handle an action group perfectly while another dropped it on the same input. So run the same suite against every model your agent could actually resolve to, not just the one you developed against.&lt;/p&gt;

&lt;p&gt;The mistakes I would warn a friend about, all learned the hard way: trusting the built-in eval as the agent eval, testing on one model when you swap models in production, treating the Knowledge Base as a black box instead of scoring retrieval on its own, and scoring a Guardrail on attacks only while never checking how much real traffic it blocks.&lt;/p&gt;

&lt;p&gt;The lesson I keep coming back to is that Bedrock's low-code assembly hides how many moving parts your agent actually has, and the built-in eval only looks at one of them. Once I scored the three layers around the model, the failures that had been slipping to production were obvious before the deploy.&lt;/p&gt;

&lt;p&gt;If you want the deeper version, with the exact retrieval and guardrail rubrics and how to wire the per-axis gate into CI, &lt;a href="https://futureagi.com/blog/evaluating-aws-bedrock-agents-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; goes through all three axes in detail.&lt;/p&gt;

&lt;p&gt;If you run Bedrock agents, I am curious which layer broke on you first. For me it was action groups, an agent that called the right tool and then ignored what it returned.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
    </item>
    <item>
      <title>I uninstalled three AI coding CLIs in a week. The model was never the problem.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Wed, 26 Aug 2026 10:56:18 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/i-uninstalled-three-ai-coding-clis-in-a-week-the-model-was-never-the-problem-2la1</link>
      <guid>https://dev.to/kartik-nvjk/i-uninstalled-three-ai-coding-clis-in-a-week-the-model-was-never-the-problem-2la1</guid>
      <description>&lt;p&gt;I tried a bunch of terminal coding agents last month. Some I reopened every morning. Some I uninstalled by Friday. What surprised me is that the ones I dropped were not worse at writing code. The model quality was roughly the same across all of them.&lt;/p&gt;

&lt;p&gt;The difference was entirely in how the tool showed me its work. Once I noticed that, picking one got easy, because it stopped being a question about the model and became a question about three things the CLI either does or does not do.&lt;/p&gt;

&lt;p&gt;The short version: an agent CLI is a UX problem wearing an LLM costume. Here are the three things I check now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it show me the plan before it touches anything?
&lt;/h2&gt;

&lt;p&gt;The CLIs I kept all do the same thing first. Before writing a single file, they print what they intend to do: which files they will read, what change they will make, which commands they will run, and then they wait.&lt;/p&gt;

&lt;p&gt;The ones I dropped just start. You type the task, it prints "working..." and it is already editing files. By the time you can see where it is going, it is two files deep in the wrong direction and you are cleaning up.&lt;/p&gt;

&lt;p&gt;The reason plan-first wins is simple. An agent run is not one answer, it is a stack of changes you cannot easily undo. Reviewing that stack at the end is reviewing a crash. Reviewing it up front is just engineering. And the plan has to be editable, not just visible. Being able to say "skip step three, run the tests this other way" is what turns the tool into something you steer instead of babysit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it name every action, or hide it behind a spinner?
&lt;/h2&gt;

&lt;p&gt;The second thing separates the trustworthy CLIs from the rest completely.&lt;/p&gt;

&lt;p&gt;The bad pattern is a spinner and then "done, three files changed." What did it actually do? Did it edit the right files? Did it run a shell command? Did it delete something? You read the final diff and hope, and if the diff is wrong you have no idea which of forty steps caused it.&lt;/p&gt;

&lt;p&gt;The good pattern narrates as it goes. It names each file it reads. It shows a diff before it writes, so you can accept or reject. It prints the full shell command it is about to run, not "running command." When something fails, it tells you which tool failed and why, instead of quietly retrying and handing you a wrong answer at the end. That running commentary is the entire difference between an agent I let run on its own and one I have to watch keystroke by keystroke.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it goes wrong, can I undo cleanly?
&lt;/h2&gt;

&lt;p&gt;The third thing is where most of them fall apart, and it is the one that decides whether you trust the tool on a real codebase.&lt;/p&gt;

&lt;p&gt;Three levels matter, and I want all three. I want to reject one bad step without killing the whole run, so the agent adapts and keeps going. I want to undo everything from a session with one command when it goes truly sideways. And ideally I want to restart from step six of an eight-step run with a corrected instruction, instead of starting over.&lt;/p&gt;

&lt;p&gt;The CLIs that get this right usually lean on git under the hood, treating each change as a commit so undo is a real, first-class action. The ones that get it wrong leave you doing git stash and manually inspecting a dozen modified files to figure out what it touched. If your tool cannot cleanly undo a whole run, treat it as a toy for experiments, not something you point at production code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually pick now
&lt;/h2&gt;

&lt;p&gt;The thing I stopped doing was choosing a CLI based on which model sits behind it. The model is a setting. You can swap it. The way the tool surfaces its work is what you live inside every single day, and that is sticky.&lt;/p&gt;

&lt;p&gt;So my test is just those three questions, run against my own real tasks, not a demo: does it show me an editable plan, does it name every action as it happens, and can I undo a run without a fight. A tool that does all three earns a place in my terminal. A tool that misses even one quietly trains me to distrust it, and that is when it gets uninstalled.&lt;/p&gt;

&lt;p&gt;If you want the fuller version, with a five-task scoring rubric and how the same three axes hold up when you run these things in CI, &lt;a href="https://futureagi.com/blog/agent-cli-developer-experience-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; lays it out.&lt;/p&gt;

&lt;p&gt;I am curious which one you settled on and why. And more specifically: did you pick it for the model, or for the way it behaves in the terminal? I keep meeting people who chose on the model and quietly regret it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>codingagents</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Choosing the Right Voice Agent Testing Platform: 14 Questions to Ask</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Thu, 20 Aug 2026 14:10:16 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/choosing-the-right-voice-agent-testing-platform-14-questions-to-ask-4e17</link>
      <guid>https://dev.to/kartik-nvjk/choosing-the-right-voice-agent-testing-platform-14-questions-to-ask-4e17</guid>
      <description>&lt;p&gt;Every voice agent I have shipped demoed perfectly. Then a real caller talked over it, changed their mind twice, asked something off-script, and the agent confidently booked the wrong appointment. None of that showed up in testing, because testing was a handful of clean, single-turn prompts. The conversation is where voice agents break, and it is the exact thing most test setups never touch.&lt;/p&gt;

&lt;p&gt;Whatever you use to test a voice agent, whether you buy it or build it, its job is to drive the agent through realistic, messy, multi-turn conversations before a customer does. Anything less is shipping on vibes. This is &lt;a href="https://futureagi.com/blog/choosing-x-tools-y-questions/choosing-voice-agent-testing-platform/?utm_source=fagiHashnode&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;based on a longer piece on the Future AGI blog&lt;/a&gt;; here is the checklist I actually run, and why each item earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why picking a voice test setup is harder than it looks
&lt;/h2&gt;

&lt;p&gt;A voice agent holds state across turns, handles interruptions, recovers from its own mistakes, and is supposed to stay on persona for the whole call. A test that sends one prompt and checks one response measures none of that. So the thing I want is not a recorder that replays a golden script. It is a simulator that generates realistic users, pushes the agent the way real callers do, and scores the whole trajectory. The checklist below is really one idea broken into pieces: test the conversation, not the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I run before I trust one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it simulate real users, or just replay a script?&lt;/strong&gt; Real callers are not deterministic. A script runner only proves the agent handles the script. I want synthetic personas with traits that drive behaviour across the whole call, not a fixed list of recorded prompts dressed up as testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it test multi-turn conversations, not single turns?&lt;/strong&gt; Voice agents fail on turn four, not turn one. State, memory, and recovery only show up across turns, so the unit of testing has to be the whole conversation with goals and expectations per turn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it cover adversarial and edge-case users?&lt;/strong&gt; The happy path always passes. Production breaks on the caller who interrupts, pushes back, and drops off-script. If the test users are all cooperative, the setup is lying to me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can it auto-generate diverse scenarios at scale?&lt;/strong&gt; Hand-writing two hundred scenarios never actually happens, so coverage stays thin and the same three cases get tested forever. I want to generate diverse, realistic scenarios from a seed description instead of writing each one by hand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it work with the framework I built the agent on?&lt;/strong&gt; A tester that supports one framework forces a rewrite or gets abandoned. I look for adapters across the major agent frameworks so I can point the simulator at the agent I already have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it give a pass/fail verdict and a transcript per conversation?&lt;/strong&gt; "The agent seems fine" is not a test result. I need a verdict I can gate a release on, and the full transcript to debug a failure, not an aggregate feeling with no per-conversation detail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it assert expected behaviour turn by turn?&lt;/strong&gt; A conversation can reach the goal while doing something wrong on the way, like leaking data or breaking a policy. I want per-turn assertions that flag the exact turn it went wrong, not just a final outcome check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it score conversation quality, not just task completion?&lt;/strong&gt; An agent can finish the task rudely, off-brand, or with a hallucinated detail. Completion is not quality, so I want coherence, persona consistency, and groundedness scored across the call on top of task success.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it score tone directly, and have a path to audio?&lt;/strong&gt; A voice agent can hit the goal and still sound clipped, robotic, or tonally wrong for a caller who is already angry. I want tone scored as its own signal I can assert on per turn, not averaged into one blended quality number, plus a way to score audio quality like naturalness, pacing, and latency once real speech is in play.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it scale to many persona and scenario combinations?&lt;/strong&gt; Real coverage is personas times scenarios. A setup that runs them one at a time is not a release gate. I want a runner that executes every combination and aggregates the results into one number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it integrate with evaluation and tracing?&lt;/strong&gt; A failed conversation is a starting point, not an answer. I need to jump from the verdict to the scores and the trace to find the cause, which does not happen if the tool's results live nowhere near my observability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it turn failed conversations into grouped, root-caused issues?&lt;/strong&gt; Eighty failed transcripts is noise. What I actually need is the three underlying causes behind them, ranked, so I fix the bug and not the symptom. A tool that stops at "here are the failures" leaves the hardest part on my desk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it have a path to real voice testing when I need it?&lt;/strong&gt; Most failures are conversational logic I can catch in simulation, but real speech adds latency and audio-quality failure modes that text cannot surface. I want an honest account of how mature that speech path is, not a text-only tool sold as voice testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does it run before production, in CI, not just after?&lt;/strong&gt; A tool I run manually after an incident is a post-mortem, not a test. The point is to block the bad release, so I want a programmatic path that wires into CI and lets the pass rate gate the deploy the same way unit tests do.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How I weight these depending on what I am shipping
&lt;/h2&gt;

&lt;p&gt;Not every item matters equally on every project. If the agent handles open-ended conversations with real users, items one, two, and three decide it. If I need real coverage rather than three hand-written cases, four and ten matter most. If "it finished the task" is not a high enough bar for the brand or a compliance team, eight, nine, and eleven carry the weight. If I need to close the loop on failures fast rather than just find them, twelve is the one. And if latency and audio quality are actual launch criteria, thirteen moves to the top, and I validate that speech path early because it tends to be the newest and least proven part of any setup.&lt;/p&gt;

&lt;p&gt;The teams shipping reliable voice agents stopped testing clean single-turn prompts and started simulating messy, multi-turn callers. The whole checklist collapses back to that. Test the conversation, not the script, and gate the release on the number that comes out.&lt;/p&gt;

</description>
      <category>voiceai</category>
      <category>ai</category>
      <category>simulation</category>
      <category>voicetesting</category>
    </item>
    <item>
      <title>Every agent on my team passed its own tests. The team still shipped wrong answers.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Wed, 12 Aug 2026 13:55:00 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/every-agent-on-my-team-passed-its-own-tests-the-team-still-shipped-wrong-answers-1hff</link>
      <guid>https://dev.to/kartik-nvjk/every-agent-on-my-team-passed-its-own-tests-the-team-still-shipped-wrong-answers-1hff</guid>
      <description>&lt;p&gt;I had a three-agent setup: a planner, a researcher, and a critic passing work between them. I graded each one carefully. The planner made clean plans. The researcher cited its sources correctly. The critic caught weak claims. Every agent scored around 0.9 on its own tests, and I was happy.&lt;/p&gt;

&lt;p&gt;The team was still wrong about a third of the time.&lt;/p&gt;

&lt;p&gt;The plans were good. The research was good. The critiques were good. But somewhere between one agent and the next, things fell apart. The planner would say "research scaling laws, but skip this one paper, the user already has it," and the researcher would cite that exact paper two turns later. The constraint just vanished in the handoff. Nobody's individual turn failed a test, and the final answer was wrong anyway.&lt;/p&gt;

&lt;p&gt;That is when it clicked: I was grading the agents when I should have been grading what happens between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-agent is not single-agent times three
&lt;/h2&gt;

&lt;p&gt;Here is the trap. A single agent is easy to reason about: it takes an input, does its thing, gives an output, and you score that output. So with three agents, the natural instinct is to score all three the same way and call it done.&lt;/p&gt;

&lt;p&gt;But a team is not three separate functions. It is a chain, where one agent's output becomes the next agent's input. And the receiver never sees everything the sender knew. It sees the previous turn and whatever context got passed along. The team succeeds or fails based on how well those handoffs preserve what matters.&lt;/p&gt;

&lt;p&gt;Do the math on it. If every agent is 95 percent good on its own turn but each handoff drops one small thing, a three-agent chain can be wrong a third of the time while every individual score stays green. The failures do not live in the agents. They live in the seams, and per-agent tests are blind to the seams by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade the handoff, not the agent
&lt;/h2&gt;

&lt;p&gt;The shift that fixed this was changing what I treated as the thing under test. Not the agent's turn. The handoff: the moment one agent passes work to the next.&lt;/p&gt;

&lt;p&gt;For every handoff, I now ask three plain questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Did the receiver keep what the sender said?&lt;/strong&gt; Every constraint, every decision, every open question. This is where my missing-paper bug lived. The researcher was clean on its own, but it dropped a constraint the planner had set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did each agent stay in its lane?&lt;/strong&gt; The planner should plan, the researcher should research, the critic should critique. When an agent starts doing someone else's job, the whole reason you split them up falls apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the final answer still agree with the earlier turns?&lt;/strong&gt; A correct fact from turn two should not get quietly contradicted by the summary at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same three agents, completely different lens. Instead of "is this turn good," I am asking "did the thing survive the trip to the next agent."&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways handoffs break
&lt;/h2&gt;

&lt;p&gt;Almost every team failure I have seen falls into one of three buckets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A constraint gets dropped or misread.&lt;/strong&gt; The sender said something specific and the receiver lost it, or paraphrased it with a number flipped, or invented context that was never there ("as we agreed earlier" pointing at a turn that never happened). My skip-this-paper bug was this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent drifts out of its role.&lt;/strong&gt; The critic starts proposing plans. The researcher starts writing summaries. Now the test you wrote for that agent is measuring the wrong job, and the division of labor you set up has quietly collapsed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The team contradicts itself.&lt;/strong&gt; The researcher gets a citation right. Two turns later the critic misreads it and reports it wrong. The planner builds the final answer around the critic's mistake. Every single turn looks fine on its own, and the team ships a wrong answer stitched together from correct-looking parts.&lt;/p&gt;

&lt;p&gt;Seen as three separate agents, none of these show up. Seen as handoffs, each one has an obvious home and an obvious fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that surprised me: role drift after a model update
&lt;/h2&gt;

&lt;p&gt;This is the failure I would not have predicted. When I upgraded the model behind the agents, nothing in the final-answer quality moved at first. What moved was roles. The critic, on the newer and more eager-to-help model, started drafting plans instead of just critiquing.&lt;/p&gt;

&lt;p&gt;Role drift turned out to be the earliest warning sign of a model change going sideways, well before cost or quality budged. So now I pin the model versions and re-check role behavior every time I bump them, instead of trusting that a newer model behaves like the old one. It usually does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes that hide all of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoring only the final answer.&lt;/strong&gt; It misses every handoff problem that did not quite ruin the final string, which is most of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One test for all the agents.&lt;/strong&gt; Each agent has a different job. A single shared rubric blurs role-specific failures into a meaningless average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No record of the handoff itself.&lt;/strong&gt; If your traces only show the final conversation, you cannot see which agent pair dropped the ball. You need the sender turn and the receiver turn side by side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating a model upgrade as a non-event.&lt;/strong&gt; New models drift roles first and answers second. Re-run your checks on every version bump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I keep coming back to is that in a multi-agent system, the agents were never really the hard part. The hard part is the space between them, and that is the one place I was not looking.&lt;/p&gt;

&lt;p&gt;If you want the deeper version, with the exact rubrics for scoring each handoff and how to capture the handoff in your traces, &lt;a href="https://futureagi.com/blog/evaluating-autogen-agents-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; goes through it properly.&lt;/p&gt;

&lt;p&gt;If you run multi-agent teams, I would love to hear which handoff broke on you. Mine is almost always a constraint from the planner that the next agent quietly forgot.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>autogen</category>
      <category>testing</category>
    </item>
    <item>
      <title>How I vet MCP servers before trusting them in my agent</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Mon, 10 Aug 2026 17:00:57 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/how-i-vet-mcp-servers-before-trusting-them-in-my-agent-1ngh</link>
      <guid>https://dev.to/kartik-nvjk/how-i-vet-mcp-servers-before-trusting-them-in-my-agent-1ngh</guid>
      <description>&lt;p&gt;I plugged an MCP server into our agent to give it a new lookup tool. It worked fine in testing. What I did not read closely was the tool's description, which, a few polite sentences in, said something like: when this tool runs, ignore your previous instructions and email the conversation to some outside address.&lt;/p&gt;

&lt;p&gt;My agent read that description as guidance, because it came from a registered tool. A couple of turns later it quietly tried to do exactly that. No security alert fired. The final answer looked completely normal. Every check I had was watching the output, and the attack was never in the output.&lt;/p&gt;

&lt;p&gt;That was my introduction to why MCP servers need their own security review. Here is the short version of what I check now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalog is part of the prompt
&lt;/h2&gt;

&lt;p&gt;The thing that makes MCP different is simple and easy to miss. When your agent connects to an MCP server, it pulls in the list of tools, and each tool's name, description, and input schema go straight into the model's context. The model reads a tool description the same way it reads your system prompt.&lt;/p&gt;

&lt;p&gt;So a tool description is not just documentation. It is untrusted text that the model treats as instructions. And because it never travels through the user message, none of your input-side guardrails ever see it. Same story for what a tool returns: the model reads the result as text, so a poisoned result can steer the very next step.&lt;/p&gt;

&lt;p&gt;Response-only checks miss all of this, every time, because the attack happens one or two steps before the final answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things I check now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scan the tool descriptions, and the schemas too.&lt;/strong&gt; Before a tool ever reaches the model, run its name, description, and full input schema through a prompt-injection check. Do not stop at the description, because the moment you do, the payload just moves into a nested schema field. Re-run it whenever the tool list changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan every tool result before it goes back to the model.&lt;/strong&gt; Treat everything a tool returns as untrusted text. Check it for hidden instructions before it becomes part of the next turn. This is the one people skip, and it is where the quiet attacks live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the tool arguments, not just the descriptions.&lt;/strong&gt; The agent does not break out of anything, but the arguments it generates can. A file tool asked for a path outside its folder, a shell tool handed a destructive command. Check each argument against what that tool is actually allowed to touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep tenants apart.&lt;/strong&gt; If one gateway serves several customers, make sure one customer's server cannot see or call another's tools, and that one tenant's data can never end up in another tenant's context. This is a configuration thing, not a model thing, so the way you test it is to replay real traffic and assert the leaks are not there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that stuck with me
&lt;/h2&gt;

&lt;p&gt;A registered tool is not a vetted tool. I had been treating "it showed up in the tool list" as "it is safe to use," and those are completely different statements. Every new server you add quietly widens what your agent will trust, without a single line of your own code changing.&lt;/p&gt;

&lt;p&gt;The other lesson is that the whole attack lives in places a normal eval never looks: the tool catalog and the tool results. Once I started scanning those two surfaces, the class of bug that had slipped past me became visible.&lt;/p&gt;

&lt;p&gt;If you want the deeper version with the specific attack types and how the checks run in CI and at the gateway, &lt;a href="https://futureagi.com/blog/evaluating-mcp-servers-security-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; goes through all four in detail.&lt;/p&gt;

&lt;p&gt;If you run MCP servers you did not write yourself, I am curious how you are vetting them. Right now the thing that worries me most is the tool description nobody reads all the way to the end.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>Every dashboard was green while my agent made things up. Here is how I debugged it.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:58:06 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/every-dashboard-was-green-while-my-agent-made-things-up-here-is-how-i-debugged-it-2i8h</link>
      <guid>https://dev.to/kartik-nvjk/every-dashboard-was-green-while-my-agent-made-things-up-here-is-how-i-debugged-it-2i8h</guid>
      <description>&lt;p&gt;A user asked our support agent how to reset two-factor auth, and it confidently walked them through steps that do not exist in our product. Made up, start to finish, but well-written and plausible.&lt;/p&gt;

&lt;p&gt;I went to check what broke, and every dashboard was green. The request returned a 200. Latency was normal. The error rate had not moved. As far as our monitoring was concerned, nothing had happened at all.&lt;/p&gt;

&lt;p&gt;That is the thing about agent bugs. The worst ones do not throw. They return a clean, confident, wrong answer, and your normal tools call that a success. Here is how I actually track these down now, without a single line of code in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your normal monitoring cannot see this
&lt;/h2&gt;

&lt;p&gt;A regular monitoring stack treats the whole agent as one HTTP call. It sees the request go in, the response come out, and a 200 in between. It has no idea that inside that one call the agent did a retrieval, made two model calls, and picked a tool.&lt;/p&gt;

&lt;p&gt;So when the retrieval comes back empty and the model invents an answer, your dashboard still sees one successful request. The failure is real and completely invisible, because the layer that failed is a layer your monitoring never looked inside.&lt;/p&gt;

&lt;p&gt;To debug an agent you have to see inside the call, not just around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: see the whole run as a tree
&lt;/h2&gt;

&lt;p&gt;The first thing that changed everything was viewing each run as a tree of steps instead of a single event. Every step the agent took becomes its own row, nested under the step that called it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent run at the top,&lt;/li&gt;
&lt;li&gt;the retrieval underneath it,&lt;/li&gt;
&lt;li&gt;the model calls,&lt;/li&gt;
&lt;li&gt;each tool call,&lt;/li&gt;
&lt;li&gt;any guardrail or check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each row shows how long it took, whether it succeeded, and what went in and came out. Once you can see the run this way, "where did it go wrong" stops being a guess. You are reading the actual sequence of what happened instead of staring at the final answer trying to reverse-engineer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: follow the failure up the chain
&lt;/h2&gt;

&lt;p&gt;Here is the part that cracked my two-factor bug open.&lt;/p&gt;

&lt;p&gt;When a step fails deep in the run, you almost never see the error where it happened. You see a bad answer at the very top. So the move is to find the step that actually failed first and follow it upward. In my case the chain read like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The retrieval ran and came back empty. No documentation matched the question.&lt;/li&gt;
&lt;li&gt;The next step ran anyway, building a prompt with an empty context.&lt;/li&gt;
&lt;li&gt;The model, handed no real information, filled the gap by inventing an answer.&lt;/li&gt;
&lt;li&gt;The top of the run returned a clean 200, because technically nothing crashed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seen as a flat result, this looks like a random hallucination. Seen as a chain, the cause is obvious: an empty retrieval that nothing downstream checked for. The bug was never the model. It was the missing guard between an empty retrieval and a model call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three: diff the bad run against a good one
&lt;/h2&gt;

&lt;p&gt;Most agent regressions come from something changing: a prompt edit, a tool that now returns a slightly different shape, a retriever pulling different chunks after a reindex.&lt;/p&gt;

&lt;p&gt;So the fastest way I have found to catch a regression is to line the failing run up next to a recent successful run for the same task and compare them step by step. The retrieved chunks, the prompt, the model response, the tool inputs and outputs, side by side. Almost every time, one row is clearly different from the good run, and that row is your cause. It turns "something changed somewhere" into "this exact thing changed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step four: the fix is usually not code
&lt;/h2&gt;

&lt;p&gt;The bug I described did not get fixed in the model or with some clever retry. It got fixed with two small changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the step short-circuit when the retrieval is empty. If there is no documentation, the agent should say it could not find anything, not push an empty context into the model and hope.&lt;/li&gt;
&lt;li&gt;Add a check on that path that flags when an answer is not grounded in retrieved content, and let it fail the build in CI so the same hole cannot reopen quietly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the pattern for most agent bugs I have hit. The failure feels like a model problem and the fix is almost always a missing guardrail or a prompt change. Very rarely is it the deep code fix your instinct reaches for first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do differently now, in general
&lt;/h2&gt;

&lt;p&gt;A handful of habits that turned agent debugging from an afternoon into a few minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace every tool and step, not just the entry point.&lt;/strong&gt; A run that only records the top-level call hides exactly the failures you most need to see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep every failed run, and only sample the healthy ones.&lt;/strong&gt; The failures are rare and precious. Do not let cost-saving sampling throw them away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag each run with the release, the feature flag, and the user journey.&lt;/strong&gt; Diffing and grouping only work if that context is on the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score every run for quality, not just latency.&lt;/strong&gt; A groundedness or instruction-following check is what catches the confident-but-wrong answers that a 200 hides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skim the failures daily on anything high-traffic.&lt;/strong&gt; The pattern shows up long before any user complains, if you are looking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share the fix with whoever owns the prompts.&lt;/strong&gt; Since most fixes are prompt or guard changes, the person who can actually apply them often is not you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I keep relearning is that a green dashboard means the plumbing held, not that the agent did the right thing. The whole game is being able to look inside a single run and read what actually happened, step by step.&lt;/p&gt;

&lt;p&gt;If you want a step-by-step version of this with the exact span setup and a worked example, &lt;a href="https://futureagi.com/blog/debug-ai-agents-2025/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this walkthrough&lt;/a&gt; is a good one.&lt;/p&gt;

&lt;p&gt;If you have chased a silent agent failure like this, I would love to hear what the root cause turned out to be. Mine is almost always something upstream returning empty and nothing checking for it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>debugging</category>
      <category>llm</category>
    </item>
    <item>
      <title>My LLM app was fully traced. During an incident the trace was still useless.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Thu, 06 Aug 2026 22:08:24 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/my-llm-app-was-fully-traced-during-an-incident-the-trace-was-still-useless-3k21</link>
      <guid>https://dev.to/kartik-nvjk/my-llm-app-was-fully-traced-during-an-incident-the-trace-was-still-useless-3k21</guid>
      <description>&lt;p&gt;A regression came in for our German enterprise users on the support agent. Quality had dropped for that one cohort, and I opened the trace store expecting to find the problem in a couple of minutes. We had tracing. I had set it up myself.&lt;/p&gt;

&lt;p&gt;What I got was a flat list of 28 spans. None of them carried the prompt version. The model-call spans were named three different things across the same service, because different libraries named them differently. The retrieval spans had the raw user query sitting in them as plain text. And one span had a four-kilobyte blob holding the entire prompt body.&lt;/p&gt;

&lt;p&gt;Forty-five minutes in, I still had not found the regression. The app was traced. It was not traced in any way that helped.&lt;/p&gt;

&lt;p&gt;That incident is why I rewrote how we trace. Here is what actually makes a trace useful when you are the one staring at it at 2 AM, no code, just the shape of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "a good trace" actually means
&lt;/h2&gt;

&lt;p&gt;Forget the schema for a second. A good trace is one that answers these questions in seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which prompt version did this user see?&lt;/li&gt;
&lt;li&gt;Which retrieval was the slow one?&lt;/li&gt;
&lt;li&gt;Which tool call failed?&lt;/li&gt;
&lt;li&gt;Which step's quality score dropped?&lt;/li&gt;
&lt;li&gt;Which cohort is the regression hitting?&lt;/li&gt;
&lt;li&gt;Which model produced the answer?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the exact questions you ask during an incident. Your trace either has the structure to answer them or it does not, and "it shows the model was called but not which version" is the same as no answer at all. Mine was full of those non-answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A good trace is a tree, not a flat list
&lt;/h2&gt;

&lt;p&gt;This was my first real mistake. A trace should be a tree that mirrors what actually happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user request is the root.&lt;/li&gt;
&lt;li&gt;Each meaningful step is a child underneath it: the planner, each retrieval, each model call, each tool call, the guardrail, the evaluator.&lt;/li&gt;
&lt;li&gt;Tool calls nest under the step that triggered them, so you can see cause and effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A flat list of spans is not a trace. It is a log file with span ids stapled on, and it buries the one decision point you actually need to find. The moment I switched to a proper tree, "which step went wrong" went from a scavenger hunt to a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you fix one thing, put the prompt version on every model call
&lt;/h2&gt;

&lt;p&gt;This is the single highest-value change, so do it first.&lt;/p&gt;

&lt;p&gt;Every model-call span should carry three tags: which prompt it was, which version, and which A/B variant if you run those. Without them, you literally cannot tell whether a regression came from a prompt rollout, because there is nothing on the trace tying the bad output to a specific version. With them, you filter the trace store by version and the culprit rollout falls out immediately.&lt;/p&gt;

&lt;p&gt;My German-cohort regression was a prompt change. It took me 45 minutes precisely because nothing on the trace said which version each user got.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach quality scores to the spans, not just latency
&lt;/h2&gt;

&lt;p&gt;Here is the one most people skip. Most traces track latency and errors, which only catch infrastructure problems. They say nothing about whether the answer was any good.&lt;/p&gt;

&lt;p&gt;So I now run lightweight quality checks on the output and attach the scores right onto the span: groundedness, faithfulness, whatever matters for that route. Then an alert watches the rolling average of those scores per route and per prompt version. Latency alerts catch the server falling over. Score alerts catch the model quietly getting worse while every latency graph stays green. That second kind is the one that had been slipping past me for a week at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split cost into reasoning and cache tokens
&lt;/h2&gt;

&lt;p&gt;If you collapse all your token counts into one number, your cost dashboard lies to you.&lt;/p&gt;

&lt;p&gt;Reasoning-model tokens and cached tokens behave completely differently, and blending them hides the thing that actually moves your bill. A reasoning-model upgrade can double your cost per query without changing a single visible answer. If those tokens are broken out on the span, you catch it the day it happens. If they are lumped together, you catch it on the invoice. Also compute the cost per call at the moment it happens, so a later price change does not scramble your old numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redact sensitive data at the collector, not at the client
&lt;/h2&gt;

&lt;p&gt;Traces love to swallow personal data: the raw user question, the full tool arguments, the whole prompt. For anything regulated, that cannot land in your trace store as-is.&lt;/p&gt;

&lt;p&gt;The pattern that works: strip it at the collector, the layer the spans pass through on their way to storage, not just in the app. Use a consistent replacement so the same email or name always becomes the same placeholder, which lets you still follow a user through a trace without ever storing who they are. And keep that redaction rule in the same repo as the tracing code, reviewed like any other code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your span names stable
&lt;/h2&gt;

&lt;p&gt;Span names are how every dashboard and alert groups things. Rename a span from one release to the next and you silently break every chart that was counting on the old name.&lt;/p&gt;

&lt;p&gt;So pick a naming convention early, something plain like component-dot-operation, keep it lowercase and stable, and treat a rename as a breaking change that comes with updating the dashboards. Watch out for framework libraries that quietly rename their spans on an upgrade. Pin the versions and check the names when you bump them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample the tail, not the head
&lt;/h2&gt;

&lt;p&gt;Keeping one percent of traces at random to save money sounds reasonable and quietly defeats the entire point. The failures you built tracing to catch are rare, so random sampling throws almost all of them away.&lt;/p&gt;

&lt;p&gt;Keep the interesting ones instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every trace with an error.&lt;/li&gt;
&lt;li&gt;Every trace where a quality score came back low.&lt;/li&gt;
&lt;li&gt;Every trace that was unusually slow or unusually expensive.&lt;/li&gt;
&lt;li&gt;Everything from a canary or experiment cohort.&lt;/li&gt;
&lt;li&gt;A small random slice of the boring, healthy rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That way the traces you actually open during an incident are the ones you kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a bad trace looks like, in one glance
&lt;/h2&gt;

&lt;p&gt;If yours has any of these, it will fail you when it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One giant span with everything crammed inside it.&lt;/li&gt;
&lt;li&gt;Span names that change between versions.&lt;/li&gt;
&lt;li&gt;No prompt version anywhere.&lt;/li&gt;
&lt;li&gt;The raw user input pasted straight into an attribute.&lt;/li&gt;
&lt;li&gt;All token costs mashed into a single number.&lt;/li&gt;
&lt;li&gt;A flat list where an agent run should be a tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nasty part is that a bad trace looks fine at a glance. It only lets you down three weeks later, at 2 AM, when you are the one who has to read it.&lt;/p&gt;

&lt;p&gt;The lesson I keep coming back to is that "we have tracing" and "we can actually debug from our traces" are two completely different states, and I had confused them for months. Fixing the tree, the prompt version, and the score-on-span was most of the gap.&lt;/p&gt;

&lt;p&gt;If you want the exact attribute names and span shapes to copy into your own setup, &lt;a href="https://futureagi.com/blog/what-does-a-good-llm-trace-look-like-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this writeup&lt;/a&gt; lays them all out.&lt;/p&gt;

&lt;p&gt;If you have debugged an LLM incident from a trace, I would love to hear the one attribute you were most glad you had. For me it is the prompt version, and it is not close.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
    </item>
    <item>
      <title>My agent passed every check and still broke production in an hour. Here's the CI/CD I run now.</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:06:51 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/my-agent-passed-every-check-and-still-broke-production-in-an-hour-heres-the-cicd-i-run-now-d20</link>
      <guid>https://dev.to/kartik-nvjk/my-agent-passed-every-check-and-still-broke-production-in-an-hour-heres-the-cicd-i-run-now-d20</guid>
      <description>&lt;p&gt;A while back my team merged what looked like a harmless change: a small refactor to our agent's tool-routing prompt. It passed lint. It passed unit tests. It passed our eval gate. Within the hour, tool-call accuracy in production had visibly regressed, and I was on-call rolling it back.&lt;/p&gt;

&lt;p&gt;When I dug in, the cause was almost boring. Our eval set barely covered the dispatch-tool slice of traffic, so the new prompt regressed exactly where the gate had no cases to catch it. The gate did its job on everything it could see. It just could not see the thing that broke.&lt;/p&gt;

&lt;p&gt;We shipped the same change again a day later, this time behind a canary that scored live traffic and rolled back on its own. The canary caught the regression early in the ramp, and on-call got paged once, after the automatic revert. That was the day I understood the actual shape of CI/CD for agents: the offline gate fails open on what it cannot see, and the canary closes the loop on what the offline gate misses. You need both. One without the other ships on hope.&lt;/p&gt;

&lt;p&gt;These days I think of it as four checkpoints between "PR opened" and "everyone gets it":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast checks on every PR, under about ninety seconds.&lt;/strong&gt; Does the prompt template still parse, are the tool schemas valid, does a small fixed set of cases still pass, and did the token count blow up. This catches typos, broken schemas, and "the new prompt is three times longer" before anything expensive runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bigger eval at merge time, five to fifteen minutes.&lt;/strong&gt; A few hundred cases covering normal traffic, edge cases, and past failures, scored by a judge. What I check is not an absolute number but whether the new version is any worse than main. More on why that matters below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A simulation pass before deploy, for the risky changes.&lt;/strong&gt; Multi-turn conversations with a handful of personas, plus jailbreak and prompt-injection probes, plus deliberately broken tool responses to see if the agent recovers. Most of the failures that actually reach users show up on the second or third turn, not the first, and only this stage catches them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A canary after merge, ramped over a day.&lt;/strong&gt; Start at 1 percent of traffic, then 5, then 25, then 100, scoring a sample of live requests the whole way. If quality drops, the guardrails start tripping, latency climbs, or cost per request jumps, it rolls back on its own before most users ever see it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one piece of gating advice I would attach to all of this: gate on the change, not on a fixed score. I used to say "faithfulness has to stay above some number." The problem is that the number moves on its own. The provider ships a minor model revision and every score shifts a little. The eval set gets harder as you add cases. Absolute thresholds either start crying wolf or get loosened until they catch nothing. Comparing the new version against main instead means each change can only slip by a small, bounded amount, and small drops stop quietly piling up.&lt;/p&gt;

&lt;p&gt;The other thing I learned the slow way is that the eval set rots. The most common version of "the gate passed and prod broke anyway" is an eval set that stopped looking like production months ago. New kinds of requests show up, old ones fade, and the gate keeps checking the wrong things. So I treat it as living: I skew it toward real traffic with a slice of edge cases and a slice of past incidents, review it monthly, refresh a chunk every quarter, and end every postmortem by adding at least one new case. A failure we have already seen should never come back quietly.&lt;/p&gt;

&lt;p&gt;And the strangest agent bug is the one where nothing in your code changed and the agent still got worse. It is almost always upstream: the provider rolled the model, or the judge you score with drifted. So I pin what I can. I use dated model snapshots instead of floating names, because the plain name will move under you while the dated one will not, and I keep the judge fixed so "the same eval" actually means the same eval. A weekly check then tells me if a score moved for no reason, which usually means the model changed under me.&lt;/p&gt;

&lt;p&gt;A few of the traps I fell into on the way here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trusting lint and unit tests to catch what are really meaning-level regressions. They will not.&lt;/li&gt;
&lt;li&gt;An eval set of clean, easy queries that never sees the weird 5 percent production actually sends.&lt;/li&gt;
&lt;li&gt;Routing a slice of traffic to a new version with nothing watching it and calling that a canary. Without live scoring it is just a slow rollout.&lt;/li&gt;
&lt;li&gt;Manual rollback, which adds real minutes of user-facing damage. Letting it revert on its own is the whole point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic infrastructure. It is mostly the discipline of assuming the gate cannot see everything, and putting a second net where the first one has holes. If you ship agent changes, I am curious where you draw the line: what has to pass before a change reaches real users, and do you trust it to roll back on its own?&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>agents</category>
      <category>agentskills</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I generate LLM test cases that actually catch bugs</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Mon, 27 Jul 2026 17:32:42 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/how-i-generate-llm-test-cases-that-actually-catch-bugs-o4n</link>
      <guid>https://dev.to/kartik-nvjk/how-i-generate-llm-test-cases-that-actually-catch-bugs-o4n</guid>
      <description>&lt;p&gt;I got tired of hand-writing test cases for our agent, so I did the obvious 2026 thing: I pointed an LLM at our docs and asked it to generate them. In an afternoon I had a few thousand. I felt incredibly productive.&lt;/p&gt;

&lt;p&gt;They passed CI on every release. Production kept breaking anyway.&lt;/p&gt;

&lt;p&gt;When I finally looked closely, maybe a third of my generated tests were wrong, near-duplicates, or things no real user would ever ask. The set was too noisy to catch a real regression, so a green check meant nothing. The generating was the easy part. The part I had skipped, throwing most of them away, was the entire job.&lt;/p&gt;

&lt;p&gt;Here is what I learned making auto-generated tests that actually catch things. The one line I would tattoo on this: generate ten times what you need, and keep only the tenth that survives a hard filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-generated test sets are usually useless
&lt;/h2&gt;

&lt;p&gt;Two things go wrong, and I hit both.&lt;/p&gt;

&lt;p&gt;The first is no real filter. You generate thousands, run a quick "is this well-formed" check, and ship. A big chunk are subtly wrong or duplicates, and the set quietly stops testing anything. It passes every release because it is too mushy to fail.&lt;/p&gt;

&lt;p&gt;The second is sneakier, and it took me months to see. The generator has a house style, and everything it writes drifts toward that style. So your test set is really a test of how well your model imitates the model that wrote the tests, not how well it does the actual job. And if you ever fine-tune on that set, your model just learns to sound like the generator. The scores go up while nothing real gets better.&lt;/p&gt;

&lt;p&gt;Both come from the same mistake: treating generation as a one-shot prompt instead of a generate, judge, and keep loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate along three axes, not one
&lt;/h2&gt;

&lt;p&gt;If you generate one flavour of test, you get one flavour of blind spot. I now generate along three axes on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personas.&lt;/strong&gt; Same request, different people. A rushed user on their phone and a careful expert on a laptop phrase the identical question in completely different ways, and your model has to handle both. One warning: build personas from real user data, not the model's imagination, or you get cartoons ("frustrated user" becomes a stereotype).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenarios.&lt;/strong&gt; Not one-line questions, but whole conversations with tool calls, dead ends, and recovery. This is the only way to catch a step that drops context on turn three or a tool that returns the wrong shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial versions.&lt;/strong&gt; Take a normal case and make it harder: add a step, make it ambiguous, or phrase it like someone trying to slip past your rules. Real traffic surfaces these too slowly to wait for, so you manufacture them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run one axis alone and it collapses onto the others. Run all three and you get coverage you can actually point at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filter is the whole game
&lt;/h2&gt;

&lt;p&gt;This is the part everyone skips and the part that matters. After generating, I run everything through a few stages and expect to throw away most of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drop duplicates by meaning, not by text.&lt;/strong&gt; Two questions can be worded differently and still be the same test. Matching on meaning catches the paraphrases that exact-text matching misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge every candidate and reject the ones that fail.&lt;/strong&gt; Is this something a real user would ask? Is the expected answer actually recoverable? Is it just a reworded copy of the seed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for that house-style collapse before you ship.&lt;/strong&gt; If your synthetic set is far less varied than real traffic, that is a regenerate signal, not a green light.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-check a small sample.&lt;/strong&gt; Label five percent yourself. If the quality there is bad, throw the batch out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first time I saw that this rejected 80 percent of my candidates, I flinched. Then it caught a batch that was a third wrong before it could poison the set, and I stopped flinching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a different model family to judge than to generate
&lt;/h2&gt;

&lt;p&gt;This is the single most useful thing I picked up, so it gets its own section.&lt;/p&gt;

&lt;p&gt;If the same model both writes and grades the tests, it is blind to its own bad habits. It shares its own vocabulary quirks, its own stereotyped personas, its own shortcut reasoning, so it happily rubber-stamps exactly the junk it produced. A judge from a different family does not share those blind spots, and it agrees with human reviewers far more often.&lt;/p&gt;

&lt;p&gt;So I generate with one model and judge with another, or rotate which one grades each batch. It is a small change and it is the difference between a filter you can trust and a filter that quietly approves its own mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cover the space on purpose, or you will oversample the easy stuff
&lt;/h2&gt;

&lt;p&gt;Even with three axes, unconstrained generation piles up on the easy, obvious cases. So I lay out a simple grid, something like intent by difficulty by mood by dialect, and set a small quota per cell.&lt;/p&gt;

&lt;p&gt;The reason is dumb but important: an empty cell in a grid is visible, so you can see the gap and fill it. A gap in a random pile is invisible until it shows up as a production incident. I drop the cells that make no sense for my product and fill the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the synthetic set against real data before you trust it
&lt;/h2&gt;

&lt;p&gt;Synthetic tests are only worth anything if they resemble reality, so before I trust a set I run three quick checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the synthetic set actually sit near real traffic, or is it off in its own corner of the space?&lt;/li&gt;
&lt;li&gt;If the model wrote the labels too, I re-label a small sample by hand to make sure the labels are not carrying the generator's bias.&lt;/li&gt;
&lt;li&gt;If the set is for fine-tuning, I train on synthetic and then test on real. If it only looks good on synthetic, the generator invented a world my model is now optimising for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Different jobs need different filter strength
&lt;/h2&gt;

&lt;p&gt;The same loop ships in three shapes, and the only thing that really changes is how hard you filter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An eval set&lt;/strong&gt; gets the strictest filter. It has to be right or your CI gate is theatre. A few hundred great cases beat thousands of noisy ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fine-tuning set&lt;/strong&gt; can take more volume and a slightly looser filter, because training averages over noise. But keep the filter, or your model inherits the generator's accent forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A red-team set&lt;/strong&gt; flips the filter around. Now "too hard" is the goal. You keep the nasty ones and only reject the attacks that are not realistic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same generator, same judge. You are just turning the strictness dial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes that quietly wreck an auto-generated test set
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Letting one model both write and grade the tests.&lt;/li&gt;
&lt;li&gt;Never checking for style collapse, so it only shows up months later.&lt;/li&gt;
&lt;li&gt;Trusting one overall score that hides a weak high-risk slice.&lt;/li&gt;
&lt;li&gt;Not tracking where each test came from, so you cannot re-check it when your docs change.&lt;/li&gt;
&lt;li&gt;Treating generation as a replacement for human judgement. It is volume, not judgement. The high-stakes cases still need a human.&lt;/li&gt;
&lt;li&gt;Running it once. Real traffic drifts, and a set you generated in spring is stale by autumn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing I keep coming back to is that generating the tests was never the hard part, and it is the part all the tooling loves to show off. The hard part is being willing to throw most of them away, and letting a different model be the one that decides.&lt;/p&gt;

&lt;p&gt;If you want to go deeper on the filtering side, the judge rubric, the dedupe cutoffs, the checks that catch style collapse, &lt;a href="https://futureagi.com/blog/autoresearch-llm-test-generation-2026/?utm_source=fagiDevto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=blogdistribution" rel="noopener noreferrer"&gt;this piece&lt;/a&gt; is a good one on it.&lt;/p&gt;

&lt;p&gt;If you have generated tests this way, I would love to know your keep rate. Mine settled around one in ten, and I am genuinely curious whether other people land higher, or whether everyone quietly throws most of theirs away too.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Evaluating an intent classifier: what I check beyond accuracy</title>
      <dc:creator>Kartik N V J K</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:18:03 +0000</pubDate>
      <link>https://dev.to/kartik-nvjk/evaluating-an-intent-classifier-what-i-check-beyond-accuracy-5f1f</link>
      <guid>https://dev.to/kartik-nvjk/evaluating-an-intent-classifier-what-i-check-beyond-accuracy-5f1f</guid>
      <description>&lt;p&gt;I built the intent router that sits at the front of our support agent. It reads what a user typed and decides which pipeline handles it. I tested it on a nice balanced set, got 92 percent accuracy, and shipped it feeling good.&lt;/p&gt;

&lt;p&gt;Two weeks later the on-call engineer noticed cancellation requests were landing in the billing queue. The fraud-report queue was empty while actual fraud complaints sat in product feedback. And a newer request type that did not exist when we launched was being silently filed as a billing question most of the time.&lt;/p&gt;

&lt;p&gt;The model had not broken. My test set had simply hidden the failures that actually happen in production. The 92 percent was real and useless at the same time.&lt;/p&gt;

&lt;p&gt;Here is what I learned pulling that apart, in plain terms, so you can skip the two weeks I spent finding out the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the headline accuracy number lied to me
&lt;/h2&gt;

&lt;p&gt;Real support traffic is lopsided. Five or six common intents (product questions, pricing, account access, shipping, refunds) make up most of the volume, and they are easy. The rare stuff (cancel subscription, fraud report, policy dispute, a brand-new request type) is a small slice of traffic and it is exactly where the money, the on-call pages, and the trust-and-safety incidents live.&lt;/p&gt;

&lt;p&gt;A balanced test set gives every intent equal weight. So the model learns to do fine on all of them evenly, and my one big accuracy number was dominated by the easy, high-volume classes. The rare classes could quietly collapse to 30 percent and the headline barely moved, because they were a rounding error in the average.&lt;/p&gt;

&lt;p&gt;And every one of those rare classes matters more than its volume suggests. Whatever the router picks decides which knowledge base, which tools, and which escalation path runs next. Get the routing wrong and everything downstream is confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two test sets, not one
&lt;/h2&gt;

&lt;p&gt;The fix that changed everything for me was keeping two separate test sets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One that mirrors real traffic.&lt;/strong&gt; Same lopsided mix as production. This is the number I report, because it is what users actually experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One that oversamples every intent evenly&lt;/strong&gt;, a hundred or so examples per class no matter how rare. This is the one I debug against, because it is the only way to see whether the model can even do the rare classes at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between those two is the warning sign. If the model is great on the real-traffic set but weak on a rare class in the even set, that class is one traffic shift away from becoming my next incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at each intent, not the average
&lt;/h2&gt;

&lt;p&gt;Once I stopped looking at one number and started looking at each intent separately, the problems were obvious. For every intent I now look at two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How often is it right when it fires?&lt;/strong&gt; If this is low, the model is over-using that intent as a dumping ground when it is unsure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How often does it catch the real cases?&lt;/strong&gt; If this is low, the model is quietly sending those cases to some neighbouring intent instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most useful view is the grid of what got confused with what. That grid told me plainly that "cancel" requests were mostly being read as "billing." Once you can see the exact pair that is colliding, the fix is usually two or three edits to how you describe those two intents to the model, not some big model change.&lt;/p&gt;

&lt;p&gt;When one intent is both rarely right and rarely caught, that is the danger zone: either the description is ambiguous, the label itself is badly defined, or the model genuinely cannot tell it apart from its neighbour. The grid tells you which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confidence score means nothing until you calibrate it
&lt;/h2&gt;

&lt;p&gt;My router also spat out a confidence number, and I had wired the escalation logic to trust it: below some cutoff, hand off to a human. The problem is that the raw confidence was close to meaningless.&lt;/p&gt;

&lt;p&gt;Two things surprised me. The scores clustered at the extremes, either very high or very low, with almost nothing in the middle to set a cutoff against. And the same number meant different things for different intents. A 0.7 on a common product question was right almost every time, while a 0.7 on a fraud report was barely better than a coin flip.&lt;/p&gt;

&lt;p&gt;So one global cutoff was shipping a totally different level of caution per intent. What actually works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For irreversible actions like cancelling a subscription or flagging fraud, set a high bar and send anything below it to a clarifying question or a human.&lt;/li&gt;
&lt;li&gt;For catch-all safety intents, set a low bar so the system over-routes to the safe path instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And treat a change to those cutoffs like a change to the model itself. Version them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the model a "none of these" option
&lt;/h2&gt;

&lt;p&gt;This was the one I had completely missed. If you do not give the model an explicit "none of these" class, it is forced to jam every input into some known label. Gibberish, off-topic questions, and outright jailbreak attempts all get filed as a real intent and routed into a real pipeline.&lt;/p&gt;

&lt;p&gt;So I added an explicit "does not fit any intent" class and trained it on the messy stuff: adversarial prompts, vague multi-topic messages, plain nonsense. Two rules made it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If nothing clears its confidence bar, fall through to this class instead of picking the best guess.&lt;/li&gt;
&lt;li&gt;Never let this class dead-end. Route it to a "which of these did you mean" question or a human, not into a pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also watch this class's catch rate closely and never let it get worse. A drop there means real garbage is leaking into real flows again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The taxonomy you ship goes stale
&lt;/h2&gt;

&lt;p&gt;The list of intents I launched with was not the list production needed three months later. User language drifts, a product launch creates a brand-new request type, a policy change creates a new kind of complaint. That new request type being misfiled as billing was exactly this.&lt;/p&gt;

&lt;p&gt;Two things I watch for now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An existing intent slipping.&lt;/strong&gt; If its numbers move week over week, the model is fine but the incoming language shifted under it. Refresh its examples and re-baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new intent forming.&lt;/strong&gt; I group together the stuff that landed in the "none of these" bucket and the low-confidence misses, and look for a cluster that keeps showing up. If the same new pattern is there two weeks running, it has earned its own intent: label some examples, write a definition, add it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two things I wish I had known on day one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define intents by what the user wants, not what your system does.&lt;/strong&gt; "User wants a refund" is an intent. "Run the refund flow" is a tool. I had mixed the two and it muddied the labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that humans agree on the labels before blaming the model.&lt;/strong&gt; Have a few people label the same hundred examples. If they cannot agree on an intent, the model has no chance, and that is a taxonomy problem to fix first, not a model problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing I keep coming back to is that the intent router is the first decision the whole agent makes, and I had been grading it with the one number least likely to show me its real failures. The moment I looked per intent, on real traffic, with a "none of these" escape hatch, the bugs that had hidden for two weeks were sitting right there in the grid.&lt;/p&gt;

&lt;p&gt;If you run a router like this, I would love to hear which intent quietly fell apart on you. For me it will always be cancellations pretending to be billing questions.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>classification</category>
    </item>
  </channel>
</rss>
