<?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: Sivananda Panda</title>
    <description>The latest articles on DEV Community by Sivananda Panda (@sivananda_panda_4812903b4).</description>
    <link>https://dev.to/sivananda_panda_4812903b4</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%2F3996153%2F06f576dc-9b6e-42db-97c9-6b12d02cc7b1.jpeg</url>
      <title>DEV Community: Sivananda Panda</title>
      <link>https://dev.to/sivananda_panda_4812903b4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sivananda_panda_4812903b4"/>
    <language>en</language>
    <item>
      <title>The Evaluation Layer: The Part of Your LLM System You Keep Skipping</title>
      <dc:creator>Sivananda Panda</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:43:52 +0000</pubDate>
      <link>https://dev.to/sivananda_panda_4812903b4/the-evaluation-layer-the-part-of-your-llm-system-you-keep-skipping-3ko4</link>
      <guid>https://dev.to/sivananda_panda_4812903b4/the-evaluation-layer-the-part-of-your-llm-system-you-keep-skipping-3ko4</guid>
      <description>&lt;p&gt;I've built two agentic AI systems over the past few months, and despite solving very different problems, both exposed the same weakness. The agents worked perfectly during demos. They passed my manual tests. They looked production-ready. But once real users started interacting with them, confidently wrong responses began slipping through. The root cause wasn't the model, the prompts, or the tools. It was much simpler: there was no proper evaluation layer. The system could tell whether it had produced an answer, but it had no way to determine whether that answer was actually good.&lt;/p&gt;

&lt;p&gt;This article is about the layer that closes that gap. What it is, why LLM systems specifically can't survive without it, the five ways people actually build it, the tooling, and the mistakes that make an eval layer worse than useless — because a miscalibrated eval gives you false confidence, which is more dangerous than no eval at all.&lt;/p&gt;

&lt;p&gt;I'll use a RAG-and-agent stack for the concrete examples (LangGraph, Claude, LangSmith), but nothing here is framework-specific. The principles move to any stack you like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why LLM systems need this and normal software doesn't
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable property that breaks every habit you brought from traditional engineering: &lt;strong&gt;the same input can produce different outputs, and a wrong output usually looks exactly like a right one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a normal codebase, &lt;code&gt;add(2, 2)&lt;/code&gt; returns &lt;code&gt;4&lt;/code&gt; or it returns a bug you can see. The failure has a shape — a stack trace, a null, a red test. You write an assertion, it passes forever, you move on. LLM failures don't have that shape. A hallucinated citation, a subtly-off summary, a tool call with the wrong argument that still "sort of" works — these render as fluent, plausible, professional-looking text. The failure is camouflaged inside a success.&lt;/p&gt;

&lt;p&gt;Three things follow from that, and they're the whole reason the eval layer exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-determinism means one passing run proves nothing.&lt;/strong&gt; You need distributions, not point checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interesting failures live in the middle.&lt;/strong&gt; An agent that made 12 tool calls and 4 reasoning hops can reach a correct answer through completely broken logic — and reach a wrong one next time from the same code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody sees the middle by default.&lt;/strong&gt; The end user gets the final answer. The reasoning, the retrieval, the tool arguments — all invisible unless you deliberately capture them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the eval layer isn't a testing afterthought. It's the observability and quality infrastructure that lets you answer three questions on a continuous basis: is the system doing the &lt;em&gt;right&lt;/em&gt; thing (correctness), doing it &lt;em&gt;efficiently&lt;/em&gt; (cost and latency), and doing it &lt;em&gt;safely&lt;/em&gt; (no leaked secrets, no disallowed tools, no confident nonsense). Skip it and you're not shipping a product; you're running an ongoing experiment on your users without reading the results.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the layer actually is
&lt;/h2&gt;

&lt;p&gt;It's tempting to picture "the eval layer" as one service you bolt on at the end. It isn't. It's a cross-cutting concern that taps into every stage of execution and scores the &lt;em&gt;trace&lt;/em&gt;, not just the &lt;em&gt;output&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rgvg3cd9fzyhz85e2x0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rgvg3cd9fzyhz85e2x0.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three moving parts. &lt;strong&gt;Tracers&lt;/strong&gt; capture what happened at every step — inputs, outputs, tool arguments, retrieved chunks, latencies, token counts. &lt;strong&gt;Scorers&lt;/strong&gt; turn those captured artifacts into numbers — a faithfulness score, a latency measurement, a pass/fail on a schema. &lt;strong&gt;Evals&lt;/strong&gt; are the curated sets and thresholds that give those numbers meaning: is 0.81 faithfulness good, and is it better or worse than last week? Lose any one of the three and the other two stop being useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five ways to build it
&lt;/h2&gt;

&lt;p&gt;These aren't competing options where you pick one. A mature system runs all five, at different frequencies. But you'll add them in roughly this order, cheapest and most objective first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Deterministic evals — start here, not with an LLM
&lt;/h3&gt;

&lt;p&gt;The instinct is to reach for an LLM judge immediately because the outputs are "fuzzy." Resist it. A surprising amount of what you care about is not fuzzy at all, and plain code checks it faster, cheaper, and without any of the reliability problems a judge brings.&lt;/p&gt;

&lt;p&gt;Did the agent call only allowed tools? Did the structured output match the schema? Did it stay under the tool-call budget? Did the JSON parse? These have crisp right answers, and a regular function is the correct instrument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;eval_tool_call_validity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentTrace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EvalResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every tool call must use an allowed tool and respect the budget.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;allowed_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch_document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disallowed tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_TOOL_CALLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exceeded call budget: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;EvalResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These cost microseconds and never disagree with themselves. Run them on every single trace in production, not just in CI. They're your smoke detectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. LLM-as-judge — powerful, and the thing most likely to lie to you
&lt;/h3&gt;

&lt;p&gt;For the genuinely subjective stuff — tone, helpfulness, whether an answer is faithful to its source — you hand the output to a &lt;em&gt;separate&lt;/em&gt; model with a rubric and collect structured scores.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are evaluating an AI agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s response.

User query: {query}
Agent response: {response}
Retrieved context: {context}

Score each dimension 1-5, and cite the specific text that justifies your score.
- Faithfulness: Is every claim grounded in the provided context?
- Relevance:    Does it address what the user actually asked?
- Completeness: Does it fully answer the question?

Respond ONLY as JSON:
{{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: N, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: N, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: N, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice I made the judge cite evidence and reason, not just emit a number. That's not decoration. A bare score is unfalsifiable; a score with quoted evidence can be audited when you disagree with it.&lt;/p&gt;

&lt;p&gt;Here's the part teams underrate: &lt;strong&gt;LLM judges have the same weaknesses as the model being judged.&lt;/strong&gt; They reward length. They're swayed by confident phrasing. They wave through fluent errors — the exact failure mode you built the judge to catch. An uncalibrated judge doesn't measure quality, it launders your existing biases into an official-looking number. So the judge is not the end of the work; it's a component that itself needs validating against human labels before you trust a word it says. More on that in the mistakes section, because it's the one people get wrong most often.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Trace-level evaluation — stop grading the black box
&lt;/h3&gt;

&lt;p&gt;Grading only the final answer is like reviewing a math test by checking the last number and ignoring the working. The number can be right for the wrong reasons and wrong for the same reasons next time.&lt;/p&gt;

&lt;p&gt;The fix is to instrument every step and evaluate the whole trace. Observability platforms — LangSmith, Langfuse, Arize Phoenix, W&amp;amp;B Weave — exist for exactly this. You wrap your nodes and they capture the tree of execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;

&lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@traceable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# captured as a child run
&lt;/span&gt;    &lt;span class="n"&gt;docs&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# captured as a child run
&lt;/span&gt;    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# captured as a child run
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is diagnostic, not just descriptive. When faithfulness tanks, the trace tells you &lt;em&gt;where&lt;/em&gt;: the retriever pulled garbage, or the retriever was fine and the generator ignored it. Those are two completely different bugs with two completely different fixes, and output-only evals can't tell them apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Human-in-the-loop — the ground truth everything else calibrates against
&lt;/h3&gt;

&lt;p&gt;Automated evals are necessary and insufficient. Humans catch what code and judges structurally cannot: domain inaccuracies a generalist judge waves through, tone that's wrong for your specific users, edge cases that are technically correct and practically useless. And critically — human labels are the yardstick you use to check whether your LLM judge is any good. Without them, every other layer is measuring against itself.&lt;/p&gt;

&lt;p&gt;Four things separate a useful human-eval pipeline from a box-ticking one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sample successes, not just failures.&lt;/strong&gt; "The agent returned an answer" is not evidence the answer was good. Review 5–10% of &lt;em&gt;all&lt;/em&gt; production traces weekly, including the ones nobody complained about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stratify the sample.&lt;/strong&gt; Cover query types, user segments, and tool paths. Review only the easy queries and you'll build a rosy, useless picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write annotation rubrics that force a decision.&lt;/strong&gt; "Was this response good?" produces noise. "Did the response answer the user's stated question without inventing information they didn't ask for?" produces labels you can act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure agreement between annotators.&lt;/strong&gt; Run the same trace past two reviewers now and then. If they disagree more than one time in five, the problem is your rubric, not your reviewers — sharpen it before you collect more labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Regression and red-teaming — so tomorrow's fix doesn't quietly break today's feature
&lt;/h3&gt;

&lt;p&gt;Every prompt tweak is a potential regression, and LLM regressions are invisible without a locked baseline to compare against. A regression suite is a curated set of &lt;code&gt;(input, expected)&lt;/code&gt; pairs you run on every deploy. Red-teaming is its adversarial twin: inputs deliberately built to break things — prompt injection, context-stuffing, multi-hop manipulation, tool misuse. In agentic systems these are nastier than in a single call, because one poisoned step can cascade through every step that follows it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evals/
  regression/
    core_qa.jsonl          # 50 representative Q&amp;amp;A pairs
    tool_use.jsonl         # 30 traces exercising tool-call patterns
    edge_cases.jsonl       # 20 known-difficult inputs
    adversarial.jsonl      # 15 red-team prompts
  run_suite.py             # runner that diffs scores against baseline
  baseline_scores.json     # locked scores from the last known-good release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it in CI before every deploy. Alert when any category drops more than ~5% from baseline. The exact number matters less than the fact that there &lt;em&gt;is&lt;/em&gt; one and something screams when you cross it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six mistakes that turn an eval layer into a liability
&lt;/h2&gt;

&lt;p&gt;An eval layer isn't automatically good. A bad one is worse than none, because "our scores are green" is a sentence that stops people from looking closer. These are the six failure modes I see most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grading only the final output.&lt;/strong&gt; Covered above, but it's the number-one blind spot so it earns repeating: a correct answer reached through a broken trace is a landmine, not a pass. Evaluate every node, not just the terminal one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapsing everything into one score.&lt;/strong&gt; A composite of 0.72 tells you something is wrong and nothing about what. Is it faithfulness? Latency? Tool selection? Keep the dimensions separate, track them separately, set thresholds separately. You can always average later; you can't un-average.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluating on your training distribution.&lt;/strong&gt; If your eval set is made from the same queries you used to build and tune the system, you've optimized &lt;em&gt;to the eval&lt;/em&gt;, not to reality — Goodhart's Law wearing a lab coat. Hold out a blind set that never touches development, and keep topping it up with real production samples. Treat eval-set contamination as seriously as data leakage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting an uncalibrated judge.&lt;/strong&gt; The single most common self-inflicted wound. Before an LLM judge scores anything that matters, run it against 100+ human-labeled examples and compute how often it agrees with humans. Disagreement above ~15%? Fix the judge prompt before it goes anywhere near your dashboard. A judge you haven't validated is a confidence machine, not a measurement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No baseline, no history.&lt;/strong&gt; Scores you don't persist can't answer "better or worse than last week?" Every run should write to a durable store with a timestamp, run ID, and git SHA. A SQLite table is enough — the discipline of storing beats the sophistication of the storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating evals as a one-time setup.&lt;/strong&gt; Eval suites rot. User behavior drifts, new failure modes appear, and a suite built in month one is riddled with blind spots by month six. Add a case from every production failure within a couple of days of finding it, and audit the whole suite quarterly. It's a living artifact, not a shipped deliverable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting it up: a practical walkthrough
&lt;/h2&gt;

&lt;p&gt;Opinionated defaults for a LangGraph production agent. Swap the tools freely; the sequence is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Define "good" in prose before you write any eval code.&lt;/strong&gt; For each capability, answer on paper: what does a correct output look like, what are the common failure modes, what constraints must always hold, and what separates "barely acceptable" from "excellent" in a domain expert's eyes? This costs half a day. Skip it and you'll spend weeks precisely measuring the wrong things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Instrument the graph.&lt;/strong&gt; Wrap every node. Capture node name, input, output, latency, token counts, and any tool calls with arguments and returns.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nd"&gt;@traceable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;planner_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@traceable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retriever_node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retriever_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Build the suite in three tiers, by frequency.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Tier 1 — every trace, real time:&lt;/em&gt; schema validation, tool-call constraints, latency thresholds. Cheap deterministic code.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Tier 2 — daily or per release:&lt;/em&gt; LLM-judge scores on a sample, retrieval metrics (precision@k, NDCG), end-to-end correctness on the regression suite.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Tier 3 — weekly or per major release:&lt;/em&gt; stratified human review, red-team annotation, edge-case triage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Set thresholds before production, not during the incident.&lt;/strong&gt; For every metric, define a green zone (normal), a yellow zone (investigate, don't block), and a red zone (block the deploy or page, someone). "We'll know bad when we see it" is the sentence people say right before a bad week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Wire it into CI/CD.&lt;/strong&gt; An eval that only runs when someone remembers doesn't run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/eval.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Eval Suite&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-evals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run regression suite&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python evals/run_suite.py --compare-to baseline_scores.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check score thresholds&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python evals/check_thresholds.py --fail-on-regression &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What to measure and what to keep
&lt;/h2&gt;

&lt;p&gt;Split metrics into three families and never blend them into one number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctness&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;How to compute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Faithfulness&lt;/td&gt;
&lt;td&gt;Is every claim grounded in retrieved context, with nothing hallucinated?&lt;/td&gt;
&lt;td&gt;LLM judge or NLI model comparing response to context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer relevance&lt;/td&gt;
&lt;td&gt;Does the response address the actual question?&lt;/td&gt;
&lt;td&gt;LLM judge or embedding similarity between query and response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context precision&lt;/td&gt;
&lt;td&gt;Of the chunks retrieved, what fraction were useful?&lt;/td&gt;
&lt;td&gt;Human or LLM label per chunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context recall&lt;/td&gt;
&lt;td&gt;Did retrieval surface everything needed to answer?&lt;/td&gt;
&lt;td&gt;Compare retrieved set against a gold document set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-call accuracy&lt;/td&gt;
&lt;td&gt;Right tools, right arguments?&lt;/td&gt;
&lt;td&gt;Deterministic diff against an expected tool trace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latency (p50/p95/p99)&lt;/td&gt;
&lt;td&gt;User-perceived speed&lt;/td&gt;
&lt;td&gt;Track trends; set SLOs per use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token consumption&lt;/td&gt;
&lt;td&gt;Cost per query&lt;/td&gt;
&lt;td&gt;Input + output tokens per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-call count&lt;/td&gt;
&lt;td&gt;Wasted calls&lt;/td&gt;
&lt;td&gt;Compare to the minimum viable count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry rate&lt;/td&gt;
&lt;td&gt;How often steps fail and rerun&lt;/td&gt;
&lt;td&gt;Under ~5% in steady state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context-window utilization&lt;/td&gt;
&lt;td&gt;How full the window runs&lt;/td&gt;
&lt;td&gt;High → truncation risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Safety and reliability&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination rate&lt;/td&gt;
&lt;td&gt;% of responses with claims unsupported by context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refusal rate&lt;/td&gt;
&lt;td&gt;% of valid queries wrongly refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task-completion rate&lt;/td&gt;
&lt;td&gt;% of queries reaching a terminal answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate by type&lt;/td&gt;
&lt;td&gt;Tool failures, timeouts, parse errors — broken out, not summed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraint-violation rate&lt;/td&gt;
&lt;td&gt;% of runs breaking a defined rule (e.g. a disallowed tool)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And keep the artifacts, because a score with no trace behind it is impossible to debug. For every run, preserve: the &lt;strong&gt;full execution trace&lt;/strong&gt; (at least 30 days of production), &lt;strong&gt;per-dimension scores&lt;/strong&gt; linked to run ID and git SHA, the &lt;strong&gt;judge's reasoning&lt;/strong&gt; (not just its number — this is gold when you contest a score), &lt;strong&gt;failure cases tagged by type&lt;/strong&gt;, the &lt;strong&gt;versioned eval-suite definition&lt;/strong&gt;, the current &lt;strong&gt;baseline snapshot&lt;/strong&gt;, and &lt;strong&gt;human annotation logs&lt;/strong&gt; with annotator ID and timestamp.&lt;/p&gt;




&lt;h2&gt;
  
  
  Picking your tooling
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;The catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LangSmith&lt;/td&gt;
&lt;td&gt;LangChain/LangGraph shops wanting tight integration&lt;/td&gt;
&lt;td&gt;Vendor lock-in; price scales with trace volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse&lt;/td&gt;
&lt;td&gt;Open-source, self-hostable&lt;/td&gt;
&lt;td&gt;More setup; smaller ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arize Phoenix&lt;/td&gt;
&lt;td&gt;Teams already on Arize for ML monitoring&lt;/td&gt;
&lt;td&gt;Stronger on classic ML; newer for LLMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&amp;amp;B Weave&lt;/td&gt;
&lt;td&gt;Teams already living in Weights &amp;amp; Biases&lt;/td&gt;
&lt;td&gt;Natural fit if you also fine-tune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAGAS&lt;/td&gt;
&lt;td&gt;RAG metrics out of the box&lt;/td&gt;
&lt;td&gt;Narrow scope — mostly retrieval + generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom (SQLite + an SDK)&lt;/td&gt;
&lt;td&gt;Maximum control, minimal dependency&lt;/td&gt;
&lt;td&gt;You own the build and the maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My honest default for a LangGraph production system: &lt;strong&gt;LangSmith for tracing, RAGAS for RAG-specific metrics, and a small custom Python runner for the deterministic checks.&lt;/strong&gt; Add human-eval tooling — even a scrappy Streamlit annotation app — once the system is past its first real users. Don't buy the enterprise platform on day one; you don't yet know what you're measuring.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual mindset shift
&lt;/h2&gt;

&lt;p&gt;The thing most teams get wrong isn't a tool choice. It's timing. They treat evaluation as a phase that comes &lt;em&gt;after&lt;/em&gt; building, and by then the design decisions that would have made the system measurable are already baked in.&lt;/p&gt;

&lt;p&gt;Flip it. Evaluation is a lens you hold up &lt;em&gt;while&lt;/em&gt; building. When you write a new node, the first question isn't "does this code run?" — it's "how will I know if this node is doing the right thing next Tuesday, in production, on a query I haven't seen?" When you tune a prompt, you don't eyeball three examples and ship on a good feeling; you run the suite and read the diff.&lt;/p&gt;

&lt;p&gt;That's the whole difference between a demo and a system you can put your name on. Without an eval layer you're steering on vibes, and vibes don't survive contact with real traffic. With one, every decision has evidence under it. Build it early, treat it as first-class engineering rather than QA cleanup, and never push a change to production without knowing what your scores say about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before first deploy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Eval criteria written down for each capability&lt;/li&gt;
&lt;li&gt;[ ] Every node instrumented with tracing&lt;/li&gt;
&lt;li&gt;[ ] Tier-1 unit evals live (schema, constraints, latency)&lt;/li&gt;
&lt;li&gt;[ ] Regression suite built (50+ curated examples)&lt;/li&gt;
&lt;li&gt;[ ] Green/yellow/red thresholds set for every metric&lt;/li&gt;
&lt;li&gt;[ ] Regression suite wired into CI/CD&lt;/li&gt;
&lt;li&gt;[ ] Baseline scores locked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weekly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Human review of a random 5–10% production sample&lt;/li&gt;
&lt;li&gt;[ ] Post-mortem on every red-zone incident&lt;/li&gt;
&lt;li&gt;[ ] New failure cases folded into the suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quarterly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Suite audit — cut stale cases, close coverage gaps&lt;/li&gt;
&lt;li&gt;[ ] Re-calibrate LLM judges against fresh human labels&lt;/li&gt;
&lt;li&gt;[ ] Revisit thresholds — still the right lines?&lt;/li&gt;
&lt;li&gt;[ ] Run a red-team exercise and act on what breaks&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>systemdesign</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>I Thought Dimensionality Reduction Belonged to Classical ML. Then It Changed How I Think About AI.</title>
      <dc:creator>Sivananda Panda</dc:creator>
      <pubDate>Tue, 23 Jun 2026 06:48:57 +0000</pubDate>
      <link>https://dev.to/sivananda_panda_4812903b4/-i-thought-dimensionality-reduction-belonged-to-classical-ml-then-it-changed-how-i-think-about-ai-1g76</link>
      <guid>https://dev.to/sivananda_panda_4812903b4/-i-thought-dimensionality-reduction-belonged-to-classical-ml-then-it-changed-how-i-think-about-ai-1g76</guid>
      <description>&lt;p&gt;In my previous article, I explored several dimensionality reduction techniques, including PCA, t-SNE, UMAP, LDA, Sammon Mapping, KNN Graphs, and Autoencoders.&lt;/p&gt;

&lt;p&gt;Going into that project, my goal was fairly simple.&lt;/p&gt;

&lt;p&gt;I wanted to understand how these algorithms worked, where they were useful, and whether they could improve model performance.&lt;/p&gt;

&lt;p&gt;Like many people learning machine learning, I saw dimensionality reduction as a classical ML topic.&lt;/p&gt;

&lt;p&gt;Something you learn alongside feature engineering and data preprocessing.&lt;/p&gt;

&lt;p&gt;Useful knowledge, but not something I expected to connect to modern AI systems.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;Not because PCA powers Large Language Models.&lt;/p&gt;

&lt;p&gt;Not because dimensionality reduction is secretly the most important topic in machine learning.&lt;/p&gt;

&lt;p&gt;But because the project forced me to think about a question I hadn't considered before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;While comparing different algorithms, I noticed something strange.&lt;/p&gt;

&lt;p&gt;The same dataset could look completely different depending on the technique I used.&lt;/p&gt;

&lt;p&gt;PCA produced one view.&lt;/p&gt;

&lt;p&gt;t-SNE produced another.&lt;/p&gt;

&lt;p&gt;UMAP showed patterns that weren't obvious in either of them.&lt;/p&gt;

&lt;p&gt;Autoencoders created their own representation altogether.&lt;/p&gt;

&lt;p&gt;At first, I was focused on which visualization looked better.&lt;/p&gt;

&lt;p&gt;Then I started asking a different question:&lt;/p&gt;

&lt;p&gt;If all of these algorithms are looking at exactly the same data, what are they actually trying to preserve?&lt;/p&gt;

&lt;p&gt;That question turned out to be far more interesting than finding the "best" dimensionality reduction algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  PCA Isn't Really About Reducing Dimensions
&lt;/h2&gt;

&lt;p&gt;Most tutorials introduce PCA as a way to reduce features.&lt;/p&gt;

&lt;p&gt;For example, a dataset with 100 features might be compressed into 10 principal components while retaining most of the variance.&lt;/p&gt;

&lt;p&gt;That explanation is correct.&lt;/p&gt;

&lt;p&gt;But while experimenting with PCA, I realized I was focusing on the wrong part of the process.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't that 100 dimensions became 10.&lt;/p&gt;

&lt;p&gt;The interesting part was that the transformed data still retained much of the structure of the original dataset.&lt;/p&gt;

&lt;p&gt;Some information was discarded.&lt;/p&gt;

&lt;p&gt;Some information was preserved.&lt;/p&gt;

&lt;p&gt;The algorithm had effectively made a decision about what mattered.&lt;/p&gt;

&lt;p&gt;And that idea kept showing up across other dimensionality reduction techniques.&lt;/p&gt;

&lt;p&gt;Each algorithm compressed the data differently because each algorithm had a different definition of what should be preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment Things Started Clicking
&lt;/h2&gt;

&lt;p&gt;The more I explored these techniques, the less I thought about dimensions and the more I thought about representations.&lt;/p&gt;

&lt;p&gt;PCA preserves variance.&lt;/p&gt;

&lt;p&gt;t-SNE focuses heavily on local neighborhoods.&lt;/p&gt;

&lt;p&gt;UMAP attempts to maintain structural relationships.&lt;/p&gt;

&lt;p&gt;Autoencoders learn their own compressed representation from data.&lt;/p&gt;

&lt;p&gt;Different algorithms.&lt;/p&gt;

&lt;p&gt;Different mathematics.&lt;/p&gt;

&lt;p&gt;Different outputs.&lt;/p&gt;

&lt;p&gt;But they all seemed to revolve around the same challenge:&lt;/p&gt;

&lt;p&gt;How do you transform information into a form that still captures the important patterns?&lt;/p&gt;

&lt;p&gt;Once I started thinking that way, I began noticing the same idea outside dimensionality reduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond Classical Machine Learning
&lt;/h2&gt;

&lt;p&gt;One thing that often happens when you're learning machine learning is that topics get placed into separate mental boxes.&lt;/p&gt;

&lt;p&gt;Classical ML.&lt;/p&gt;

&lt;p&gt;Deep Learning.&lt;/p&gt;

&lt;p&gt;LLMs.&lt;/p&gt;

&lt;p&gt;Recommendation Systems.&lt;/p&gt;

&lt;p&gt;Computer Vision.&lt;/p&gt;

&lt;p&gt;NLP.&lt;/p&gt;

&lt;p&gt;They can feel like completely different worlds.&lt;/p&gt;

&lt;p&gt;But sometimes the same ideas appear in all of them.&lt;/p&gt;

&lt;p&gt;Take image classification.&lt;/p&gt;

&lt;p&gt;A neural network doesn't look at an image the same way throughout the entire model.&lt;/p&gt;

&lt;p&gt;Early layers respond to simple patterns.&lt;/p&gt;

&lt;p&gt;Edges.&lt;/p&gt;

&lt;p&gt;Textures.&lt;/p&gt;

&lt;p&gt;Basic shapes.&lt;/p&gt;

&lt;p&gt;Deeper layers work with increasingly abstract representations.&lt;/p&gt;

&lt;p&gt;By the time the model makes a prediction, it is operating on something very different from the original pixels.&lt;/p&gt;

&lt;p&gt;The representation has changed multiple times.&lt;/p&gt;

&lt;p&gt;The model has transformed the data into a form that makes the task easier.&lt;/p&gt;

&lt;p&gt;That sounded surprisingly familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I Started Looking at LLMs
&lt;/h2&gt;

&lt;p&gt;The same thing happened when I started learning more about embeddings.&lt;/p&gt;

&lt;p&gt;Consider the words "car" and "vehicle."&lt;/p&gt;

&lt;p&gt;They are different words, yet most embedding models place them relatively close together in vector space.&lt;/p&gt;

&lt;p&gt;The model isn't storing dictionary definitions.&lt;/p&gt;

&lt;p&gt;It is learning a representation that captures part of the relationship between those words.&lt;/p&gt;

&lt;p&gt;The exact mechanism is very different from PCA.&lt;/p&gt;

&lt;p&gt;The mathematics is different.&lt;/p&gt;

&lt;p&gt;The scale is different.&lt;/p&gt;

&lt;p&gt;But the underlying idea felt familiar.&lt;/p&gt;

&lt;p&gt;Once again, information was being transformed into a representation that preserved what mattered for the task.&lt;/p&gt;

&lt;p&gt;That was the connection I hadn't expected when I started learning dimensionality reduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Mental Model
&lt;/h2&gt;

&lt;p&gt;Many beginners think about machine learning like this:&lt;/p&gt;

&lt;p&gt;Data → Model → Prediction&lt;/p&gt;

&lt;p&gt;After working through these dimensionality reduction techniques, I think a more useful mental model is:&lt;/p&gt;

&lt;p&gt;Data → Representation → Model → Prediction&lt;/p&gt;

&lt;p&gt;Because the way information is represented often determines what patterns a model can learn.&lt;/p&gt;

&lt;p&gt;Two systems can work with the same underlying data and arrive at different outcomes simply because they represent that data differently.&lt;/p&gt;

&lt;p&gt;That's true for PCA.&lt;/p&gt;

&lt;p&gt;It's true for Autoencoders.&lt;/p&gt;

&lt;p&gt;It's true for embeddings.&lt;/p&gt;

&lt;p&gt;And it's true for many modern AI systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I started learning dimensionality reduction because I wanted to understand PCA, t-SNE, UMAP, and a few other algorithms.&lt;/p&gt;

&lt;p&gt;What I didn't expect was that those techniques would change how I think about machine learning itself.&lt;/p&gt;

&lt;p&gt;The biggest lesson wasn't about reducing dimensions.&lt;/p&gt;

&lt;p&gt;It wasn't about preprocessing.&lt;/p&gt;

&lt;p&gt;And it wasn't about improving model accuracy.&lt;/p&gt;

&lt;p&gt;It was realizing that many AI systems, regardless of how different they appear on the surface, spend a significant amount of effort answering the same question:&lt;/p&gt;

&lt;p&gt;How should information be represented so that useful patterns become easier to discover?&lt;/p&gt;

&lt;p&gt;For me, dimensionality reduction was the first place where that idea became visible.&lt;/p&gt;

&lt;p&gt;And once I noticed it, I started seeing it everywhere.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Compressed 784 Dimensions Into 2. Here's What 70,000 Handwritten Digits Actually Look Like</title>
      <dc:creator>Sivananda Panda</dc:creator>
      <pubDate>Mon, 22 Jun 2026 08:59:01 +0000</pubDate>
      <link>https://dev.to/sivananda_panda_4812903b4/i-compressed-784-dimensions-into-2-heres-what-70000-handwritten-digits-actually-look-like-4ng2</link>
      <guid>https://dev.to/sivananda_panda_4812903b4/i-compressed-784-dimensions-into-2-heres-what-70000-handwritten-digits-actually-look-like-4ng2</guid>
      <description>&lt;h2&gt;
  
  
  PCA Didn't Improve My Model. It Changed How I Think About Data Instead.
&lt;/h2&gt;

&lt;p&gt;When I ran PCA on a dataset I was exploring, I expected a fairly straightforward outcome.&lt;/p&gt;

&lt;p&gt;Reduce dimensionality.&lt;/p&gt;

&lt;p&gt;Remove noise.&lt;/p&gt;

&lt;p&gt;Train the model again.&lt;/p&gt;

&lt;p&gt;Get better performance.&lt;/p&gt;

&lt;p&gt;That's the story dimensionality reduction is often associated with.&lt;/p&gt;

&lt;p&gt;The reality was much less exciting.&lt;/p&gt;

&lt;p&gt;The accuracy barely moved.&lt;/p&gt;

&lt;p&gt;At first, I treated PCA as a failed experiment.&lt;/p&gt;

&lt;p&gt;Looking back, the failed experiment was actually my mental model.&lt;/p&gt;

&lt;p&gt;I had been focused on improving the model without understanding something more fundamental:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What did the data actually look like?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question eventually led me into one of the most interesting machine learning rabbit holes I've explored so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Didn't Realize I Had
&lt;/h2&gt;

&lt;p&gt;Like most practitioners, I started with exploratory data analysis.&lt;/p&gt;

&lt;p&gt;I checked distributions.&lt;/p&gt;

&lt;p&gt;Looked for missing values.&lt;/p&gt;

&lt;p&gt;Analyzed correlations.&lt;/p&gt;

&lt;p&gt;Built baseline models.&lt;/p&gt;

&lt;p&gt;Reviewed performance metrics.&lt;/p&gt;

&lt;p&gt;All useful activities.&lt;/p&gt;

&lt;p&gt;But none of them answered a question that suddenly felt important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If every sample is represented as hundreds of features, what shape does this data actually have?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Machine learning models operate in spaces that humans can't visualize.&lt;/p&gt;

&lt;p&gt;A dataset with 500 features exists in a 500-dimensional space.&lt;/p&gt;

&lt;p&gt;A dataset with 1000 features exists in a 1000-dimensional space.&lt;/p&gt;

&lt;p&gt;We can calculate distances in those spaces.&lt;/p&gt;

&lt;p&gt;We can train models in those spaces.&lt;/p&gt;

&lt;p&gt;But we can't intuitively understand them.&lt;/p&gt;

&lt;p&gt;And yet many of the questions we care about are geometric in nature.&lt;/p&gt;

&lt;p&gt;Are classes naturally separable?&lt;/p&gt;

&lt;p&gt;Are there meaningful clusters?&lt;/p&gt;

&lt;p&gt;Are there outliers?&lt;/p&gt;

&lt;p&gt;Do the samples lie on some hidden structure?&lt;/p&gt;

&lt;p&gt;The information already exists in the data.&lt;/p&gt;

&lt;p&gt;The challenge is making it visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discovering a Better Playground
&lt;/h2&gt;

&lt;p&gt;While reading about dimensionality reduction, I came across Christopher Olah's brilliant article on visualizing the MNIST dataset.&lt;/p&gt;

&lt;p&gt;For anyone unfamiliar with it, MNIST contains handwritten digits from 0 to 9.&lt;/p&gt;

&lt;p&gt;Each image is only 28×28 pixels.&lt;/p&gt;

&lt;p&gt;That sounds tiny.&lt;/p&gt;

&lt;p&gt;But once flattened, every image becomes a point in a 784-dimensional space.&lt;/p&gt;

&lt;p&gt;Each handwritten digit is represented by 784 numbers.&lt;/p&gt;

&lt;p&gt;Humans can't visualize 784 dimensions.&lt;/p&gt;

&lt;p&gt;Dimensionality reduction algorithms can help us project that space into something we can see.&lt;/p&gt;

&lt;p&gt;What fascinated me wasn't the mathematics.&lt;/p&gt;

&lt;p&gt;It was the possibility that different algorithms might reveal different aspects of the same dataset.&lt;/p&gt;

&lt;p&gt;So I downloaded MNIST and started experimenting.&lt;/p&gt;

&lt;p&gt;What began as curiosity quickly turned into a project.&lt;/p&gt;

&lt;p&gt;I implemented and compared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PCA&lt;/li&gt;
&lt;li&gt;LDA&lt;/li&gt;
&lt;li&gt;t-SNE&lt;/li&gt;
&lt;li&gt;UMAP&lt;/li&gt;
&lt;li&gt;Sammon Mapping&lt;/li&gt;
&lt;li&gt;KNN Graph Visualizations&lt;/li&gt;
&lt;li&gt;Autoencoders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My expectation was simple.&lt;/p&gt;

&lt;p&gt;Different algorithms would produce slightly different versions of the same visualization.&lt;/p&gt;

&lt;p&gt;I was completely wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Interesting Question
&lt;/h2&gt;

&lt;p&gt;After generating the first set of visualizations, I found myself staring at the outputs, wondering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which one of these is actually correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;PCA showed one picture.&lt;/p&gt;

&lt;p&gt;t-SNE showed another.&lt;/p&gt;

&lt;p&gt;UMAP showed something different again.&lt;/p&gt;

&lt;p&gt;The Autoencoder latent space looked completely different from everything else.&lt;/p&gt;

&lt;p&gt;They couldn't all be right.&lt;/p&gt;

&lt;p&gt;Except they were.&lt;/p&gt;

&lt;p&gt;The mistake was assuming they were trying to answer the same question.&lt;/p&gt;

&lt;p&gt;Each algorithm was optimizing for a different definition of "important structure."&lt;/p&gt;

&lt;p&gt;Once I understood that, dimensionality reduction became much more interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  What PCA Actually Taught Me
&lt;/h2&gt;

&lt;p&gt;PCA was the first method I explored because it's usually the default recommendation.&lt;/p&gt;

&lt;p&gt;The intuition is elegant.&lt;/p&gt;

&lt;p&gt;Find the directions where the data varies the most and project onto those directions.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Fast.&lt;/p&gt;

&lt;p&gt;Interpretable.&lt;/p&gt;

&lt;p&gt;What surprised me was that PCA didn't produce the clean digit separation I expected.&lt;/p&gt;

&lt;p&gt;Some digits remained heavily mixed together.&lt;/p&gt;

&lt;p&gt;Initially, this felt disappointing.&lt;/p&gt;

&lt;p&gt;Then I realized PCA had taught me something important.&lt;/p&gt;

&lt;p&gt;Variance is not the same thing as class separation.&lt;/p&gt;

&lt;p&gt;The largest source of variation in a dataset isn't necessarily the information that distinguishes one class from another.&lt;/p&gt;

&lt;p&gt;A thick handwritten "2" and a thin handwritten "2" can contribute substantial variance while still belonging to the same class.&lt;/p&gt;

&lt;p&gt;PCA isn't trying to separate classes.&lt;/p&gt;

&lt;p&gt;It's trying to preserve variance.&lt;/p&gt;

&lt;p&gt;That distinction seems obvious in hindsight, but seeing it visually made the lesson stick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kxr9e4mjbwk96pkpiw8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kxr9e4mjbwk96pkpiw8.png" alt="PCA" width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What LDA Taught Me About Labels
&lt;/h2&gt;

&lt;p&gt;The jump from PCA to LDA was dramatic.&lt;/p&gt;

&lt;p&gt;Suddenly, the classes looked much cleaner.&lt;/p&gt;

&lt;p&gt;The clusters became easier to distinguish.&lt;/p&gt;

&lt;p&gt;The reason wasn't that LDA is universally superior.&lt;/p&gt;

&lt;p&gt;The reason is that LDA has access to information PCA never sees.&lt;/p&gt;

&lt;p&gt;Labels.&lt;/p&gt;

&lt;p&gt;PCA asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where is the variance?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LDA asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can I maximize separation between known classes?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are fundamentally different objectives.&lt;/p&gt;

&lt;p&gt;Running both methods side by side highlighted something important about machine learning in general.&lt;/p&gt;

&lt;p&gt;The information contained in labels is incredibly valuable.&lt;/p&gt;

&lt;p&gt;Once an algorithm knows what you want to separate, it can optimize directly for that objective.&lt;/p&gt;

&lt;p&gt;Without labels, it has to infer structure on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Floj3g4u94ddqnnixyg9r.png" alt=" " width="799" height="378"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What t-SNE Taught Me About Beautiful Visualizations
&lt;/h2&gt;

&lt;p&gt;The first visualization that made me stop and stare was t-SNE.&lt;/p&gt;

&lt;p&gt;The clusters looked incredible.&lt;/p&gt;

&lt;p&gt;Digits formed tight, well-separated groups.&lt;/p&gt;

&lt;p&gt;The output was visually satisfying in a way PCA never was.&lt;/p&gt;

&lt;p&gt;It almost looked as though the dataset had organized itself.&lt;/p&gt;

&lt;p&gt;Then I started reading more about how t-SNE works.&lt;/p&gt;

&lt;p&gt;That's when I learned an important lesson.&lt;/p&gt;

&lt;p&gt;t-SNE prioritizes preserving local neighborhoods.&lt;/p&gt;

&lt;p&gt;Points that are close together remain close together.&lt;/p&gt;

&lt;p&gt;Global geometry becomes much less important.&lt;/p&gt;

&lt;p&gt;This means something subtle but important.&lt;/p&gt;

&lt;p&gt;The clusters themselves are often meaningful.&lt;/p&gt;

&lt;p&gt;The distances between clusters are often not.&lt;/p&gt;

&lt;p&gt;Humans naturally assume that if Cluster A is closer to Cluster B than Cluster C, then A and B must be more similar.&lt;/p&gt;

&lt;p&gt;With t-SNE, that assumption can easily be wrong.&lt;/p&gt;

&lt;p&gt;The experience taught me something I now apply beyond dimensionality reduction.&lt;/p&gt;

&lt;p&gt;The most visually impressive result isn't always the most informative one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F840gner3j1tlqoko0i8w.png" alt=" " width="774" height="705"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why UMAP Felt Different
&lt;/h2&gt;

&lt;p&gt;After seeing the extremes of PCA and t-SNE, UMAP felt like the first algorithm that was trying to strike a balance rather than optimize a single objective.&lt;/p&gt;

&lt;p&gt;PCA focuses on preserving variance.&lt;/p&gt;

&lt;p&gt;t-SNE focuses heavily on preserving local neighborhoods.&lt;/p&gt;

&lt;p&gt;UMAP sits somewhere in between.&lt;/p&gt;

&lt;p&gt;The underlying assumption behind UMAP is that high-dimensional data often lies on a lower-dimensional manifold. Instead of asking where the variance is largest, UMAP asks a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which points genuinely belong together, and what hidden structure could explain those relationships?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For my experiments, I used 15 neighbors, a cosine distance metric, and projected the data into three dimensions. These choices turned out to be important.&lt;/p&gt;

&lt;p&gt;Using 15 neighbors meant that each digit considered a reasonably sized local neighborhood when constructing the manifold. If I had chosen a much smaller value, the visualization would have focused almost entirely on local structure, producing tighter but potentially misleading clusters. A much larger value would have emphasized global relationships at the expense of local detail. Fifteen felt like a practical middle ground.&lt;/p&gt;

&lt;p&gt;The cosine distance metric was equally interesting. Instead of measuring absolute pixel differences, cosine similarity focuses on whether two images share similar patterns. For handwritten digits, that matters. Two people can write the same digit with different stroke thicknesses and intensities, yet humans immediately recognize them as the same shape. Cosine distance captures that intuition surprisingly well.&lt;/p&gt;

&lt;p&gt;What stood out most in the visualization was that the clusters remained well-defined without feeling artificially separated. Unlike t-SNE, where some clusters appeared isolated islands floating in space, UMAP preserved more of the broader organization of the dataset. Digits with similar visual characteristics often occupied nearby regions, and the overall arrangement felt more coherent.&lt;/p&gt;

&lt;p&gt;The decision to use three dimensions also revealed something I would have missed in a standard 2D plot. Some groups that appeared partially overlapping in two dimensions unfolded more naturally when given an additional degree of freedom. The manifold had more room to express its structure, making the relationships between digits easier to interpret.&lt;/p&gt;

&lt;p&gt;What I appreciated most about UMAP was that it felt less interested in creating the prettiest visualization and more interested in preserving a useful representation of the data. The clusters were slightly less dramatic than t-SNE, but they felt more trustworthy.&lt;/p&gt;

&lt;p&gt;If PCA taught me that variance is not the same as separability, and t-SNE taught me to be cautious of beautiful plots, UMAP taught me that understanding data often requires balancing local detail with global structure. That balance is probably why UMAP has become the default visualization tool for many machine learning practitioners today.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l393162cxa8kofdgb95.png" alt=" " width="799" height="419"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Surprisingly Interesting Sammon Mapping
&lt;/h2&gt;

&lt;p&gt;Before this project, I had barely encountered Sammon Mapping.&lt;/p&gt;

&lt;p&gt;Compared to PCA or t-SNE, it's rarely discussed.&lt;/p&gt;

&lt;p&gt;After experimenting with it, I think that's unfortunate.&lt;/p&gt;

&lt;p&gt;Sammon Mapping attempts to preserve pairwise distances during projection.&lt;/p&gt;

&lt;p&gt;In other words, it's trying to stay faithful to the geometry of the original space.&lt;/p&gt;

&lt;p&gt;The trade-off becomes obvious immediately.&lt;/p&gt;

&lt;p&gt;It's computationally expensive.&lt;/p&gt;

&lt;p&gt;The visualizations aren't as dramatic.&lt;/p&gt;

&lt;p&gt;The clusters don't explode apart like they do with t-SNE.&lt;/p&gt;

&lt;p&gt;But that's exactly the point.&lt;/p&gt;

&lt;p&gt;Sammon Mapping is optimizing for honesty rather than visual appeal.&lt;/p&gt;

&lt;p&gt;That made it one of the most interesting methods in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2anhbvn7vikb0qzzlc6.png" alt=" " width="800" height="730"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  When the KNN Graph Changed My Perspective
&lt;/h2&gt;

&lt;p&gt;Most dimensionality reduction techniques represent data as points.&lt;/p&gt;

&lt;p&gt;KNN Graphs represent data as relationships.&lt;/p&gt;

&lt;p&gt;That sounds like a small difference.&lt;/p&gt;

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

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which cluster does this point belong to?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found myself asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which points connect different regions of the dataset?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The graph exposed bridge points, ambiguous digits, and unusual samples that were much harder to notice in traditional scatter plots.&lt;/p&gt;

&lt;p&gt;It shifted my focus away from clusters and toward connectivity.&lt;/p&gt;

&lt;p&gt;For exploratory analysis, that perspective can be incredibly valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfsnkofy8936ce90n9c3.png" alt=" " width="800" height="334"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Autoencoders Revealed
&lt;/h2&gt;

&lt;p&gt;The Autoencoder was where the project started feeling less like classical machine learning and more like modern AI.&lt;/p&gt;

&lt;p&gt;Unlike PCA or t-SNE, the Autoencoder isn't applying a predefined projection rule.&lt;/p&gt;

&lt;p&gt;It's learning a representation.&lt;/p&gt;

&lt;p&gt;The network compresses the input into a latent space and then attempts to reconstruct the original image.&lt;/p&gt;

&lt;p&gt;To succeed, it must learn which information matters.&lt;/p&gt;

&lt;p&gt;The resulting latent space felt fundamentally different from the classical methods.&lt;/p&gt;

&lt;p&gt;It wasn't simply a compressed version of the original data.&lt;/p&gt;

&lt;p&gt;It was a learned representation of the data.&lt;/p&gt;

&lt;p&gt;The structure felt smooth.&lt;/p&gt;

&lt;p&gt;Continuous.&lt;/p&gt;

&lt;p&gt;Almost as though the digits existed on an underlying manifold rather than as isolated clusters.&lt;/p&gt;

&lt;p&gt;For the first time, I could see why latent representations became such an important idea in deep learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluw84zvma78ut191q8nq.png" alt=" " width="800" height="402"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Lesson I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;I started this journey because PCA didn't improve a model.&lt;/p&gt;

&lt;p&gt;I finished it with a completely different appreciation for exploratory data analysis.&lt;/p&gt;

&lt;p&gt;The most important insight wasn't that one dimensionality reduction technique is better than another.&lt;/p&gt;

&lt;p&gt;It was that every technique reveals a different aspect of the data.&lt;/p&gt;

&lt;p&gt;PCA reveals variance.&lt;/p&gt;

&lt;p&gt;LDA reveals separability.&lt;/p&gt;

&lt;p&gt;t-SNE reveals neighborhoods.&lt;/p&gt;

&lt;p&gt;UMAP balances local and global structure.&lt;/p&gt;

&lt;p&gt;Sammon Mapping reveals geometry.&lt;/p&gt;

&lt;p&gt;KNN Graphs reveal connectivity.&lt;/p&gt;

&lt;p&gt;Autoencoders reveal learned representations.&lt;/p&gt;

&lt;p&gt;The algorithms weren't competing.&lt;/p&gt;

&lt;p&gt;They were answering different questions.&lt;/p&gt;

&lt;p&gt;And that's probably the biggest lesson I took away from the project.&lt;/p&gt;

&lt;p&gt;Before spending weeks tuning hyperparameters or experimenting with new models, it's worth asking a simpler question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I actually understand the shape of my data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the fastest way to improve a model isn't another optimization trick.&lt;/p&gt;

&lt;p&gt;It's developing a better intuition for the space your data lives in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore the Project
&lt;/h2&gt;

&lt;p&gt;I open-sourced the entire project so anyone can experiment with the visualizations themselves.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/siva-rgb/Dim_Reduction" rel="noopener noreferrer"&gt;https://github.com/siva-rgb/Dim_Reduction&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run it, I'd encourage you to spend less time looking for the "best" dimensionality reduction technique and more time asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is each technique trying to tell me about the data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where the interesting insights usually begin.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>computerscience</category>
      <category>community</category>
    </item>
  </channel>
</rss>
