<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Hamel&#39;s Blog</title>
<link>https://glittering-puffpuff-87102e.netlify.app/blog/secret.html</link>
<atom:link href="https://glittering-puffpuff-87102e.netlify.app/blog/secret.xml" rel="self" type="application/rss+xml"/>
<description>Notes on applied AI engineering, machine learning, and data science.</description>
<image>
<url>https://glittering-puffpuff-87102e.netlify.app/quarto.png</url>
<title>Hamel&#39;s Blog</title>
<link>https://glittering-puffpuff-87102e.netlify.app/blog/secret.html</link>
<height>81</height>
<width>144</width>
</image>
<generator>quarto-1.9.38</generator>
<lastBuildDate>Mon, 29 Jun 2026 07:00:00 GMT</lastBuildDate>
<item>
  <title>“It’s Hard to Eval” Is a Product Smell</title>
  <dc:creator>Hamel Husain</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-smell/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->




<!-- Shared styles for the inline figure mockups. Included once near the top of index.qmd. -->
<style>
.da-fig {
  max-width: 600px; margin: 1.6rem auto;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* proof-surface tint: neutrals nudged toward one hue at low chroma.
     Change --proof-hue (and --proof-chroma) to retint the whole panel. Hue 75 = warm stone. */
  --proof-hue: 75;
  --proof-chroma: 1;
  --proof-surface: oklch(0.985 calc(0.006 * var(--proof-chroma)) var(--proof-hue));
  /* the disclosure header is a control: step it darker + a touch warmer than the
     surface so it reads as a distinct band, not part of the body */
  --proof-header: oklch(0.945 calc(0.02 * var(--proof-chroma)) var(--proof-hue));
  --proof-header-hover: oklch(0.93 calc(0.024 * var(--proof-chroma)) var(--proof-hue));
  --proof-border: oklch(0.915 calc(0.014 * var(--proof-chroma)) var(--proof-hue));
  --proof-line: oklch(0.93 calc(0.009 * var(--proof-chroma)) var(--proof-hue));
  --proof-track: oklch(0.93 calc(0.012 * var(--proof-chroma)) var(--proof-hue));
}
.da-fig__win { border: 1px solid #d9dce1; border-radius: 12px; overflow: hidden; background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.da-fig__bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: #f5f6f8; border-bottom: 1px solid #e6e8ec; font-size: 13px; font-weight: 600; color: #3b3f46; }
.da-fig__lights { display: flex; gap: 6px; align-items: center; }
.da-fig__light { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
.da-fig__light--r { background: #ff5f57; }
.da-fig__light--y { background: #febc2e; }
.da-fig__light--g { background: #28c840; }
.da-fig__title { margin-left: 4px; }
.da-fig__body { padding: 18px 16px; background: #ffffff; }
.da-fig__user { display: flex; justify-content: flex-end; margin-bottom: 16px; }
.da-fig__user span { background: #eef1f6; color: #1f2329; padding: 9px 13px; border-radius: 14px 14px 4px 14px; font-size: 14px; max-width: 80%; }
.da-fig__bot { color: #1f2329; font-size: 15px; line-height: 1.5; }
.da-fig__bot strong { font-weight: 700; }
.da-fig__input { display: flex; align-items: center; gap: 8px; margin-top: 18px; padding: 10px 13px; border: 1px solid #e2e4e9; border-radius: 10px; color: #9aa0a8; font-size: 13px; background: #fafbfc; }
.da-fig__send { margin-left: auto; color: #c2c6cc; }
.da-fig__cap { text-align: center; font-size: 13px; color: #6b7079; margin-top: 10px; font-style: italic; }

/* "after" figure: the answer is primary, the proof is a quiet subsection */
.da-ans { font-size: 16px; color: #1f2329; line-height: 1.5; }
.da-ans strong { font-weight: 700; }
/* a heads-up on the answer itself: some inputs could not be verified, check details */
.da-ans__info { color: #d4493c; font-size: 14px; font-weight: 700; margin-left: 5px; cursor: help; vertical-align: 1px; }
/* the detail card is subordinate to the answer: indented, with a dotted thread that drops and curves into it */
.da-sub { position: relative; margin: 14px 0 0 26px; }
.da-thread { position: absolute; left: -20px; top: -14px; width: 22px; height: 46px; overflow: visible; }
.da-thread path { stroke: var(--proof-border); }

/* verification subsection: a tinted, low-elevation card with a clickable header bar */
.da-verify { margin: 0; border: 1px solid var(--proof-border); border-radius: 12px; overflow: hidden; background: var(--proof-surface); box-shadow: 0 1px 2px rgba(40, 40, 50, 0.05); }
.da-verify > summary { cursor: pointer; list-style: none; display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; background: var(--proof-header); border-bottom: 1px solid var(--proof-line); font-size: 13px; }
.da-verify > summary:hover { background: var(--proof-header-hover); }
.da-verify > summary::-webkit-details-marker { display: none; }
.da-verify > summary::after { content: ""; width: 7px; height: 7px; border-right: 1.5px solid #6b7079; border-bottom: 1.5px solid #6b7079; transform: rotate(45deg); margin: -3px 4px 0 0; transition: transform 0.15s ease; }
.da-verify:not([open]) > summary::after { transform: rotate(-45deg); }
.da-verify__label { font-weight: 600; color: #1f2329; font-size: 13.5px; }
.da-verify__body { padding: 2px 14px 12px; }

.da-group { background: #ffffff; border: 1px solid var(--proof-border); border-radius: 9px; padding: 1px 12px 5px; margin-top: 9px; box-shadow: 0 1px 2px rgba(40, 40, 50, 0.04); }
.da-group:first-child { margin-top: 5px; }
.da-group__head { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: #8a909a; font-weight: 700; padding: 8px 0 7px; border-bottom: 1px solid var(--proof-line); }

.da-step { display: grid; grid-template-columns: 1fr auto; gap: 0 10px; align-items: start; padding: 9px 0 9px 16px; font-size: 13px; }
.da-step--flag { border-left: 2px solid #e08379; padding-left: 14px; }
.da-step + .da-step { border-top: 1px solid var(--proof-line); }
.da-squig { text-decoration: none; }
.da-info { color: #d4493c; font-size: 12.5px; font-weight: 700; margin-left: 4px; }
.da-step__label { color: #797f87; }
.da-step__val { color: #2a2d33; font-weight: 400; margin-left: 8px; }
.da-step__check { color: #9aa0a8; text-align: right; }
.da-step__src { color: #6b7079; text-decoration: underline; text-decoration-color: #c4c9d0; text-underline-offset: 2px; }

/* region breakdown: shown inline as part of the expanded view */
.da-dist { margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--proof-line); }
.da-chart { margin: 4px 0 6px; }
.da-chart__cap { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #9aa0a8; margin: 6px 0 7px; }
.da-bar { display: grid; grid-template-columns: 92px 1fr 58px; align-items: center; gap: 10px; margin: 6px 0; font-size: 12.5px; }
.da-bar__label { color: #6b7079; }
.da-bar__track { background: var(--proof-track); border-radius: 4px; height: 14px; overflow: hidden; }
.da-bar__fill { display: block; background: #5b8def; height: 100%; border-radius: 4px; }
.da-bar__val { text-align: right; color: #3b3f46; font-weight: 600; }

/* tools, quiet */
.da-actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
/* the primary action: a raised white button with dark text. The white fill and
   lift separate it from the warm panel without the shout of a solid dark button. */
.da-btn { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; color: #2a2d33; background: #fff; border: 1px solid #c7ccd3; border-radius: 7px; padding: 7px 13px; box-shadow: 0 1px 3px rgba(40, 40, 50, 0.10); }

/* provenance: the answer was built from a vetted analysis a colleague authored.
   Warm tint and an open affordance, echoing the vetted-lesson-plan idea elsewhere. */
/* a white card lifts it off the warm panel like the data groups, so it belongs;
   a small notebook glyph and the amber "Open" link are the only distinguishing cues. */
.da-prov { display: flex; align-items: center; gap: 10px; margin: 2px 0 11px; padding: 11px 13px; background: #fff; border: 1px solid var(--proof-border); border-radius: 9px; box-shadow: 0 1px 2px rgba(40,40,50,0.05); text-decoration: none; }
.da-prov__ic { flex: none; width: 15px; height: 15px; color: #9aa0a8; }
.da-prov__ic svg { display: block; width: 15px; height: 15px; }
.da-prov__main { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.da-prov__title { font-size: 12.5px; color: #2a2d33; font-weight: 400; }
.da-prov__by { font-size: 11px; color: #8a909a; margin-top: 1px; }
.da-prov__open { margin-left: auto; font-size: 11.5px; font-weight: 600; color: #9a7a30; white-space: nowrap; }

/* ===========================================================================
   PE Lesson Planner: a desktop app. Deliberately distinct from the data agent
   (window chrome with traffic lights, split panes, a teal accent, wider canvas).
   =========================================================================== */
.pe-figure { width: 100%; margin: 1.8rem auto; }
.pe-app {
  --pe-accent: #1f7a6b;
  /* warm-stone proof tint, scoped here so the lineage card, avatar, chips and
     changed-line highlight read as a differentiated surface (same hue as Example 1). */
  --proof-hue: 75;
  --proof-chroma: 1;
  --proof-surface: oklch(0.985 calc(0.006 * var(--proof-chroma)) var(--proof-hue));
  --proof-border: oklch(0.915 calc(0.014 * var(--proof-chroma)) var(--proof-hue));
  --proof-line: oklch(0.93 calc(0.009 * var(--proof-chroma)) var(--proof-hue));
  width: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  border: 1px solid #d4d7dd; border-radius: 10px; overflow: hidden;
  background: #ffffff; box-shadow: 0 6px 22px rgba(30, 32, 40, 0.10);
}
.pe-app__bar { display: flex; align-items: center; gap: 11px; padding: 9px 14px; background: #eceef1; border-bottom: 1px solid #dfe2e7; }
.pe-app__lights { display: flex; gap: 6px; }
.pe-light { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
.pe-light--r { background: #ff5f57; }
.pe-light--y { background: #febc2e; }
.pe-light--g { background: #28c840; }
.pe-app__title { font-size: 12.5px; font-weight: 600; color: #5b6068; }
.pe-app__cap { text-align: center; font-size: 13px; color: #6b7079; margin-top: 11px; font-style: italic; }

/* shared plan document rendering */
.pe-doc__header { display: flex; align-items: baseline; gap: 9px; margin-bottom: 15px; }
.pe-doc__tag { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5059; }
.pe-doc__title { font-size: 17px; font-weight: 700; color: #1f2329; }
.pe-doc__sub { font-size: 12px; color: #5b6068; }
.pe-doc__header .pe-doc__tag { margin-left: auto; }
.pe-scroll { overflow-y: auto; padding-right: 10px; }
.pe-scroll::-webkit-scrollbar { width: 10px; }
.pe-scroll::-webkit-scrollbar-track { background: #f1f2f4; border-radius: 6px; }
.pe-scroll::-webkit-scrollbar-thumb { background: #c4c9d0; border-radius: 6px; border: 2px solid #f1f2f4; }
.pe-fill { position: relative; flex: 1; min-height: 0; }
.pe-fill .pe-scroll { position: absolute; inset: 0; }
.pe-sec + .pe-sec { margin-top: 14px; }
.pe-sec__head { font-size: 13px; font-weight: 700; color: #1f2329; margin-bottom: 4px; }
.pe-sec p { font-size: 13.5px; line-height: 1.55; color: #3b3f46; margin: 0; }

/* "before": inputs panel on the left, generated plan on the right */
.pe-app__split { display: grid; grid-template-columns: 268px 1fr; }
.pe-inputs { padding: 18px 18px 20px; background: #f7f8fa; border-right: 1px solid #e6e8ec; }
.pe-inputs__head { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5059; margin-bottom: 13px; }
.pe-field { display: flex; flex-direction: column; gap: 4px; }
.pe-field + .pe-field { margin-top: 11px; }
.pe-field__label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5059; }
.pe-field__input { display: flex; align-items: center; justify-content: space-between; font-size: 13.5px; color: #1f2329; background: #ffffff; border: 1px solid #e2e4e9; border-radius: 8px; padding: 8px 11px; }
.pe-field__caret { color: #b4b9c0; font-size: 11px; margin-left: 8px; }
.pe-generate { display: block; text-align: center; margin-top: 16px; font-size: 13.5px; font-weight: 600; color: #ffffff; background: var(--pe-accent); border-radius: 8px; padding: 10px 16px; }
.pe-doc { padding: 20px 22px; display: flex; flex-direction: column; min-height: 0; }

/* "after": lineage header, then a split of the full plan and its diff */
.pe-app__body { padding: 18px 20px 20px; }
.pe-src { position: relative; border: 1px solid var(--proof-border); border-radius: 12px; background: var(--proof-surface); padding: 15px; text-align: center; }
.pe-src__id { display: inline-flex; align-items: center; justify-content: center; gap: 9px; }
.pe-src__avatar { flex: none; width: 30px; height: 30px; border-radius: 50%; background: oklch(0.88 0.035 var(--proof-hue)); color: #4b5059; font-size: 11.5px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
.pe-src__name { font-size: 14px; font-weight: 600; color: #1f2329; }
.pe-src__meta { font-size: 12px; color: #6b7079; margin-top: 3px; }
.pe-src__signals { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; justify-content: center; }
.pe-sig { font-size: 11.5px; color: #4b5059; background: #ffffff; border: 1px solid #d3d7dd; border-radius: 999px; padding: 4px 11px; }
.pe-src__link { position: absolute; top: 14px; right: 15px; font-size: 12.5px; color: var(--pe-accent); text-decoration: none; font-weight: 500; }

.pe-split2 { display: grid; grid-template-columns: 1fr 1fr; margin-top: 16px; border: 1px solid #e6e8ec; border-radius: 10px; overflow: hidden; }
.pe-pane { padding: 14px 16px; }
.pe-pane--full { border-right: 1px solid #e6e8ec; display: flex; flex-direction: column; min-height: 0; }
.pe-pane__head { display: flex; align-items: center; justify-content: space-between; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5059; margin-bottom: 11px; }

/* full plan, with changed lines marked to match the diff */
.pe-line__sec { font-size: 13px; font-weight: 700; color: #1f2329; margin: 13px 0 5px; }
.pe-line__sec:first-child { margin-top: 0; }
.pe-line { font-size: 13px; line-height: 1.5; color: #3b3f46; }
.pe-line + .pe-line { margin-top: 7px; }
.pe-line--changed { display: flex; gap: 9px; align-items: baseline; border-left: 2px solid #d7dbe1; padding: 3px 0 3px 11px; }
.pe-line--changed + .pe-line { margin-top: 9px; }
.pe-badge { flex: none; width: 16px; height: 16px; border-radius: 50%; background: #eaecef; color: #5b6068; font-size: 10px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }

/* diff pane, each group keyed to a badge in the full plan */
.pe-dgroup + .pe-dgroup { margin-top: 13px; padding-top: 13px; border-top: 1px solid #eef0f3; }
.pe-dgroup__head { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 700; color: #1f2329; margin-bottom: 7px; }
.pe-drow { display: grid; grid-template-columns: 14px 1fr; gap: 7px; align-items: baseline; font-size: 12.5px; padding: 4px 8px; border-radius: 5px; }
.pe-drow__mark { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; text-align: center; }
.pe-drow--del { background: #fbecea; }
.pe-drow--del .pe-drow__text { color: #9aa0a8; text-decoration: line-through; }
.pe-drow--del .pe-drow__mark { color: #c2705f; }
.pe-drow--add { background: #eaf6ee; margin-top: 3px; }
.pe-drow--add .pe-drow__text { color: #2f3338; }
.pe-drow--add .pe-drow__mark { color: #3fae6a; }
.pe-dwhy { font-size: 11.5px; color: #8a909a; margin-top: 5px; padding-left: 8px; }

/* Cursor-style review controls: accept / reject per change, plus an accept-all bar */
.pe-review { display: inline-flex; gap: 12px; text-transform: none; letter-spacing: normal; }
.pe-allbtn { font-size: 11.5px; font-weight: 600; cursor: pointer; color: #8a909a; }
.pe-allbtn--accept { color: var(--pe-accent); }
.pe-dactions { display: flex; gap: 8px; margin-top: 10px; padding-left: 8px; }
.pe-dbtn { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: 6px; border: 1px solid #e2e4e9; background: #ffffff; color: #5b6068; cursor: pointer; }
.pe-dbtn--accept { color: #2f7a52; background: #eaf6ee; border-color: #cfe9d8; }
.pe-dbtn--reject { color: #b05c4f; background: #fbecea; border-color: #f0d7d2; }
.pe-dbtn__key { font-size: 10px; opacity: 0.55; font-weight: 400; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ===========================================================================
   ClaimDraft: a workers-comp report tool. A clinical/enterprise desktop app,
   distinct again from the data agent and the PE planner: a slate title bar with
   window controls on the right, a classic menu bar, a source-records sidebar,
   and a serif report body paginated to show how long the output is.
   =========================================================================== */
.wc-figure { width: 100%; margin: 1.8rem auto; --wc-accent: #3d4d6b; }
.wc-app { width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; border: 1px solid #c5c8ce; border-radius: 8px; overflow: hidden; background: #ffffff; box-shadow: 0 6px 22px rgba(30, 32, 40, 0.10); }
.wc-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 8px 13px; background: var(--wc-accent); color: #ffffff; }
.wc-titlebar__name { font-size: 12.5px; font-weight: 600; }
.wc-win { display: flex; gap: 15px; font-size: 11px; color: #c3cad8; }
.wc-menubar { display: flex; gap: 18px; padding: 6px 14px; background: #f1f2f4; border-bottom: 1px solid #e2e4e9; font-size: 12px; color: #4b5059; }
.wc-body { display: grid; grid-template-columns: 150px 1fr; }
.wc-side { background: #f6f7f9; border-right: 1px solid #e6e8ec; padding: 13px 13px 16px; }
.wc-side__head { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5059; margin-bottom: 10px; }
.wc-doc { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #3b3f46; padding: 4px 0; }
.wc-doc__ic { position: relative; width: 12px; height: 15px; flex: none; background: #ffffff; border: 1px solid #aeb4bd; border-radius: 1px; }
.wc-doc__ic::before { content: ""; position: absolute; top: -1px; right: -1px; width: 5px; height: 5px; background: #f6f7f9; border-left: 1px solid #aeb4bd; border-bottom: 1px solid #aeb4bd; }
.wc-doc__ic::after { content: ""; position: absolute; left: 2px; right: 2px; top: 7px; height: 1px; background: #c2c7ce; box-shadow: 0 3px 0 #c2c7ce; }
.wc-side__more { font-size: 11.5px; color: #8a909a; margin-top: 7px; }
.wc-main { display: flex; flex-direction: column; min-height: 0; }
.wc-case { display: flex; flex-wrap: wrap; gap: 3px 16px; padding: 11px 18px; border-bottom: 1px solid #eceef1; font-size: 11.5px; color: #6b7079; }
.wc-case b { color: #2a2d33; font-weight: 600; }
.wc-report { padding: 18px 22px; overflow-y: auto; max-height: 300px; }
.wc-report::-webkit-scrollbar { width: 10px; }
.wc-report::-webkit-scrollbar-track { background: #f1f2f4; border-radius: 6px; }
.wc-report::-webkit-scrollbar-thumb { background: #c4c9d0; border-radius: 6px; border: 2px solid #f1f2f4; }
.wc-report__title { font-family: Georgia, "Times New Roman", serif; font-size: 16px; font-weight: 700; color: #1f2329; text-align: center; }
.wc-report__sub { font-size: 10.5px; color: #8a909a; text-align: center; margin: 3px 0 18px; text-transform: uppercase; letter-spacing: 0.06em; }
.wc-sec { margin-bottom: 16px; }
.wc-sec__h { font-family: Georgia, "Times New Roman", serif; font-size: 13.5px; font-weight: 700; color: #1f2329; margin-bottom: 5px; }
.wc-sec p { font-family: Georgia, "Times New Roman", serif; font-size: 12.5px; line-height: 1.62; color: #33373d; margin: 0; }
.wc-foot { display: flex; align-items: center; justify-content: space-between; padding: 9px 18px; border-top: 1px solid #eceef1; background: #fafbfc; font-size: 11px; color: #8a909a; }
.wc-app__cap { text-align: center; font-size: 13px; color: #6b7079; margin-top: 11px; font-style: italic; }

/* "after": a review console. documents get a review status; the AI's findings are
   typed (contradiction, key fact, open question), each cited back to a page, with
   an action so the doctor resolves them; the report is gated on that review. */
.wc-doc__st { margin-left: auto; font-size: 11px; line-height: 1; }
.wc-doc__st--done { color: #2f7a52; }
.wc-doc__st--todo { color: #b9bec6; }
.wc-finds { padding: 14px 15px; overflow-y: auto; max-height: 380px; }
.wc-finds::-webkit-scrollbar { width: 10px; }
.wc-finds::-webkit-scrollbar-track { background: #f1f2f4; border-radius: 6px; }
.wc-finds::-webkit-scrollbar-thumb { background: #c4c9d0; border-radius: 6px; border: 2px solid #f1f2f4; }
.wc-find { border: 1px solid #e6e8ec; border-radius: 8px; padding: 11px 13px; }
.wc-find + .wc-find { margin-top: 10px; }
.wc-find__head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.wc-find__num { flex: none; width: 17px; height: 17px; border-radius: 50%; background: #eef0f3; color: #8a909a; font-size: 10.5px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; }
.wc-tag { display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px; border-radius: 999px; }
.wc-tag--warn { color: #8a5a00; background: #fbf0dc; }
.wc-tag--fact { color: #2f6a52; background: #e7f3ec; }
.wc-tag--q { color: #4b5563; background: #eceef1; }
.wc-find__body { font-family: Georgia, "Times New Roman", serif; font-size: 12.5px; line-height: 1.55; color: #33373d; }
.wc-cite { color: var(--wc-accent); text-decoration: none; font-weight: 600; font-size: 12px; border-bottom: 1px solid rgba(61, 77, 107, 0.4); }
.wc-find__foot { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.wc-abtn { font-size: 11.5px; font-weight: 600; padding: 4px 11px; border-radius: 6px; border: 1px solid #d7dbe1; background: #ffffff; color: #44484e; }
.wc-abtn--primary { color: #ffffff; background: var(--wc-accent); border-color: var(--wc-accent); }
.wc-foot__status { font-size: 11.5px; color: #3b3f46; }
.wc-foot__gen { font-size: 11.5px; font-weight: 600; padding: 6px 13px; border-radius: 6px; background: #e9eaed; color: #9aa0a8; border: 1px solid #dcdfe4; }
@media (max-width: 700px) {
  .wc-body { grid-template-columns: 1fr; }
  .wc-side { border-right: none; border-bottom: 1px solid #e6e8ec; }
}

@media (max-width: 700px) {
  .pe-app__split, .pe-split2 { grid-template-columns: 1fr; }
  .pe-inputs { border-right: none; border-bottom: 1px solid #e6e8ec; }
  .pe-pane--full { border-right: none; border-bottom: 1px solid #e6e8ec; }
}

/* data-agent chat card: stack the proof rows on phones so the right-hand
   check can't crush the label/value or overflow the card */
@media (max-width: 520px) {
  .da-step { grid-template-columns: 1fr; gap: 2px 0; }
  .da-step__check { text-align: left; }
  .da-prov { flex-wrap: wrap; gap: 6px 10px; }
  .da-prov__open { margin-left: 0; }
}

/* intro text above a figure, colored to match the figure captions */
.fig-intro, .fig-intro p { color: #6b7079; }

/* the tabset border is nearly invisible in dark mode; make it readable */
.quarto-dark .panel-tabset > .nav-tabs { border-bottom-color: #515861; }
.quarto-dark .panel-tabset > .tab-content { border-color: #515861; }
.quarto-dark .panel-tabset > .nav-tabs .nav-link.active {
  border-color: #515861 #515861 var(--bs-body-bg, #151515);
}
</style>
<!-- Shared "this is only a mockup" easter egg.  Any link or button in any
     figure on this page can opt in by adding the class `mockup-noop`; one
     page-level handler shows a subtle, dismissable note at the cursor. -->
<style>
.mockup-egg { position: fixed; z-index: 60; max-width: calc(100vw - 20px); background: rgba(38,40,46,0.93); color: #fff; font-size: 11.5px; font-weight: 500; line-height: 1.35; padding: 5px 10px; border-radius: 7px; box-shadow: 0 3px 12px rgba(0,0,0,0.22); white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .14s ease; }
.mockup-egg.is-on { opacity: 1; }
.mockup-noop { cursor: pointer; }
</style>
<script>
(function(){
  if (window.__mockupEgg) return;            // install once per page
  window.__mockupEgg = true;
  function install(){
    var egg = document.createElement("div");
    egg.className = "mockup-egg";
    egg.textContent = "Just a mockup, not a live app :)";
    document.body.appendChild(egg);
    var timer;
    function hide(){ egg.classList.remove("is-on"); }
    document.addEventListener("click", function(e){
      if(e.target.closest(".mockup-noop")){
        e.preventDefault();
        var M = 10;                              // viewport margin
        // place once to measure, then clamp inside the viewport (mobile-safe)
        egg.style.left = (e.clientX + 11) + "px";
        egg.style.top = (e.clientY + 12) + "px";
        egg.classList.add("is-on");
        var r = egg.getBoundingClientRect();
        var x = e.clientX + 11, y = e.clientY + 12;
        if(x + r.width > window.innerWidth - M) x = window.innerWidth - M - r.width;
        if(x < M) x = M;
        if(y + r.height > window.innerHeight - M) y = e.clientY - 12 - r.height;
        if(y < M) y = M;
        egg.style.left = x + "px";
        egg.style.top = y + "px";
        clearTimeout(timer);
        timer = setTimeout(hide, 2600);
      } else {
        hide();
      }
    });
  }
  if(document.body){ install(); }
  else { document.addEventListener("DOMContentLoaded", install); }
})();
</script>
<p>For the past 3 years, AI evals have been my professional focus.<sup>1</sup> The most common objection I hear to evals is “our product is hard to eval”.</p>
<p>This objection is a product smell. Artifacts that are hard for you to verify are often hard for users too. In the worst case, users have to redo the work from scratch to verify the output. More importantly, designing your product for ease of verification should come before building evals.</p>
<p>In this post, I’ll walk through three products I advised on that faced this issue. I’ll also show before and after sketches to demonstrate design principles. After these examples, I’ll discuss how to apply this general pattern to your product.</p>
<section id="example-1-the-ai-data-agent" class="level2">
<h2 class="anchored" data-anchor-id="example-1-the-ai-data-agent">Example 1: the AI data agent</h2>
<p>Almost every company I’ve worked with builds an internal AI data agent. You ask it a business question, like what was net revenue for Product A last quarter, and it finds relevant data sources, runs the queries, and provides an answer. The goal of this agent is to reduce dependency on data analysts.</p>
<p>A common mistake when building AI data agents is to make the answer the only output, as illustrated below.</p>
<!-- Figure: the data agent whose only output is the answer (the "before" design). -->
<div class="da-fig">
  <div class="da-fig__win">
    <div class="da-fig__bar"><span class="da-fig__lights"><span class="da-fig__light da-fig__light--r"></span><span class="da-fig__light da-fig__light--y"></span><span class="da-fig__light da-fig__light--g"></span></span><span class="da-fig__title">Data Agent</span></div>
    <div class="da-fig__body">
      <div class="da-fig__user"><span>What was net revenue for Product A last quarter?</span></div>
      <div class="da-fig__bot">Net revenue for Product A last quarter was <strong>$4.21M</strong>.</div>
      <div class="da-fig__input">Ask anything about your business…<span class="da-fig__send">➤</span></div>
    </div>
  </div>
  <div class="da-fig__cap">Since the only output is the answer, there is nothing here to check.</div>
</div>
<p>In the sketch above, the user has no way to verify the answer beyond redoing work.<sup>2</sup> A better design is to provide the user with checkable artifacts, informed by how a domain expert might validate the output. Here are techniques I use to validate metrics as a data scientist:</p>
<ol type="1">
<li>Compare the quantity and any intermediate calculations against a trusted source, like a vetted dashboard or report, or a similar analysis a colleague has already vetted.<sup>3</sup></li>
<li>Confirm the metric definition precisely. A number like net revenue can include or exclude things like returns and discounts.</li>
<li>Sanity-check a related quantity. If I can’t verify the number directly, I pull a related number that should move with it, like units sold or unique customers, and check if the combination is plausible.</li>
<li>Look at what is beneath the aggregate. A total can hide problems, so I break it down by dimensions like region or time period and sanity-check the distribution.</li>
<li>Read the query. For an important number I look at the SQL to confirm it does what I think, and I tweak it and rerun to test my assumptions.</li>
<li>Note anything I could not verify. If a step has no trusted reference to check it against, I flag it instead of presenting it as settled.</li>
</ol>
<p>Here’s what a better interface might look like. The two tabs below show the same answer at two levels of detail. The chat reply surfaces the details worth seeing up front, and the notebook holds the full analysis behind the answer. Use the tabs to switch between them.</p>
<div class="tabset-margin-container"></div><div class="panel-tabset">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-1-1-tab" data-bs-toggle="tab" data-bs-target="#tabset-1-1" aria-controls="tabset-1-1" aria-selected="true" href="">Chat</a></li><li class="nav-item"><a class="nav-link" id="tabset-1-2-tab" data-bs-toggle="tab" data-bs-target="#tabset-1-2" aria-controls="tabset-1-2" aria-selected="false" href="">Notebook</a></li></ul>
<div class="tab-content">
<div id="tabset-1-1" class="tab-pane active" aria-labelledby="tabset-1-1-tab">
<!-- Figure: the same answer with the proof laid out as a calm, scannable subsection (the "after" design). -->
<div class="da-fig">
  <div class="da-fig__win">
    <div class="da-fig__bar"><span class="da-fig__lights"><span class="da-fig__light da-fig__light--r"></span><span class="da-fig__light da-fig__light--y"></span><span class="da-fig__light da-fig__light--g"></span></span><span class="da-fig__title">Data Agent</span></div>
    <div class="da-fig__body">
      <div class="da-fig__user"><span>What was net revenue for Product A last quarter?</span></div>
      <div class="da-ans">Net revenue for Product A last quarter was <strong>$4.21M</strong>.<span class="da-ans__info" title="Some inputs could not be verified — see details">ⓘ</span></div>

      <div class="da-sub">
      
      <details class="da-verify" open="">
        <summary><span class="da-verify__label">Details</span></summary>
        <div class="da-verify__body">

          
            <span class="da-prov__ic"><svg viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="1.5" width="10" height="13" rx="1.5" stroke="currentColor" stroke-width="1.3"></rect><line x1="5.6" y1="1.5" x2="5.6" y2="14.5" stroke="currentColor" stroke-width="1.3"></line></svg></span>
            <span class="da-prov__main"><span class="da-prov__title">Adapted from “Quarterly revenue review”</span><span class="da-prov__by">authored by Priya Nair · Jun 28, 2026</span></span>
            <span class="da-prov__open">Open ↗</span>
          

          <div class="da-group">
            <div class="da-group__head">Assumptions</div>
            <div class="da-step">
              <span class="da-step__main"><span class="da-step__label">Metric definition</span><span class="da-step__val">gross − returns − discounts</span></span>
              <span class="da-step__check">matches governed definition</span>
            </div>
          </div>

          <div class="da-group">
            <div class="da-group__head">Intermediate calculations</div>
            <div class="da-step da-step--flag">
              <span class="da-step__main"><span class="da-step__label">Returns netted out</span><span class="da-step__val">−$0.7M</span></span>
              <span class="da-step__check"><span class="da-squig">no trusted source</span><span class="da-info">ⓘ</span></span>
            </div>
            <div class="da-step da-step--flag">
              <span class="da-step__main"><span class="da-step__label">Unique customers</span><span class="da-step__val">12,480</span></span>
              <span class="da-step__check"><span class="da-squig">183 unmatched</span><span class="da-info">ⓘ</span>, CRM ↔ Billing</span>
            </div>
          </div>

          <div class="da-actions">
            <span class="da-btn">Open as notebook</span>
          </div>
        </div>
      </details>
      </div>
    </div>
  </div>
  <div class="da-fig__cap">The agent surfaces the important details behind the answer.  Select the Notebook tab above to see the full analysis generated by the agent.</div>
</div>
<script>
(function(){
  // In the slide deck this advances to the notebook slide. In the blog post it
  // keeps the original behavior of selecting the Notebook tab.
  var btns = document.querySelectorAll(".da-fig .da-btn");
  btns.forEach(function(b){
    if(b.textContent.toLowerCase().indexOf("notebook") === -1) return;
    b.style.cursor = "pointer";
    b.addEventListener("click", function(event){
      event.preventDefault();
      if(window.Reveal && b.closest(".reveal")){
        window.Reveal.next();
        return;
      }
      var tabs = document.querySelectorAll('[role="tab"]');
      for(var i = 0; i < tabs.length; i++){
        if(tabs[i].textContent.trim() === "Notebook"){ tabs[i].click(); break; }
      }
    });
  });
})();
</script>
</div>
<div id="tabset-1-2" class="tab-pane" aria-labelledby="tabset-1-2-tab">
<div class="fig-intro">
<p>This is the notebook the agent worked in while producing its answer. Scroll within the figure to see all the cells. The sidebar has a Contents tab for jumping between sections and an Assistant tab for asking follow-ups.</p>
</div>
<!-- Figure: what "Open as notebook" opens. The agent's answer, rendered as a readable analysis notebook: markdown prose interleaved with code/SQL cells, a TOC to navigate, and an explicit section for what could not be verified. Self-contained styles. -->
<style>
.nb {
  --nb-accent: #5b8def;
  --nb-ai: #8a5cf5;
  --nb-ink: #1f2329;
  --nb-mut: #6b7079;
  --nb-line: #e7e9ee;
  --nb-code-bg: #fbfbfd;
  width: 100%; margin: 1.8rem auto;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--nb-ink);
}
.nb__win { border: 1px solid #d9dce1; border-radius: 12px; overflow: hidden; background: #fff; box-shadow: 0 6px 24px rgba(30,32,40,0.10); }

/* top chrome: traffic lights, sidebar toggle, file name, assistant toggle, run-all */
.nb__bar { display: flex; align-items: center; gap: 9px; padding: 9px 13px; background: #f5f6f8; border-bottom: 1px solid #e6e8ec; }
.nb__lights { display: flex; gap: 6px; }
.nb__light { width: 11px; height: 11px; border-radius: 50%; }
.nb__light--r { background: #ff5f57; } .nb__light--y { background: #febc2e; } .nb__light--g { background: #28c840; }
.nb__file { font-size: 13px; font-weight: 600; color: #3b3f46; }
.nb__file .nb__ext { color: #9aa0a8; font-weight: 500; }
.nb__spacer { margin-left: auto; }
.nb__runall { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: #3b3f46; background: #fff; border: 1px solid #d3d7dd; border-radius: 6px; padding: 4px 11px; cursor: pointer; }
.nb__runall:hover { background: #fbfbfc; }
.nb__sidebtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 26px; border-radius: 6px; color: #6b7079; background: #fff; border: 1px solid #d3d7dd; cursor: pointer; }
.nb__sidebtn:hover { background: #fbfbfc; }
.nb__sidebtn svg { width: 14px; height: 14px; }
.nb.is-side-open .nb__sidebtn { background: #eef3fd; color: var(--nb-accent); border-color: #cfe0fb; }
.nb__publish { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: #fff; background: var(--nb-accent); border: 1px solid var(--nb-accent); border-radius: 6px; padding: 4px 11px; cursor: pointer; }
.nb__publish:hover { filter: brightness(0.96); }

/* provenance strip: shows this notebook was built from a vetted one (echoes the
   "adapted from a vetted lesson plan" idea in the PE example). Warm tint, openable. */
.nb__prov { display: flex; align-items: baseline; gap: 7px; padding: 7px 14px; background: #faf6ee; border-bottom: 1px solid #efe7d6; font-size: 11.5px; line-height: 1.45; color: #6b5f48; }
.nb__prov__ic { color: #b48a3c; font-size: 13px; flex: none; position: relative; top: 1px; }
.nb__prov__txt { flex: 1 1 auto; min-width: 0; }
/* the source name is itself the "open" affordance: a small diagonal arrow rides
   with it, and the pair stays on one line so the arrow never detaches */
.nb__prov a { color: #8a6d2f; text-decoration: none; border-bottom: 1px solid rgba(138,109,47,0.45); white-space: nowrap; }
.nb__prov__arr { border-bottom: 0; font-size: 0.92em; }
.nb__prov b { color: #4b4334; font-weight: 600; }

/* AI assistant: a tab inside the right sidebar */
.nb__ai { min-height: 0; }
.nb__ai-head { display: flex; align-items: center; gap: 8px; padding: 9px 13px; border-bottom: 1px solid #eef0f3; flex: none; }
.nb__ai-spark { color: var(--nb-ai); font-size: 13px; }
.nb__ai-new { font-size: 11px; color: #9aa0a8; }
.nb__ai-x { margin-left: auto; color: #c2c6cc; font-size: 13px; cursor: pointer; }
.nb__ai-body { flex: 1; overflow-y: auto; padding: 13px; display: flex; flex-direction: column; gap: 12px; }
.nb__ai-user { align-self: flex-end; max-width: 92%; background: #eef1f6; color: #1f2329; font-size: 12px; line-height: 1.45; padding: 8px 11px; border-radius: 12px 12px 4px 12px; }
.nb__ai-msg { display: flex; gap: 8px; }
.nb__ai-ava { flex: none; color: var(--nb-ai); font-size: 12px; line-height: 1.6; }
.nb__ai-msg p { margin: 0; font-size: 12px; line-height: 1.5; color: #33373d; }
.nb__ai-done { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #9aa0a8; padding-left: 1px; }
.nb__ai-done b { color: #2f7a52; font-weight: 700; }
.nb__ai-input { border-top: 1px solid #eef0f3; padding: 10px 12px 12px; }
.nb__ai-field { border: 1px solid #d9dce1; border-radius: 10px; background: #fff; padding: 9px 11px; box-shadow: 0 1px 2px rgba(40,40,50,0.04); }
.nb__ai-ph { color: #9aa0a8; font-size: 12.5px; }
.nb__ai-controls { display: flex; align-items: center; gap: 7px; margin-top: 9px; }
.nb__ai-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: #5b6068; background: #f1f2f5; border: 1px solid #e6e8ec; border-radius: 6px; padding: 3px 8px; }
.nb__ai-model { margin-left: auto; font-size: 11px; color: #6b7079; }
.nb__ai-send { width: 24px; height: 24px; border-radius: 6px; background: var(--nb-ai); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; }

/* layout: cells on the left, one collapsible sidebar on the right holding both the
   contents navigator and the AI assistant (as tabs) so the figure stays text-width */
.nb__main { display: grid; grid-template-columns: 1fr 238px; }
.nb:not(.is-side-open) .nb__main { grid-template-columns: 1fr; }
.nb:not(.is-side-open) .nb__side { display: none; }
.nb__side { display: flex; flex-direction: column; min-height: 0; background: #fcfcfd; border-left: 1px solid #eceef1; }
/* collapsed-sidebar rail: only used on phones (see the mobile media query) */
.nb__rail { display: none; }
.nb__tabs { display: flex; border-bottom: 1px solid #eef0f3; flex: none; }
.nb__tab { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 6px; font-size: 11.5px; font-weight: 600; color: #9aa0a8; cursor: pointer; border-bottom: 2px solid transparent; }
.nb__tab:hover { color: #5b6068; }
.nb__tab.is-active { color: var(--nb-ink); border-bottom-color: var(--nb-accent); }
.nb__tab-flag { width: 7px; height: 7px; border-radius: 50%; background: #e0795f; }
.nb__panel { display: none; flex: 1; min-height: 0; overflow-y: auto; }
.nb__panel.is-active { display: flex; flex-direction: column; }
.nb__toc { padding: 12px 11px; }
.nb__toc-h { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: #9aa0a8; font-weight: 700; padding: 0 8px 9px; }
.nb__toc a { display: flex; align-items: baseline; gap: 7px; text-decoration: none; font-size: 12.5px; color: #5b6068; padding: 6px 8px; border-radius: 6px; line-height: 1.3; }
.nb__toc a:hover { background: #f0f1f4; }
.nb__toc a.is-active { background: #eef3fd; color: var(--nb-accent); font-weight: 600; }
.nb__toc .nb__toc-n { font-size: 10.5px; color: #b4b9c0; font-variant-numeric: tabular-nums; }
.nb__toc a.is-active .nb__toc-n { color: var(--nb-accent); }
.nb__toc-flag { position: relative; margin-left: auto; align-self: center; width: 14px; height: 14px; border-radius: 50%; background: #e0795f; color: #fff; font-size: 9px; font-weight: 700; line-height: 1; display: inline-flex; align-items: center; justify-content: center; cursor: help; }
/* tooltip shows only when the icon itself is hovered, not the whole row */
.nb__toc-flag::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 8px); left: -4px; background: #2a2d33; color: #fff; font-weight: 400; font-size: 11px; line-height: 1.3; white-space: nowrap; padding: 5px 9px; border-radius: 6px; box-shadow: 0 3px 12px rgba(0,0,0,0.22); opacity: 0; pointer-events: none; transition: opacity .12s; z-index: 6; }
.nb__toc-flag::before { content: ""; position: absolute; bottom: calc(100% + 3px); left: 4px; border: 5px solid transparent; border-top-color: #2a2d33; opacity: 0; transition: opacity .12s; }
.nb__toc-flag:hover::after, .nb__toc-flag:hover::before { opacity: 1; }

.nb__scroll { max-height: 720px; overflow-y: auto; padding: 8px 0 14px; }
.nb__scroll::-webkit-scrollbar { width: 11px; }
.nb__scroll::-webkit-scrollbar-track { background: #f6f7f9; }
.nb__scroll::-webkit-scrollbar-thumb { background: #cfd3da; border-radius: 6px; border: 3px solid #f6f7f9; }

/* a cell: gutter prompt on the left, body on the right; toolbar reveals the cell type */
.nb-cell { display: grid; grid-template-columns: 56px 1fr; padding: 4px 18px 4px 0; }
.nb-cell__gutter { padding-top: 9px; text-align: right; padding-right: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; font-weight: 600; color: var(--nb-accent); user-select: none; }
.nb-cell__body { min-width: 0; border-left: 2px solid transparent; padding-left: 14px; }
.nb-cell--md .nb-cell__body:hover { border-left-color: #e7e9ee; }
/* every code cell gets a quiet grey rail; only the ACTIVE cell gets the blue accent */
.nb-cell--code .nb-cell__body { border-left-color: #e2e4e9; }
.nb-cell__body--sel { border-left: 3px solid var(--nb-accent) !important; padding-left: 13px; background: linear-gradient(90deg, rgba(91,141,239,0.06), rgba(91,141,239,0) 40%); border-radius: 0 6px 6px 0; }

/* the little per-cell toolbar that signals each cell is typed and editable */
.nb-cell__tools { display: flex; align-items: center; gap: 6px; opacity: 0.62; transition: opacity .12s; margin-bottom: 6px; }
.nb-cell__body:hover .nb-cell__tools { opacity: 1; }
.nb-chip { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 7px; border-radius: 5px; }
.nb-chip--md { color: #6b7079; background: #eef0f3; }
.nb-chip--py { color: #3f6fd6; background: #eaf0fc; }
.nb-chip--sql { color: #1f7a6b; background: #e3f3ef; }
.nb-tool { min-width: 22px; height: 18px; padding: 0 4px; border-radius: 4px; border: 1px solid #e2e4e9; background: #fff; color: #9aa0a8; font-size: 11px; display: inline-flex; align-items: center; justify-content: center; }
.nb-tool--b { font-weight: 800; }
.nb-tool--i { font-style: italic; font-family: Georgia, serif; }

/* code cell input is collapsible, but only the chevron toggles it */
.nb-codecell { border: 0; }
.nb-codecell__bar { list-style: none; cursor: default; display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.nb-codecell__bar::-webkit-details-marker { display: none; }
.nb-codecell__bar .nb-tool { cursor: pointer; }
.nb-codecell__chev { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; margin-left: 1px; border-radius: 5px; color: #aab0b8; cursor: pointer; transition: background .12s, transform .15s; }
.nb-codecell__chev:hover { background: #eceef1; color: #6b7079; }
.nb-codecell__chev svg { width: 11px; height: 11px; display: block; }
.nb-codecell:not([open]) .nb-codecell__chev { transform: rotate(-90deg); }
.nb-codecell__hint { font-size: 11px; color: #aeb4bd; }
.nb-codecell[open] .nb-codecell__hint { display: none; }

/* per-cell hint that AI can be invoked on this specific cell; revealed on hover
   so it doesn't clutter every cell at rest */
.nb-aibtn { margin-left: auto; flex: none; display: none; align-items: center; gap: 5px; font-size: 11px; font-weight: 500; line-height: 1.25; white-space: nowrap; color: var(--nb-ai); background: #f5f1fe; border: 1px solid #e7dcfb; border-radius: 6px; padding: 2px 8px; }
.nb-cell--code.is-cell-active .nb-aibtn { display: inline-flex; }
.nb-aibtn .nb-aikbd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9.5px; color: #a99ad6; }

/* markdown cell rendering */
.nb-md { font-size: 14px; line-height: 1.62; color: #2a2d33; }
.nb-md h1 { font-size: 21px; font-weight: 700; margin: 6px 0 6px; letter-spacing: -0.01em; }
.nb-md h2 { font-size: 16px; font-weight: 700; margin: 8px 0 6px; }
.nb-md p { margin: 7px 0; }
.nb-md a { color: var(--nb-accent); text-decoration: none; border-bottom: 1px solid rgba(91,141,239,0.35); }
.nb-md code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: #f1f2f5; padding: 1px 5px; border-radius: 4px; color: #3b3f46; }
.nb-md .nb-q { color: var(--nb-mut); font-style: italic; }
.nb-md strong { font-weight: 700; }

/* code + sql cell */
.nb-code { background: var(--nb-code-bg); border: 1px solid var(--nb-line); border-radius: 8px; padding: 11px 13px; overflow-x: auto; }
.nb-code pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.62; color: #2a2d33; white-space: pre; }
.tk-kw { color: #8a5cf5; } .tk-bn { color: #1f9e8f; } .tk-fn { color: #3f6fd6; }
.tk-str { color: #4f9a52; } .tk-num { color: #b06400; } .tk-com { color: #a6abb3; font-style: italic; } .tk-pn { color: #8a909a; }

/* output area */
.nb-out { margin-top: 7px; padding-left: 2px; }
.nb-out__lbl { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #d0664f; padding-top: 2px; }
.nb-result { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: #2a2d33; margin-top: 3px; }
.nb-check { color: #2f7a52; font-weight: 600; }
/* the variable each cell outputs, shown as a small chip the way Hex labels results */
.nb-outvar { display: inline-flex; align-items: center; gap: 7px; margin-top: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.nb-outvar__a { color: #b4b9c0; font-size: 12px; }
.nb-outvar__n { font-size: 11px; color: #2f7a52; background: #e7f3ec; border: 1px solid #d3e9da; border-radius: 5px; padding: 2px 7px; }
.nb-outvar__n--chart { color: #6b5bd1; background: #efebfb; border-color: #ddd3f6; }
.nb-warnline { color: #b05c4f; }

/* a dataframe-style table output */
.nb-df { border-collapse: collapse; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; margin-top: 5px; }
.nb-df th, .nb-df td { padding: 4px 14px 4px 0; text-align: right; }
.nb-df th { color: #8a909a; font-weight: 600; border-bottom: 1px solid var(--nb-line); }
.nb-df td:first-child, .nb-df th:first-child { text-align: left; color: #5b6068; }
.nb-df td { color: #2a2d33; border-bottom: 1px solid #f3f4f6; }

/* a chart output, drawn to look like a generated matplotlib figure:
   title, horizontal bars, value labels, gridlines, an x-axis with ticks and a label */
.nb-fig { border: 1px solid var(--nb-line); border-radius: 8px; background: #fff; padding: 13px 16px 9px; margin-top: 8px; }
.nb-fig__title { font-size: 12.5px; font-weight: 600; color: #2a2d33; text-align: center; margin-bottom: 13px; }
.nb-fig__plot { position: relative; }
.nb-fig__grid { position: absolute; top: 0; bottom: 0; left: 62px; right: 40px; pointer-events: none; }
.nb-fig__grid span { position: absolute; top: 0; bottom: 0; width: 1px; background: #eef0f3; }
.nb-fig__grid span:first-child { background: #d4d8de; }
.nb-fig__row { display: grid; grid-template-columns: 62px 1fr 40px; align-items: center; height: 27px; }
.nb-fig__yl { font-size: 11.5px; color: #5b6068; text-align: right; padding-right: 11px; }
.nb-fig__track { position: relative; height: 15px; }
.nb-fig__bar { position: absolute; left: 0; top: 0; height: 100%; background: var(--nb-accent); border-radius: 0 2px 2px 0; }
.nb-fig__vl { font-size: 11.5px; color: #3b3f46; font-weight: 600; padding-left: 9px; font-variant-numeric: tabular-nums; }
.nb-fig__baseline { position: absolute; left: 62px; right: 40px; bottom: 0; height: 1px; background: #c4c9d0; }
.nb-fig__axis { display: grid; grid-template-columns: 62px 1fr 40px; margin-top: 4px; }
.nb-fig__ticks { grid-column: 2; position: relative; height: 13px; }
.nb-fig__ticks span { position: absolute; top: 0; transform: translateX(-50%); font-size: 10px; color: #9aa0a8; font-variant-numeric: tabular-nums; }
.nb-fig__xlabel { text-align: center; font-size: 10.5px; color: #8a909a; margin-top: 1px; padding-left: 22px; }

.nb__cap { text-align: center; font-size: 13px; color: #6b7079; margin-top: 11px; font-style: italic; }

@media (max-width: 640px) {
  .nb-cell { grid-template-columns: 40px 1fr; padding-right: 12px; }
  .nb-cell__gutter { padding-right: 8px; }
  /* keep the top chrome on one row: traffic lights on the left, actions pushed
     to the right by the spacer. the filename is dropped on phones (a 2-char
     truncated stub looks broken), and the sidebar toggle lives on the rail */
  .nb__bar { flex-wrap: nowrap; gap: 7px; }
  .nb__sidebtn, .nb__file { display: none; }
  .nb__runall, .nb__publish { flex: none; padding: 4px 9px; }
  .nb-codecell__bar { gap: 6px; min-width: 0; }
  .nb-codecell__hint { display: none; }
  .nb-aibtn { padding: 3px 7px; }
  .nb-aibtn .nb-aikbd { display: none; }
  .nb__scroll { max-height: min(720px, calc(100vh - 180px)); padding: 48px 0 14px; }

  /* the assistant opens from a floating pill instead of a permanent rail, so the
     cells keep the full notebook width on phones */
  .nb__main,
  .nb:not(.is-side-open) .nb__main,
  .nb.is-side-open .nb__main { grid-template-columns: 1fr; position: relative; }
  .nb__rail {
    position: absolute; right: 10px; top: 10px; z-index: 6;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    min-height: 32px; padding: 0 11px;
    background: #fff; border: 1px solid #ded6fb; border-radius: 999px;
    box-shadow: 0 4px 14px rgba(40, 32, 80, 0.16);
    color: var(--nb-ai); cursor: pointer; -webkit-appearance: none; appearance: none;
  }
  .nb__rail::after { content: "Ask AI"; font-size: 11.5px; font-weight: 600; }
  .nb__rail:hover { background: #fbf9ff; }
  .nb__rail__ic svg { display: none; }
  .nb__rail__ic::before { content: "\2726"; font-size: 12px; line-height: 1; }
  .nb.is-side-open .nb__rail { opacity: 0; pointer-events: none; }

  .nb__side {
    position: absolute; top: 0; right: 0; bottom: 0;
    width: 100%; z-index: 8;
    border-left: 0;
    box-shadow: -12px 0 30px rgba(20, 22, 28, 0.20);
  }
  /* keep it mounted in both states (so it can slide), driven by two mutually
     exclusive rules of equal weight: off-screen when closed, in when open */
  .nb:not(.is-side-open) .nb__side { display: flex; transform: translateX(101%); }
  .nb.is-side-open .nb__side { display: flex; transform: translateX(0); }
  /* slide-in on open. a CSS transition between these two conditional rules gets
     stuck at the closed value in Chrome, so animate with a keyframe instead;
     gated by .is-anim (added on first user toggle) so it never fires on load */
  .nb.is-anim.is-side-open .nb__side { animation: nbDrawerIn .24s ease; }
  @keyframes nbDrawerIn { from { transform: translateX(101%); } to { transform: translateX(0); } }
  /* dim the cells behind an open drawer; tapping the dim area closes it (JS) */
  .nb.is-side-open .nb__main::before {
    content: ""; position: absolute; inset: 0; z-index: 7;
    background: rgba(18, 20, 26, 0.34);
  }
}

@media (max-width: 360px) {
  .nb-cell__tools .nb-tool:nth-last-child(-n+2) { display: none; }
  .nb-chip--py { font-size: 0; }
  .nb-chip--py::before { content: "PY"; font-size: 10px; }
  .nb-aibtn { width: 24px; height: 22px; padding: 0; gap: 0; justify-content: center; font-size: 0; }
  .nb-aibtn::before { content: "\2726"; font-size: 11px; line-height: 1; }
}

/* the per-cell AI action appears only after a code cell is selected */
</style>

<div class="nb is-side-open">
  <div class="nb__win">

    <div class="nb__bar">
      <div class="nb__lights"><span class="nb__light nb__light--r"></span><span class="nb__light nb__light--y"></span><span class="nb__light nb__light--g"></span></div>
      <span class="nb__file">net_revenue_product_a<span class="nb__ext">.ipynb</span></span>
      <span class="nb__spacer"></span>
      <span class="nb__sidebtn" aria-label="Toggle sidebar"><svg viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1.6" y="2.6" width="12.8" height="10.8" rx="2" stroke="currentColor" stroke-width="1.4"></rect><line x1="10.3" y1="2.6" x2="10.3" y2="13.4" stroke="currentColor" stroke-width="1.4"></line></svg></span>
      <span class="nb__runall">▶ Run all</span>
      <span class="nb__publish">↑ Publish</span>
    </div>

    <div class="nb__prov">
      <span class="nb__prov__ic">◫</span>
      <span class="nb__prov__txt">Adapted from Quarterly revenue review <span class="nb__prov__arr">↗</span>, authored by <b>Priya Nair</b> · Jun 28, 2026</span>
    </div>

    <div class="nb__main">

      <div class="nb__scroll">

        <!-- title -->
        <div class="nb-cell nb-cell--md">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body nb-cell__body--sel">
            <div class="nb-cell__tools" style="opacity:1"><span class="nb-chip nb-chip--md">Markdown</span><span class="nb-tool nb-tool--b">B</span><span class="nb-tool nb-tool--i">i</span><span class="nb-tool">H</span><span class="nb-tool">🔗</span><span class="nb-tool">☰</span></div>
            <div class="nb-md">
              <h1>Net revenue — Product A, Q4 FY25</h1>
              <p class="nb-q">“What was net revenue for Product A last quarter?”</p>
              <p>Short answer: <strong>$4.21M</strong>. This notebook shows how that number was built and what was checked against a trusted source.</p>
            </div>
          </div>
        </div>

        <!-- 1. metric definition -->
        <div class="nb-cell nb-cell--md" id="nbsec-1">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md">
              <h2 class="anchored">1. Metric definition</h2>
              <p>Net revenue is <code>gross − returns − discounts</code>. Read the definition from the governed metrics layer so this matches what finance reports.</p>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[1]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell"><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--py">Python</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre><span class="tk-kw">import</span> yaml

defn <span class="tk-pn">=</span> yaml<span class="tk-pn">.</span><span class="tk-fn">safe_load</span>(<span class="tk-fn">open</span>(<span class="tk-str">"metrics/net_revenue.yml"</span>))
defn<span class="tk-pn">[</span><span class="tk-str">"expr"</span><span class="tk-pn">]</span>, defn<span class="tk-pn">[</span><span class="tk-str">"source"</span><span class="tk-pn">]</span></pre></div></details>
            <div class="nb-out">
              <div class="nb-result"><span class="tk-pn">(</span><span class="tk-str">'gross - returns - discounts'</span><span class="tk-pn">,</span> <span class="tk-str">'finance.order_lines'</span><span class="tk-pn">)</span></div>
            </div>
          </div>
        </div>

        <!-- 2. net revenue for Product A -->
        <div class="nb-cell nb-cell--md" id="nbsec-2">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md">
              <h2 class="anchored">2. Net revenue for Product A</h2>
              <p>Pull net revenue for Product A for the quarter, straight from the order lines.</p>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[2]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell" open=""><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--sql">SQL</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre><span class="tk-kw">SELECT</span> <span class="tk-fn">SUM</span>(gross <span class="tk-pn">-</span> returns <span class="tk-pn">-</span> discounts) <span class="tk-kw">AS</span> net_revenue
<span class="tk-kw">FROM</span>   finance<span class="tk-pn">.</span>order_lines
<span class="tk-kw">WHERE</span>  product <span class="tk-pn">=</span> <span class="tk-str">'Product A'</span>
  <span class="tk-kw">AND</span>  fiscal_quarter <span class="tk-pn">=</span> <span class="tk-str">'Q4-FY25'</span>;</pre></div></details>
            <div class="nb-out">
              
<table class="nb-df caption-top table">
<tbody>
<tr class="odd">
<th data-quarto-table-cell-role="th">net_revenue</th>
</tr>
<tr class="even">
<td>4,210,442</td>
</tr>
</tbody>
</table>

            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--md">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md"><p>That's the <strong>$4.21M</strong> the chat answer reported.</p></div>
          </div>
        </div>

        <!-- 4. by region -->
        <div class="nb-cell nb-cell--md" id="nbsec-4">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md">
              <h2 class="anchored">3. Sanity-check the distribution</h2>
              <p>Break Product A down by region, then plot it.  Nothing should look out of place against last quarter's mix.</p>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[3]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell" open=""><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--sql">SQL</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre><span class="tk-kw">SELECT</span> region,
       <span class="tk-fn">SUM</span>(gross <span class="tk-pn">-</span> returns <span class="tk-pn">-</span> discounts) <span class="tk-kw">AS</span> net_revenue
<span class="tk-kw">FROM</span>   finance<span class="tk-pn">.</span>order_lines
<span class="tk-kw">WHERE</span>  product <span class="tk-pn">=</span> <span class="tk-str">'Product A'</span>
  <span class="tk-kw">AND</span>  fiscal_quarter <span class="tk-pn">=</span> <span class="tk-str">'Q4-FY25'</span>
<span class="tk-kw">GROUP BY</span> region;</pre></div></details>
            <div class="nb-out">
              
<table class="nb-df caption-top table">
<tbody>
<tr class="odd">
<th data-quarto-table-cell-role="th">region</th>
<th data-quarto-table-cell-role="th">net_revenue</th>
</tr>
<tr class="even">
<td>West</td>
<td>1,740,000</td>
</tr>
<tr class="odd">
<td>Central</td>
<td>1,160,000</td>
</tr>
<tr class="even">
<td>East</td>
<td>890,000</td>
</tr>
<tr class="odd">
<td>Intl</td>
<td>420,000</td>
</tr>
</tbody>
</table>

            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[4]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell"><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--py">Python</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre>m <span class="tk-pn">=</span> by_region<span class="tk-pn">.</span><span class="tk-fn">set_index</span>(<span class="tk-str">"region"</span>)<span class="tk-pn">[</span><span class="tk-str">"net_revenue"</span><span class="tk-pn">]</span> <span class="tk-pn">/</span> <span class="tk-num">1e6</span>
m<span class="tk-pn">.</span>plot<span class="tk-pn">.</span><span class="tk-fn">barh</span>(title<span class="tk-pn">=</span><span class="tk-str">"Net revenue by region · Q4 FY25"</span>)</pre></div></details>
            <div class="nb-out">
              <div class="nb-fig">
                <div class="nb-fig__title">Net revenue by region · Q4 FY25</div>
                <div class="nb-fig__plot">
                  <div class="nb-fig__grid"><span style="left:0%"></span><span style="left:25%"></span><span style="left:50%"></span><span style="left:75%"></span><span style="left:100%"></span></div>
                  <div class="nb-fig__row"><span class="nb-fig__yl">West</span><span class="nb-fig__track"><span class="nb-fig__bar" style="width:87%"></span></span><span class="nb-fig__vl">1.74</span></div>
                  <div class="nb-fig__row"><span class="nb-fig__yl">Central</span><span class="nb-fig__track"><span class="nb-fig__bar" style="width:58%"></span></span><span class="nb-fig__vl">1.16</span></div>
                  <div class="nb-fig__row"><span class="nb-fig__yl">East</span><span class="nb-fig__track"><span class="nb-fig__bar" style="width:44.5%"></span></span><span class="nb-fig__vl">0.89</span></div>
                  <div class="nb-fig__row"><span class="nb-fig__yl">Intl</span><span class="nb-fig__track"><span class="nb-fig__bar" style="width:21%"></span></span><span class="nb-fig__vl">0.42</span></div>
                  <div class="nb-fig__baseline"></div>
                </div>
                <div class="nb-fig__axis"><span class="nb-fig__ticks"><span style="left:0%">0</span><span style="left:25%">0.5</span><span style="left:50%">1.0</span><span style="left:75%">1.5</span><span style="left:100%">2.0</span></span></div>
                <div class="nb-fig__xlabel">net revenue ($M)</div>
              </div>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--md">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md"><p>West and Central drive most of the revenue, with International a small tail.</p></div>
          </div>
        </div>

        <!-- 5. open items: left as runnable cells, not a styled card -->
        <div class="nb-cell nb-cell--md" id="nbsec-5">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md">
              <h2 class="anchored">4. What I could not verify</h2>
              <p>Two inputs have no trusted source to check against. Instead of treating them as settled, each is left below as a cell you can run and edit to dig in.</p>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[5]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell" open=""><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--sql">SQL</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre><span class="tk-com">-- the agent's -$0.7M returns figure is an estimate;</span>
<span class="tk-com">-- check the returns table for Q4 rows to back it</span>
<span class="tk-kw">SELECT</span> <span class="tk-fn">COUNT</span>(<span class="tk-pn">*</span>) <span class="tk-kw">AS</span> n_rows, <span class="tk-fn">SUM</span>(amount) <span class="tk-kw">AS</span> returns
<span class="tk-kw">FROM</span>   finance<span class="tk-pn">.</span>returns
<span class="tk-kw">WHERE</span>  product <span class="tk-pn">=</span> <span class="tk-str">'Product A'</span>
  <span class="tk-kw">AND</span>  fiscal_quarter <span class="tk-pn">=</span> <span class="tk-str">'Q4-FY25'</span>;</pre></div></details>
            <div class="nb-out">
              
<table class="nb-df caption-top table">
<tbody>
<tr class="odd">
<th data-quarto-table-cell-role="th">n_rows</th>
<th data-quarto-table-cell-role="th">returns</th>
</tr>
<tr class="even">
<td>0</td>
<td>NULL</td>
</tr>
</tbody>
</table>

              <div class="nb-result" style="margin-top:7px"><span class="nb-warnline"># Q4 returns haven't loaded yet, so the −$0.7M is an estimate</span></div>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--md">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md"><p>So that <strong>−$0.7M</strong> has no source to check against yet.  The other open item is the customer join:</p></div>
          </div>
        </div>

        <div class="nb-cell nb-cell--code">
          <div class="nb-cell__gutter">[6]</div>
          <div class="nb-cell__body">
            <details class="nb-codecell" open=""><summary class="nb-codecell__bar"><span class="nb-chip nb-chip--sql">SQL</span><span class="nb-tool">▶</span><span class="nb-codecell__chev" aria-label="Toggle code"><svg viewbox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.2 4.6 L6 7.4 L8.8 4.6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="nb-codecell__hint">Code collapsed</span><span class="nb-aibtn">✦ Ask AI <span class="nb-aikbd">⌘K</span></span></summary>
            <div class="nb-code"><pre><span class="tk-com">-- 183 of 12,480 unique customers are in Billing, not CRM,</span>
<span class="tk-com">-- so some revenue can't be attributed. Pull them:</span>
<span class="tk-kw">SELECT</span> b<span class="tk-pn">.</span>customer_id, b<span class="tk-pn">.</span>amount
<span class="tk-kw">FROM</span>   billing<span class="tk-pn">.</span>invoices b
<span class="tk-kw">LEFT JOIN</span> crm<span class="tk-pn">.</span>customers c <span class="tk-kw">USING</span> (customer_id)
<span class="tk-kw">WHERE</span>  c<span class="tk-pn">.</span>customer_id <span class="tk-kw">IS NULL</span>
<span class="tk-kw">ORDER BY</span> b<span class="tk-pn">.</span>amount <span class="tk-kw">DESC</span>
<span class="tk-kw">LIMIT</span> <span class="tk-num">5</span>;</pre></div></details>
            <div class="nb-out">
              
<table class="nb-df caption-top table">
<tbody>
<tr class="odd">
<th data-quarto-table-cell-role="th">customer_id</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
<tr class="even">
<td>BIL-44821</td>
<td>18,400</td>
</tr>
<tr class="odd">
<td>BIL-39105</td>
<td>12,950</td>
</tr>
<tr class="even">
<td>BIL-50277</td>
<td>9,310</td>
</tr>
<tr class="odd">
<td>…</td>
<td>…</td>
</tr>
</tbody>
</table>

              <div class="nb-result" style="margin-top:7px"><span class="nb-warnline"># 183 rows · $61,540 unattributed</span></div>
            </div>
          </div>
        </div>

        <div class="nb-cell nb-cell--md">
          <div class="nb-cell__gutter"></div>
          <div class="nb-cell__body">
            <div class="nb-md"><p>That's $61,540 of revenue whose customer attribution is uncertain, surfaced so a person can resolve it before the total is trusted.</p></div>
          </div>
        </div>

      </div>

      <div class="nb__side">
        <div class="nb__tabs">
          <span class="nb__tab" data-tab="toc">Contents</span>
          <span class="nb__tab is-active" data-tab="ai"><span class="nb__ai-spark">✦</span>Assistant</span>
        </div>

        

        <div class="nb__ai nb__panel is-active" data-panel="ai">
          <div class="nb__ai-head"><span class="nb__ai-new">New thread</span><span class="nb__ai-x">✕</span></div>
          <div class="nb__ai-body">
            <div class="nb__ai-user">What was net revenue for Product A last quarter?</div>
            <div class="nb__ai-msg">
              <span class="nb__ai-ava">✦</span>
              <p>I pulled Product A's net revenue from the order lines using finance's governed definition, broke it out by region, and flagged what I couldn't verify. The cells are on the left.</p>
            </div>
            <div class="nb__ai-done"><b>✓</b> Generated the cells in this notebook</div>
          </div>
          <div class="nb__ai-input">
            <div class="nb__ai-field">
              <div class="nb__ai-ph">Ask a follow-up…</div>
              <div class="nb__ai-controls"><span class="nb__ai-chip">SQL [2]</span><span class="nb__ai-model">Auto ▾</span><span class="nb__ai-send">↑</span></div>
            </div>
          </div>
        </div>
      </div>

      <button class="nb__rail" aria-label="Open assistant">
        <span class="nb__rail__ic"><svg viewbox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1.6" y="2.6" width="12.8" height="10.8" rx="2" stroke="currentColor" stroke-width="1.4"></rect><line x1="10.3" y1="2.6" x2="10.3" y2="13.4" stroke="currentColor" stroke-width="1.4"></line></svg></span>
      </button>
    </div>
  </div>
  <div class="nb__cap">The notebook reads top to bottom: the assumptions the agent made, the queries it ran, and an explicit list of what it could not verify.</div>
<script>
(function(){
  var here = document.currentScript;
  var nb = here.closest(".nb");
  if(!nb) return;
  var scroller = nb.querySelector(".nb__scroll");
  var links = nb.querySelectorAll(".nb__toc a");
  // native smooth scroll is unreliable inside this container, so animate by hand
  function glide(el, to, dur){
    var start = el.scrollTop, delta = to - start, t0 = null;
    function ease(t){ return t < 0.5 ? 2*t*t : -1 + (4 - 2*t)*t; }
    function step(ts){
      if(t0 === null) t0 = ts;
      var p = Math.min((ts - t0) / dur, 1);
      el.scrollTop = start + delta * ease(p);
      if(p < 1) requestAnimationFrame(step);
    }
    requestAnimationFrame(step);
  }
  links.forEach(function(a){
    a.addEventListener("click", function(e){
      e.preventDefault();
      var id = (a.hash || "").slice(1);
      var target = id && nb.querySelector("#" + id);
      if(target && scroller){
        var top = target.getBoundingClientRect().top
                - scroller.getBoundingClientRect().top
                + scroller.scrollTop;
        var dest = Math.max(0, top - 6);
        var reduce = window.matchMedia &&
                     window.matchMedia("(prefers-reduced-motion: reduce)").matches;
        if(reduce || document.hidden) scroller.scrollTop = dest;
        else glide(scroller, dest, 360);
      }
      links.forEach(function(x){ x.classList.remove("is-active"); });
      a.classList.add("is-active");
      // on phones the contents live in a drawer; close it so the cell is visible
      if(window.matchMedia("(max-width: 640px)").matches) nb.classList.remove("is-side-open");
    });
  });

  // one collapsible sidebar; tabs switch between contents and the assistant.
  // on phones it is a slide-in drawer, so start it closed there (open on desktop)
  var sideMq = window.matchMedia("(max-width: 640px)");
  function isPhone(){ return sideMq.matches; }
  function syncSide(){
    if(isPhone()) nb.classList.remove("is-side-open");
    else nb.classList.add("is-side-open");
  }
  syncSide();
  if(sideMq.addEventListener) sideMq.addEventListener("change", syncSide);
  else if(sideMq.addListener) sideMq.addListener(syncSide);

  // desktop: top toolbar button. phones: the collapsed sidebar rail. both toggle.
  function toggleSide(){
    nb.classList.add("is-anim");            // enable the slide animation after first use
    nb.classList.toggle("is-side-open");
  }
  var sidebtn = nb.querySelector(".nb__sidebtn");
  if(sidebtn) sidebtn.addEventListener("click", toggleSide);
  var rail = nb.querySelector(".nb__rail");
  if(rail) rail.addEventListener("click", toggleSide);
  var aiClose = nb.querySelector(".nb__ai-x");
  if(aiClose) aiClose.addEventListener("click", function(){
    nb.classList.remove("is-side-open");
  });
  // on phones, tapping the dimmed cells behind the open drawer closes it
  var main = nb.querySelector(".nb__main");
  if(main) main.addEventListener("click", function(e){
    if(!isPhone() || !nb.classList.contains("is-side-open")) return;
    if(e.target.closest(".nb__side") || e.target.closest(".nb__rail")) return;
    nb.classList.remove("is-side-open");
  });
  var tabs = nb.querySelectorAll(".nb__tab");
  var panels = nb.querySelectorAll(".nb__panel");
  tabs.forEach(function(t){
    t.addEventListener("click", function(){
      var name = t.getAttribute("data-tab");
      tabs.forEach(function(x){ x.classList.toggle("is-active", x === t); });
      panels.forEach(function(p){ p.classList.toggle("is-active", p.getAttribute("data-panel") === name); });
    });
  });
  // clicking a contents link also makes sure the contents tab is the one showing
  links.forEach(function(a){
    a.addEventListener("click", function(){
      tabs.forEach(function(x){ x.classList.toggle("is-active", x.getAttribute("data-tab") === "toc"); });
      panels.forEach(function(p){ p.classList.toggle("is-active", p.getAttribute("data-panel") === "toc"); });
    });
  });

  // only the chevron collapses a code cell, not the rest of the bar
  nb.querySelectorAll(".nb-codecell > summary").forEach(function(s){
    s.addEventListener("click", function(e){
      if(!e.target.closest(".nb-codecell__chev")) e.preventDefault();
    });
  });

  var codeCells = nb.querySelectorAll(".nb-cell--code");
  function activateCodeCell(cell){
    codeCells.forEach(function(x){ x.classList.toggle("is-cell-active", x === cell); });
  }
  codeCells.forEach(function(cell){
    cell.addEventListener("click", function(){ activateCodeCell(cell); });
  });
  nb.addEventListener("click", function(e){
    if(!e.target.closest(".nb-cell--code")) activateCodeCell(null);
  });

  // mark this notebook's "live" controls as no-ops; the shared page-level
  // handler (see _fig-styles.qmd) shows the "just a mockup" note on click
  var EGG_SEL = ".nb__runall, .nb__publish, .nb-codecell__bar .nb-tool, .nb-aibtn, .nb__ai-field, .nb__prov a";
  nb.querySelectorAll(".nb-aibtn").forEach(function(el){
    el.setAttribute("role", "button");
    el.setAttribute("aria-label", "Ask AI about this cell");
  });
  nb.querySelectorAll(EGG_SEL).forEach(function(el){ el.classList.add("mockup-noop"); });
})();
</script>
</div>
</div>
</div>
</div>
<p>There is a lot to unpack here. Here are notable changes:</p>
<ul>
<li>The agent optionally performs retrieval from vetted analyses, and the interface shows which one was used along with who authored it.</li>
<li>There is progressive disclosure of details. The chat reply shows high value items like sources, assumptions, and issues. The user can optionally open an interactive notebook to see the full context.</li>
<li>The AI-generated notebook (see notebook tab above) is organized to promote verification: it opens with the assumptions the agent made, like the metric definition and where it came from, then shows the queries it ran and the numbers they returned. It breaks the total down so you can sanity-check the distribution, and it closes with a list of what it could not verify, each item left as a cell you can run.</li>
<li>The AI agent is also available in the notebook to help with follow-ups. Finally, the user can publish the notebook back to a knowledge base, where it can be retrieved by future analyses, creating a virtuous cycle.</li>
</ul>
<p>This design sketch is far from perfect. The point is that the product should help the user verify the answer as a domain expert would. Compare it to the earlier approach, where the only output was the number.</p>
<p>Data agents like these are not science fiction. Hex<sup>4</sup> is my favorite product in this genre; it integrates notebooks and chat better than anything I’ve seen. Here are screenshots from their landing page:</p>
<div class="quarto-layout-panel" data-layout-ncol="2">
<div class="quarto-layout-row">
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-smell/hex-chat.png" class="img-fluid figure-img"></p>
<figcaption>Chat interface.</figcaption>
</figure>
</div>
</div>
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-smell/hex-notebook.png" class="img-fluid figure-img"></p>
<figcaption>Notebook view which allows the user to see the intermediate steps and data.</figcaption>
</figure>
</div>
</div>
</div>
</div>
<p>But what does this have to do with evals? If you design your product for verification, annotation becomes less expensive and evals will have better signals to draw from. More importantly, you’ll provide your users with a better product.</p>
</section>
<section id="example-2-the-pe-curriculum-builder" class="level2">
<h2 class="anchored" data-anchor-id="example-2-the-pe-curriculum-builder">Example 2: the PE curriculum builder</h2>
<p>A founder I advised was building an AI tool that writes physical education lesson plans for K-12 teachers. A teacher enters their constraints, like the grade they teach, how long the class is, whether it meets indoors or outdoors, and what equipment they have. The tool then writes a lesson plan for those constraints. The goal is to save teachers the time they spend planning and give them a plan that fits their class. Here is a sketch of what the product looks like:</p>
<!-- Figure: the PE lesson plan desktop app. Inputs on the left, a plan generated from scratch on the right (the "before" design). -->
<div class="pe-figure">
  <div class="pe-app">
    <div class="pe-app__bar">
      <span class="pe-app__lights"><span class="pe-light pe-light--r"></span><span class="pe-light pe-light--y"></span><span class="pe-light pe-light--g"></span></span>
      <span class="pe-app__title">PE Planner</span>
    </div>
    <div class="pe-app__split">

      <div class="pe-inputs">
        <div class="pe-inputs__head">Class details</div>
        <div class="pe-field">
          <span class="pe-field__label">Grade</span>
          <span class="pe-field__input">Grade 4<span class="pe-field__caret">▾</span></span>
        </div>
        <div class="pe-field">
          <span class="pe-field__label">Class length</span>
          <span class="pe-field__input">45 minutes<span class="pe-field__caret">▾</span></span>
        </div>
        <div class="pe-field">
          <span class="pe-field__label">Location</span>
          <span class="pe-field__input">Outdoors<span class="pe-field__caret">▾</span></span>
        </div>
        <div class="pe-field">
          <span class="pe-field__label">Equipment</span>
          <span class="pe-field__input">12 cones, 6 balls</span>
        </div>
        <span class="pe-generate">Generate lesson plan</span>
      </div>

      <div class="pe-doc">
        <div class="pe-doc__header">
          <span class="pe-doc__title">Ball skills</span>
          <span class="pe-doc__sub">Lesson 1 of 8</span>
          <span class="pe-doc__tag">Generated plan</span>
        </div>
        <div class="pe-fill"><div class="pe-scroll">
          <div class="pe-sec">
            <div class="pe-sec__head">Warm-up · 8 min</div>
            <p>Jog the perimeter, then dynamic stretches: arm circles, lunges, and high knees.  Finish with a partner toss to warm up the hands.</p>
          </div>
          <div class="pe-sec">
            <div class="pe-sec__head">Main activity · 30 min</div>
            <p>Pair students and set cones 10 feet apart.  Partners practice underhand throws, then overhand.  Cue them to step with the opposite foot and follow through.  Widen the gap as accuracy improves and rotate partners every 5 minutes.</p>
          </div>
          <div class="pe-sec">
            <div class="pe-sec__head">Cool-down · 7 min</div>
            <p>Static stretches and a quick recap of throwing cues: step, point, follow through.</p>
          </div>
          <div class="pe-sec">
            <div class="pe-sec__head">Assessment · notes</div>
            <p>Watch for a stepped throw with the opposite foot forward and eyes on the target.  Note students who need a shorter throwing distance next class.</p>
          </div>
          <div class="pe-sec">
            <div class="pe-sec__head">Equipment</div>
            <p>12 cones, 6 foam balls, 3 station markers.</p>
          </div>
        </div></div>
      </div>

    </div>
  </div>
  <div class="pe-app__cap">The teacher enters constraints and the tool writes a plan from scratch.  The only output is the plan, so its difficult to verify.</div>
</div>
<p>The founder asked me how to eval the lesson plans. I turned the question around: <strong>what does a teacher care about?</strong></p>
<p>The fastest way to trust a plan is to see that a teacher like them already uses it. Additionally, teachers value visibility into what others are doing so they can learn new approaches. Therefore, a better design might start from vetted lesson plans that are actively used in schools. When the tool generates a plan, it shows which vetted plan it started from, who uses that plan, and a diff of what it changed for this teacher’s constraints.</p>
<p>Next, the teacher can check a small set of changes against a plan they already trust, instead of judging a whole plan from scratch. Here’s a sketch of what a better interface might look like:</p>
<!-- Figure: the PE plan as a desktop app. Lineage on top, then a split of the full plan and its diff, with changed lines keyed to the diff (the "after" design). -->
<div class="pe-figure">
  <div class="pe-app">
    <div class="pe-app__bar">
      <span class="pe-app__lights"><span class="pe-light pe-light--r"></span><span class="pe-light pe-light--y"></span><span class="pe-light pe-light--g"></span></span>
      <span class="pe-app__title">PE Planner</span>
    </div>
    <div class="pe-app__body">

      <div class="pe-doc__header">
        <span class="pe-doc__title">Ball skills</span>
        <span class="pe-doc__sub">Lesson 1 of 8</span>
        <span class="pe-doc__tag">Generated plan</span>
      </div>

      <div class="pe-src">
        View original
        <div class="pe-src__id">
          <span class="pe-src__avatar">DR</span>
          <span class="pe-src__name">Adapted from Dana Ruiz's plan</span>
        </div>
        <div class="pe-src__meta">Grade 4 PE · Lincoln Elementary, Austin TX</div>
        <div class="pe-src__signals">
          <span class="pe-sig">Used at 14 schools</span>
          <span class="pe-sig">Run 30+ times this year</span>
          <span class="pe-sig">Aligned to SHAPE standards</span>
        </div>
      </div>

      <div class="pe-split2">

        <div class="pe-pane pe-pane--full">
          <div class="pe-pane__head"><span>Full plan</span></div>
          <div class="pe-fill"><div class="pe-scroll">
            <div class="pe-line__sec">Warm-up · 8 min</div>
            <div class="pe-line pe-line--changed"><span class="pe-badge">1</span><span>Jog 1 lap to the far cones and back, then dynamic stretches: arm circles, lunges, and high knees.</span></div>
            <div class="pe-line">Finish with partner toss to warm up the hands, 10 throws each.</div>
            <div class="pe-line__sec">Main activity · 30 min</div>
            <div class="pe-line pe-line--changed"><span class="pe-badge">2</span><span>3 stations with cones as targets.  Pairs throw underhand then overhand with 6 balls, rotating every 5 minutes.</span></div>
            <div class="pe-line">Cue students to step with the opposite foot and follow through toward the target.</div>
            <div class="pe-line">Widen the distance between partners as accuracy improves.</div>
            <div class="pe-line__sec">Cool-down · 7 min</div>
            <div class="pe-line">Static stretches and a recap of throwing cues: step, point, follow through.</div>
            <div class="pe-line__sec">Assessment · notes</div>
            <div class="pe-line">Watch for a stepped throw with the opposite foot forward and eyes on the target.</div>
            <div class="pe-line">Note students who need a shorter throwing distance next class.</div>
            <div class="pe-line__sec">Equipment</div>
            <div class="pe-line">12 cones, 6 foam balls, 3 station markers.</div>
          </div></div>
        </div>

        <div class="pe-pane pe-pane--diff">
          <div class="pe-pane__head"><span>What changed · 2 edits</span><span class="pe-review"><span class="pe-allbtn pe-allbtn--accept">Accept all</span><span class="pe-allbtn">Reject all</span></span></div>

          <div class="pe-dgroup">
            <div class="pe-dgroup__head"><span class="pe-badge">1</span> Warm-up</div>
            <div class="pe-drow pe-drow--del"><span class="pe-drow__mark">−</span><span class="pe-drow__text">Jog 2 laps, then dynamic stretches (10 min)</span></div>
            <div class="pe-drow pe-drow--add"><span class="pe-drow__mark">+</span><span class="pe-drow__text">Jog 1 lap, then dynamic stretches (8 min)</span></div>
            <div class="pe-dwhy">Shortened to fit a 45-minute class</div>
            <div class="pe-dactions">
              <span class="pe-dbtn pe-dbtn--accept">Accept <span class="pe-dbtn__key">⌘⏎</span></span>
              <span class="pe-dbtn pe-dbtn--reject">Reject <span class="pe-dbtn__key">⌘⌫</span></span>
            </div>
          </div>

          <div class="pe-dgroup">
            <div class="pe-dgroup__head"><span class="pe-badge">2</span> Main activity</div>
            <div class="pe-drow pe-drow--del"><span class="pe-drow__mark">−</span><span class="pe-drow__text">4 stations, 8 balls, hoops as targets</span></div>
            <div class="pe-drow pe-drow--add"><span class="pe-drow__mark">+</span><span class="pe-drow__text">3 stations, 6 balls, cones as targets</span></div>
            <div class="pe-dwhy">Matched to your equipment</div>
            <div class="pe-dactions">
              <span class="pe-dbtn pe-dbtn--accept">Accept <span class="pe-dbtn__key">⌘⏎</span></span>
              <span class="pe-dbtn pe-dbtn--reject">Reject <span class="pe-dbtn__key">⌘⌫</span></span>
            </div>
          </div>
        </div>

      </div>

    </div>
  </div>
  <div class="pe-app__cap">The plan is anchored to a vetted plan another teacher uses.  The changes are marked so the teacher can check a diff against a plan they trust.</div>
</div>
<p>In this version, most of the plan is inherited from a vetted plan. The teacher’s review is scoped to a few edits, each with a reason explaining why the change was made. This is a more efficient way to review a plan because it reduces the cognitive load of judging a whole plan from scratch.</p>
<p>Designing for this makes the product simpler to build. Instead of stuffing hundreds of examples into a prompt, the tool captures important dimensions, retrieves a close match, and adapts it. Automated evals now become tractable because there is less surface area to test. For example, you can verify that the plan retrieval picked a sensible anchor, and each edit honors the constraints.</p>
</section>
<section id="example-3-the-workers-comp-medical-report" class="level2">
<h2 class="anchored" data-anchor-id="example-3-the-workers-comp-medical-report">Example 3: the workers-comp medical report</h2>
<p>The last example comes from a workers’ compensation tool a founder asked me to help with. It reads a patient’s chart (intake forms, imaging reports, therapy notes, prior exams) and generates a long expert opinion report, often fifty pages or more. Here is a sketch of the product:</p>
<!-- Figure: the workers-comp tool whose only output is a long narrative report (the "before" design). -->
<div class="wc-figure">
  <div class="wc-app">
    <div class="wc-titlebar">
      <span class="wc-titlebar__name">ClaimDraft</span>
      <span class="wc-win"><span>–</span><span>□</span><span>✕</span></span>
    </div>
    <div class="wc-menubar"><span>File</span><span>Edit</span><span>Case</span><span>Report</span><span>View</span><span>Help</span></div>
    <div class="wc-body">

      <div class="wc-side">
        <div class="wc-side__head">Source records · 18</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Intake questionnaire</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>MRI, lumbar spine</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>X-ray report</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Physical therapy notes</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Treating physician notes</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Prior IME</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Work restrictions</div>
        <div class="wc-side__more">+ 11 more</div>
      </div>

      <div class="wc-main">
        <div class="wc-case"><span><b>Claimant</b> J. Doe</span><span><b>Claim</b> #WC-20259417</span><span><b>Date of injury</b> Mar 3, 2025</span><span><b>Examiner</b> Dr. A. Patel</span></div>

        <div class="wc-report">
          <div class="wc-report__title">Independent Medical Evaluation</div>
          <div class="wc-report__sub">Permanent &amp; Stationary Report</div>

          <div class="wc-sec">
            <div class="wc-sec__h">1.  History of injury</div>
            <p>The claimant is a 47-year-old warehouse associate who reports a lumbar spine injury on March 3, 2025 while lifting a carton estimated at sixty pounds.  He describes immediate low back pain radiating into the right lower extremity, followed by numbness along the lateral calf.</p>
          </div>
          <div class="wc-sec">
            <div class="wc-sec__h">2.  Review of records</div>
            <p>Records reviewed include the intake questionnaire, an MRI of the lumbar spine dated March 18, 2025, twelve physical therapy notes, and the treating physician's progress reports through August 2025.</p>
          </div>
          <div class="wc-sec">
            <div class="wc-sec__h">3.  Physical examination</div>
            <p>On examination, lumbar flexion was limited to 40 degrees with pain.  Straight leg raise was positive on the right at 50 degrees.  Strength was 4 of 5 in the right extensor hallucis longus, with diminished sensation in the L5 distribution.</p>
          </div>
          <div class="wc-sec">
            <div class="wc-sec__h">4.  Diagnoses</div>
            <p>Lumbar disc herniation at L5-S1 with associated right L5 radiculopathy, supported by the imaging and examination findings above.</p>
          </div>
          <div class="wc-sec">
            <div class="wc-sec__h">5.  Causation analysis</div>
            <p>Within reasonable medical probability, the disc herniation is causally related to the industrial lifting event of March 3, 2025.  The claimant had no documented history of lumbar treatment prior to that date.</p>
          </div>
        </div>

        <div class="wc-foot"><span>Page 1 of 52</span><span>Generated from 18 records</span></div>
      </div>

    </div>
  </div>
  <div class="wc-app__cap">The only output is a fifty-page narrative.  To trust it, the doctor has to re-read the whole chart and check every claim, which can take as long as writing the report from scratch.</div>
</div>
<p>The problem is the same as the other examples, but the stakes are higher. The only output is the report, and the doctor is the one accountable for it. To trust it they have to go back through the chart and confirm the facts and inferences themselves. That can take as long as writing the report from scratch, which defeats the point of the tool.</p>
<p>You might object that a fifty-page opinion is hard to verify. That is true, and the product should not pretend otherwise. Helping a doctor understand the evidence is arguably more valuable than the finished document. Therefore, I advised the founder to make the product work like a research assistant instead of a report generator.</p>
<p>For example, the product could read every record and pull out relevant facts, with a link back to the page so the doctor can check each one. Where two exams disagree, or the chart leaves a question open, the product should surface that. The doctor can then resolve any contradictions and fill in the gaps. Finally, the product can assemble the final report from what they have already checked. Here is a sketch of what this might look like:</p>
<!-- Figure: the workers-comp tool as a review console. The AI surfaces typed, cited findings the doctor resolves; the report is gated on that review (the "after" design). -->
<div class="wc-figure">
  <div class="wc-app">
    <div class="wc-titlebar">
      <span class="wc-titlebar__name">ClaimDraft</span>
      <span class="wc-win"><span>–</span><span>□</span><span>✕</span></span>
    </div>
    <div class="wc-menubar"><span>File</span><span>Edit</span><span>Case</span><span>Review</span><span>Report</span><span>Help</span></div>
    <div class="wc-case"><span><b>Claimant</b> J. Doe</span><span><b>Claim</b> #WC-20259417</span><span><b>Date of injury</b> Mar 3, 2025</span><span><b>Examiner</b> Dr. A. Patel</span></div>
    <div class="wc-body">

      <div class="wc-side">
        <div class="wc-side__head">Records · 18</div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>MRI, lumbar spine<span class="wc-doc__st wc-doc__st--done">✓</span></div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Physical therapy notes<span class="wc-doc__st wc-doc__st--done">✓</span></div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Treating physician notes<span class="wc-doc__st wc-doc__st--done">✓</span></div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Prior IME<span class="wc-doc__st wc-doc__st--todo">○</span></div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>Intake questionnaire<span class="wc-doc__st wc-doc__st--todo">○</span></div>
        <div class="wc-doc"><span class="wc-doc__ic"></span>X-ray report<span class="wc-doc__st wc-doc__st--todo">○</span></div>
        <div class="wc-side__more">9 of 18 reviewed</div>
      </div>

      <div class="wc-finds">
        <div class="wc-find">
          <div class="wc-find__head"><span class="wc-find__num">1</span><span class="wc-tag wc-tag--warn">Contradiction</span></div>
          <div class="wc-find__body">Straight-leg-raise is recorded as positive on the right by the treating physician (p. 31) and negative by the prior IME (p. 22).</div>
          <div class="wc-find__foot"><span class="wc-abtn wc-abtn--primary">Resolve</span>Open both pages</div>
        </div>

        <div class="wc-find">
          <div class="wc-find__head"><span class="wc-find__num">2</span><span class="wc-tag wc-tag--fact">Key fact</span></div>
          <div class="wc-find__body">The intake form notes a prior lumbar strain in 2019, which bears on causation (p. 14).</div>
          <div class="wc-find__foot"><span class="wc-abtn wc-abtn--primary">Include</span><span class="wc-abtn">Dismiss</span></div>
        </div>

        <div class="wc-find">
          <div class="wc-find__head"><span class="wc-find__num">3</span><span class="wc-tag wc-tag--q">Open question</span></div>
          <div class="wc-find__body">No imaging appears in the file after March 18, 2025, so the current status of the herniation is unconfirmed.</div>
          <div class="wc-find__foot"><span class="wc-abtn">Add a note</span></div>
        </div>

        <div class="wc-find">
          <div class="wc-find__head"><span class="wc-find__num">4</span><span class="wc-tag wc-tag--fact">Key fact</span></div>
          <div class="wc-find__body">The MRI report describes pre-existing degenerative changes at L4-L5 (p. 9).</div>
          <div class="wc-find__foot"><span class="wc-abtn wc-abtn--primary">Include</span><span class="wc-abtn">Dismiss</span></div>
        </div>
      </div>

    </div>
    <div class="wc-foot"><span class="wc-foot__status">6 of 24 findings still need review</span><span class="wc-foot__gen">Generate report draft</span></div>
  </div>
  <div class="wc-app__cap">The product walks the doctor through the evidence first, surfacing each finding with a link back to the chart. Afterwards, the final report is built from the facts they reviewed.</div>
</div>
<p>The research assistant version of this product allows the doctor to build trust by verifying facts as they go. Similar to the other examples, this design is easier to build and evaluate. Now there are scoped units to grade, such as whether a contradiction is real or whether a citation supports a claim.</p>
</section>
<section id="generalizing-the-pattern" class="level2">
<h2 class="anchored" data-anchor-id="generalizing-the-pattern">Generalizing the pattern</h2>
<p>It is important to understand how users verify your product’s AI artifacts. Sometimes, this may require assembling supporting evidence. In other cases, it could mean refactoring the entire workflow so that the user is in the loop (like the workers’ comp example).</p>
<p>Below are questions that can guide your product’s design for verification:</p>
<ol type="1">
<li>What does the user actually need to check?</li>
<li>What trusted thing can they compare it against?</li>
<li>Are there signals or heuristics that experts use to aid in verification?</li>
<li>What smaller units can they accept, edit, or reject?</li>
</ol>
<p>A common thread across these examples is provenance. The fastest way to make an output checkable is to show where each part came from, with links to see more detail. Additionally, you can use progressive disclosure so these sources don’t overwhelm the user.</p>
<p>What needs verifying also changes as the user’s trust grows. Early on, the data agent should make provenance obvious, like where a metric definition came from. Once the user trusts the agent gets it right, that detail can collapse by default in the card. Good design meets users where they are instead of showing everything.</p>
<p>These principles hold even when a product seems easy to eval. Coding is a good example: it is one of the most verifiable kinds of work, with tests, types, and diffs. Even so, some coding agents go the extra mile to make their work checkable. Cursor and Devin both record a short video of the UI changes they make, so you can confirm the work is right without reproducing it yourself.<sup>5</sup></p>
</section>
<section id="none-of-this-is-new" class="level2">
<h2 class="anchored" data-anchor-id="none-of-this-is-new">None of this is new</h2>
<p>Evals thinking is aligned with good product design. Gathering supporting data and breaking down workflows into smaller units makes automated grading easier. However, I don’t want to pretend like any wisdom here is new.</p>
<p>All of these ideas stem from well-established design principles. For example, watching an expert work to learn what they check before you build is called needfinding.<sup>6</sup> In research-heavy work like the medical case, there is a design goal called sensemaking, which is the work of building a structured understanding of a body of evidence you can reason over.<sup>7</sup> There are many other concepts, but I think you get the idea.</p>
<p>Even though these ideas are well established, a reminder is due in the age of AI. Before AI, verification often happened incidentally during the process of creating work product. With AI, verification is the bottleneck. It is time to think about it more explicitly.</p>
<hr>
<p><em>Thanks to <a href="https://www.sh-reya.com/">Shreya Shankar</a> and <a href="https://isaacflath.com/">Isaac Flath</a> for feedback on this post.</em></p>
<div class="eval-smell-newsletter">
<p>To stay updated on my writing on AI evals, subscribe to my newsletter below.</p>
<script async="" data-uid="295eebc652" src="https://hamel.kit.com/295eebc652/index.js"></script>
<style>
.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] {
  padding-top: 10px !important;
}

.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] .formkit-fields {
  max-width: 480px !important;
  width: 100% !important;
}

.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] .formkit-field {
  flex: 1 1 auto !important;
}

.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] .formkit-input {
  width: 100% !important;
}

.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] .formkit-submit {
  background-color: #447099 !important;
  border-color: #447099 !important;
  flex: 0 0 128px !important;
  width: 128px !important;
}

.eval-smell-newsletter .formkit-form[data-uid="295eebc652"] .formkit-submit:hover {
  background-color: #305775 !important;
  border-color: #305775 !important;
}

.eval-smell-newsletter .formkit-powered-by,
.eval-smell-newsletter .formkit-powered-by-convertkit,
.eval-smell-newsletter .formkit-powered-by-convertkit-container {
  display: none !important;
}
</style>
</div>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>More of my writing and teaching on evals: <a href="../../../blog/posts/evals/index.html">Your AI Product Needs Evals</a>, <a href="../../../blog/posts/field-guide/index.html">A Field Guide to Rapidly Improving AI Products</a>, <a href="../../../blog/posts/llm-judge/index.html">Using LLM-as-a-Judge for Evaluation</a>, <a href="../../../blog/posts/evals-faq/index.html">LLM Evals: Everything You Need to Know</a>, <a href="../../../blog/posts/eval-tools/index.html">Selecting the Right AI Evals Tool</a>, <a href="../../../blog/posts/evals-skills/index.html">Evals Skills for Coding Agents</a> and <a href="../../../blog/posts/revenge/index.html">The Revenge of the Data Scientist</a>. I also co-teach the <a href="https://maven.com/parlance-labs/evals?promoCode=evals-info-url">AI Evals for Engineers &amp; PMs</a> course and co-authored the O’Reilly book <a href="https://www.oreilly.com/library/view/evals-for-ai/9798341660717/">Evals for AI Engineers</a>.↩︎</p></li>
<li id="fn2"><p><a href="https://x.com/lennysan/status/2054631157191598294">Lenny Rachitsky tweeted about this recently</a>: most of one data science team’s work is now reviewing half-baked AI analysis from PMs and engineers, and half of it is wrong.↩︎</p></li>
<li id="fn3"><p>When I worked at Airbnb we had an internal tool called the <a href="https://github.com/airbnb/knowledge-repo">Knowledge Repo</a>, a place where data scientists published notebooks of their deep dives on analytics, modeling, and so on (<a href="https://medium.com/airbnb-engineering/scaling-knowledge-at-airbnb-875d73eff091">they wrote about it here</a>). It was one of the fastest ways to get context on a new project, since you could read what someone had already worked out. I don’t know if it is still in use, but the paradigm is a good one.↩︎</p></li>
<li id="fn4"><p><a href="https://www.linkedin.com/in/bryan-bischof/">Bryan Bischof</a> led the creation of the AI at <a href="https://www.hex.tech/">Hex</a>. Bryan is a data scientist himself, the kind of domain expert the product serves, and I think that is part of why it is designed well.↩︎</p></li>
<li id="fn5"><p>See a <a href="https://www.youtube.com/watch?v=XbZvC4KTH68">demo of Cursor</a> recording its UI changes, and <a href="https://docs.devin.ai/work-with-devin/testing-and-recordings">Devin’s documentation</a> on testing and recordings.↩︎</p></li>
<li id="fn6"><p>Dev Patnaik and Robert Becker, <a href="https://onlinelibrary.wiley.com/doi/10.1111/j.1948-7169.1999.tb00250.x">“Needfinding: The Why and How of Uncovering People’s Needs”</a>, Design Management Journal.↩︎</p></li>
<li id="fn7"><p>Daniel M. Russell, Mark J. Stefik, Peter Pirolli, and Stuart K. Card, <a href="https://www.markstefik.com/wp-content/uploads/2014/04/1993-Cost-Structure-of-Sensemaking1.pdf">“The Cost Structure of Sensemaking”</a>.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-smell/</guid>
  <pubDate>Mon, 29 Jun 2026 07:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-smell/social-card.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Q: What gaps in eval tooling should I be prepared to fill myself?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Most eval tools handle the basics well: logging complete traces, tracking metrics, prompt playgrounds, and annotation queues. These are table stakes. Here are four areas where you’ll likely need to supplement existing tools.</p>
<p>Watch for vendors addressing these gaps: it’s a strong signal they understand practitioner needs.</p>
<section id="error-analysis-and-pattern-discovery" class="level3">
<h3 class="anchored" data-anchor-id="error-analysis-and-pattern-discovery">1. Error Analysis and Pattern Discovery</h3>
<p>After reviewing traces where your AI fails, can your tooling automatically cluster similar issues? For instance, if multiple traces show the assistant using casual language for luxury clients, you need something that recognizes this broader “persona-tone mismatch” pattern. We recommend building capabilities that use AI to suggest groupings, rewrite your observations into clearer failure taxonomies, help find similar cases through semantic search, etc.</p>
</section>
<section id="ai-powered-assistance-throughout-the-workflow" class="level3">
<h3 class="anchored" data-anchor-id="ai-powered-assistance-throughout-the-workflow">2. AI-Powered Assistance Throughout the Workflow</h3>
<p>The most effective workflows use AI to accelerate every stage of evaluation. During error analysis, you want an LLM helping categorize your open-ended observations into coherent failure modes. For example, you might annotate several traces with notes like “wrong tone for investor,” “too casual for luxury buyer,” etc. Your tooling should recognize these as the same underlying pattern and suggest a unified “persona-tone mismatch” category.</p>
<p>You’ll also want AI assistance in proposing fixes. After identifying 20 cases where your assistant omits pet policies from property summaries, can your workflow analyze these failures and suggest specific prompt modifications? Can it draft refinements to your SQL generation instructions when it notices patterns of missing WHERE clauses?</p>
<p>Good workflows also help you conduct data analysis of your annotations and traces. I like using notebooks with AI in-the-loop like <a href="https://julius.ai/" target="_blank">Julius</a> or <a href="https://hex.tech" target="_blank">Hex</a>. These help me discover insights like “location ambiguity errors spike 3x when users mention neighborhood names” or “tone mismatches occur 80% more often in email generation than other modalities.”</p>
</section>
<section id="custom-evaluators-over-generic-metrics" class="level3">
<h3 class="anchored" data-anchor-id="custom-evaluators-over-generic-metrics">3. Custom Evaluators Over Generic Metrics</h3>
<p>Be prepared to build most of your evaluators from scratch. Generic metrics like “hallucination score” or “helpfulness rating” rarely capture what actually matters for your application—like proposing unavailable showing times or omitting budget constraints from emails. In our experience, successful teams spend most of their effort on application-specific metrics.</p>
</section>
<section id="apis-that-support-custom-annotation-apps" class="level3">
<h3 class="anchored" data-anchor-id="apis-that-support-custom-annotation-apps">4. APIs That Support Custom Annotation Apps</h3>
<p>Custom annotation interfaces <a href="../../../blog/posts/evals-faq/should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf.html" target="_blank">work best for most teams</a>. This requires observability platforms with thoughtful APIs. I often have to build my own libraries and abstractions just to make bulk data export manageable. You shouldn’t have to paginate through thousands of requests or handle timeout-prone endpoints just to get your data. Look for platforms that provide true bulk export capabilities and, crucially, APIs that let you write annotations back efficiently.</p>
<p><a href="../../../blog/posts/evals-faq/#q-what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>


</section>

 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself.html</guid>
  <pubDate>Wed, 10 Jun 2026 04:47:12 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: What’s a minimum viable evaluation setup?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/whats-a-minimum-viable-evaluation-setup.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Start with <a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" target="_blank">error analysis</a>, not infrastructure. Spend 30 minutes manually reviewing 20-50 LLM outputs whenever you make significant changes. Use one <a href="../../../blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html" target="_blank">domain expert</a> who understands your users as your quality decision maker (a “<a href="../../../blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html" target="_blank">benevolent dictator</a>”).</p>
<p><strong>Use a notebook</strong> to review traces and analyze data, or build your own <a href="../../../blog/posts/evals-faq/what-makes-a-good-custom-interface-for-reviewing-llm-outputs.html" target="_blank">custom annotation interface</a> with an AI coding assistant like Claude or Codex. Either way, you can write arbitrary code, visualize data, and iterate quickly. The <a href="https://youtu.be/aqKUwPKBkB0?si=5KDmMQnRzO_Ce9xH" target="_blank">video</a> below shows a simple annotation interface built inside a notebook.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/aqKUwPKBkB0" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="../../../blog/posts/evals-faq/#q-whats-a-minimum-viable-evaluation-setup" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/whats-a-minimum-viable-evaluation-setup.html</guid>
  <pubDate>Wed, 10 Jun 2026 04:47:12 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: Why is “error analysis” so important in LLM evals, and how is it performed?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Error analysis is <strong>the most important activity in evals</strong>. Error analysis helps you decide what evals to write in the first place. It allows you to identify failure modes unique to your application and data. The process involves:</p>
<section id="creating-a-dataset" class="level3">
<h3 class="anchored" data-anchor-id="creating-a-dataset">1. Creating a Dataset</h3>
<p>Gathering representative traces of user interactions with the LLM. If you do not have any data, you can <a href="../../../blog/posts/evals-faq/what-is-the-best-approach-for-generating-synthetic-data.html" target="_blank">generate synthetic data</a> to get started.</p>
</section>
<section id="open-coding" class="level3">
<h3 class="anchored" data-anchor-id="open-coding">2. Open Coding</h3>
<p>Human annotator(s) (ideally a <a href="../../../blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html" target="_blank">benevolent dictator</a>) review and write open-ended notes about traces, noting any issues. This process is akin to “journaling” and is adapted from qualitative research methodologies. When beginning, it is recommended to focus on noting the <a href="../../../blog/posts/evals-faq/how-do-i-debug-multi-turn-conversation-traces.html" target="_blank">first failure</a> observed in a trace, as upstream errors can cause downstream issues, though you can also tag all independent failures if feasible. A <a href="https://hamel.dev/blog/posts/llm-judge/#step-1-find-the-principal-domain-expert" target="_blank">domain expert</a> should be performing this step.</p>
</section>
<section id="axial-coding" class="level3">
<h3 class="anchored" data-anchor-id="axial-coding">3. Axial Coding</h3>
<p>Categorize the open-ended notes into a “failure taxonomy.”. In other words, group similar failures into distinct categories. This is the most important step. At the end, count the number of failures in each category. You can use a LLM to help with this step.</p>
</section>
<section id="iterative-refinement" class="level3">
<h3 class="anchored" data-anchor-id="iterative-refinement">4. Iterative Refinement</h3>
<p>Keep iterating on more traces until you reach <a href="https://delvetool.com/blog/theoreticalsaturation" target="_blank">theoretical saturation</a>, meaning new traces do not seem to reveal new failure modes or information to you. As a rule of thumb, you should aim to review at least 100 traces. My rough heuristic is if ~20 traces don’t turn up a new category, you can stop (but review at least 100 to start). Remember, the goal is to prioritize the failures that actually happen the most, not catch every possible failure (evals are not free, so we need to be pragmatic).</p>
<p>You should frequently revisit this process. There are advanced ways to <a href="how-can-i-efficiently-sample-production-traces-for-review.html" target="_blank">sample data more efficiently</a>, like clustering, sorting by user feedback, and sorting by high probability failure patterns. Over time, you’ll develop a “nose” for where to look for failures in your data.</p>
<p>Do not skip error analysis. It ensures that the evaluation metrics you develop are supported by real application behaviors instead of counter-productive generic metrics (which most platforms nudge you to use). For examples of how error analysis can be helpful, see <a href="https://www.youtube.com/watch?v=e2i6JbU2R-s" target="_blank">this video</a>, or this <a href="https://hamel.dev/blog/posts/field-guide/" target="_blank">blog post</a>.</p>
<p>Here is a visualization of the error analysis process by one of our students, <a href="https://www.linkedin.com/in/pawel-huryn/" target="_blank">Pawel Huryn</a> - including how it fits into the overall evaluation process:</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/pawel-error-analysis.png" class="img-fluid"></p>
<p><a href="../../../blog/posts/evals-faq/#q-why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>


</section>

 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html</guid>
  <pubDate>Wed, 10 Jun 2026 04:47:12 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>The Revenge of the Data Scientist</title>
  <dc:creator>Hamel Husain</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->




<style>
img.img-fluid {
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 4px;
}
</style>
<p>Is the heyday of the data scientist over? The Harvard Business Review once called it “The Sexiest Job of the 21st Century.”<sup>1</sup> In tech, data scientist roles were often among the best paid.<sup>2</sup> The job also demanded an unusual mix of skills:</p>
<blockquote class="twitter-tweet blockquote">
<p lang="en" dir="ltr">
Data Scientist (n.): Person who is better at statistics than any software engineer and better at software engineering than any statistician.
</p>
— JosH100 (<span class="citation" data-cites="josh_wills">@josh_wills</span>) <a href="https://twitter.com/josh_wills/status/198093512149958656?ref_src=twsrc%5Etfw">May 3, 2012</a>
</blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>In addition to creating a high-barrier to entry, these skills enabled data scientists to build predicitive models, measure casuality and find patterns in data. Of these, predicitive modeling paid best. Companies later peeled that work off into a new title: Machine Learning Engineer (“MLE”).<sup>3</sup></p>
<p>For years, shipping AI meant keeping data scientists and MLEs on the critical path. With LLMs, this stopped being the default. Foundation-model APIs now allow teams to integrate AI independently.</p>
<p>Getting cut out of the loop rattled data scientists and MLEs I know. If the company no longer needs you to ship AI, it is fair to wonder whether the job still has the same upside. The harsher story people tell themselves: unless you are pretraining at a foundation-model lab, you are not where the action is.</p>
<p>I read it the other way. Training models was never most of the job. The bulk of the work is setting up experiments to test how well the AI generalizes to unseen data, debugging stochastic systems, and designing good metrics. Calling an LLM over an API does not make this work go away.</p>
<p>I recently gave a talk titled “The Revenge of the Data Scientist” at <a href="https://pyai.events/">PyAI Conf</a> to make that case with examples rather than assertion alone. Below is an annotated version of that presentation.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_1.png" class="img-fluid"></p>
<section id="the-harness-is-data-science" class="level2">
<h2 class="anchored" data-anchor-id="the-harness-is-data-science">The Harness Is Data Science</h2>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_2.png" class="img-fluid"></p>
<p>OpenAI published a blog post on <a href="https://openai.com/index/harness-engineering/">harness engineering</a> that I recommend reading. They describe how Codex worked on a software project for months, autonomously, with agents developing code bounded by a harness of tests and specifications.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_3.png" class="img-fluid"></p>
<p>One detail in that blog post is easy to miss. The harness includes an observability stack: logs, metrics, and traces exposed to the agent so it can tell when it is going off track. In addition to tests and specifications, there are metrics. That is a key component of the system.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_4.png" class="img-fluid"></p>
<p>Andrej Karpathy’s <a href="https://x.com/karpathy/status/1936185694238064845">auto-research project</a> shows the same pattern: models iteratively optimize against a validation loss metric. Same idea, different harness.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_5.png" class="img-fluid"></p>
<p>What I want to convince you of is that a large portion of the harness is data science.</p>
<p>Let’s take a step back and take stock of where we are.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_6.png" class="img-fluid"></p>
<p>Years ago, practitioners spent hours examining data, checking label alignment, and designing metrics. Today, we build on “vibes,” ask the model if it did a good job, and grab off-the-shelf metric libraries without looking at the data.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_7.png" class="img-fluid"></p>
<p>This shows up most around retrieval and evals. Without a data background, engineers fear what they don’t understand. They claim “RAG is dead” or “evals are dead,” yet build systems that depend on those concepts.</p>
<p>The rest of this post walks through five eval pitfalls I see repeatedly, and what a data scientist would do differently in each case.</p>
<hr>
</section>
<section id="generic-metrics" class="level2">
<h2 class="anchored" data-anchor-id="generic-metrics">Generic Metrics</h2>
<p>The first pitfall is generic metrics.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_9.png" class="img-fluid"></p>
<p>It is tempting to reach for an eval framework and use its metrics off the shelf. The problem: you have no idea what is actually broken. Most teams put up a dashboard with helpfulness scores, coherence scores, hallucination scores. These sound reasonable. They are also generic enough to be useless for diagnosing your application’s failures.</p>
<p>A data scientist would not adopt metrics off the shelf. They would explore the data, explore the traces, ask “what is actually breaking here?”, and figure out the highest-value thing to start measuring. There are infinite things to measure. You have to form hypotheses and iterate.</p>
<p>The best medicine for this pitfall is looking at the data.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_12.png" class="img-fluid"></p>
<p>What does “looking at the data” mean in practice? It means reading traces. Code your own custom trace viewer so you can remove friction and customize the display for your domain’s quirks. Take notes on problems you find. Do error analysis: categorize failures, figure out what to prioritize, decide what to work on.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_13.png" class="img-fluid"></p>
<p>When you look at your data, you end up driving toward application-specific metrics. Off-the-shelf similarity metrics like ROUGE or BLEU rarely fit LLM outputs. The metrics that matter look like “Calendar Scheduling Failure” or “Failure to Escalate To Human.”</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_14.png" class="img-fluid"></p>
<p>If there is one thing to take away from this post: look at the data. How to look at it is a separate question and takes practice. This is the higest ROI activity you can engage in and is often skipped.</p>
<hr>
</section>
<section id="unverified-judges" class="level2">
<h2 class="anchored" data-anchor-id="unverified-judges">Unverified Judges</h2>
<p>The second pitfall is unverified judges. A lot of teams use an LLM as a judge to figure out whether their AI is working. Most of the time, nobody has a good answer to “how do you trust the judge?”</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_15.png" class="img-fluid"></p>
<p>The default: ask an LLM to rate outputs on a scale and use the numbers. A data scientist would treat the judge like a classifier. You have a black box giving you a prediction. How do you trust it? Get human labels, partition the data into train/dev/test, and measure whether the classifier is trustworthy.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_18.png" class="img-fluid"></p>
<p>Source few-shot examples from your training set. Hill-climb your judge’s prompt against a dev set. Keep a test set aside to confirm you haven’t overfit. If you have done machine learning before, this is boring. But people are not doing it. Verifying classifiers has become a lost art in modern AI.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_19.png" class="img-fluid"></p>
<p>Treat your judge like a classifier in how you report results, too. Everywhere I go I see accuracy reported. If a failure mode occurs 5% of the time, accuracy hides the system’s true performance. Use precision and recall.</p>
<hr>
</section>
<section id="bad-experimental-design" class="level2">
<h2 class="anchored" data-anchor-id="bad-experimental-design">Bad Experimental Design</h2>
<p>The third pitfall is experimental design. There are many dimensions to this. Here are two that come up most.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_20.png" class="img-fluid"></p>
<p>The first is constructing test sets. Most teams generate synthetic data by prompting an LLM: “Give me 50 test queries.” They get generic, unrepresentative data. A data scientist would look at real production data first, use hypotheses to determine which dimensions matter, then generate synthetic examples along those dimensions.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_23.png" class="img-fluid"></p>
<p>Ground synthetic data in real logs or traces. Figure out what dimensions to vary. Inject edge cases. Base the synthetic data off real data.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_26.png" class="img-fluid"></p>
<p>The second is metric design. Teams bundle entire rubrics into a single LLM call and default to 1-5 Likert scales. A data scientist would reduce complexity, make each metric actionable, and tie it to a business outcome. Replace subjective scales with binary pass/fail on scoped criteria. Likert scales hide ambiguity and kick the can down the road on hard decisions about system performance.</p>
<hr>
</section>
<section id="bad-data-and-labels" class="level2">
<h2 class="anchored" data-anchor-id="bad-data-and-labels">Bad Data and Labels</h2>
<p>The fourth pitfall is bad data and labels. Data scientists don’t trust the data. They don’t trust the labels. They don’t trust anything. They are skeptical by training. AI engineers at large have not built this muscle yet.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_27.png" class="img-fluid"></p>
<p>When it comes to labeling, most teams make it someone else’s problem. Labeling seems unglamorous, so it gets delegated to the dev team or outsourced. A data scientist would insist that domain experts label the data, stay skeptical of the labels, and look at the data.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_30.png" class="img-fluid"></p>
<p>But labeling matters for a deeper reason than label quality. It is impossible to know what you want unless you look at the data. There is a concept called “criteria drift,” validated in a <a href="https://arxiv.org/abs/2404.12272">paper by Shreya Shankar and colleagues</a>: users need criteria to grade outputs, but grading outputs helps users define their criteria. People don’t know what they want until they see the LLM’s outputs. The labeling process itself surfaces what matters.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_31.png" class="img-fluid"></p>
<p>Data scientists champion this: get domain experts and product managers in front of raw data, not summary scores.</p>
<hr>
</section>
<section id="automating-too-much" class="level2">
<h2 class="anchored" data-anchor-id="automating-too-much">Automating Too Much</h2>
<p>The fifth pitfall is automating too much. All of this is human work. The temptation is to automate it away.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_32.png" class="img-fluid"></p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_33.png" class="img-fluid"></p>
<p>LLMs can help wire things up, write the plumbing, generate boilerplate for evaluations. They cannot look at the data for you, for the exact reason we just discussed: you don’t know what you want until you see the outputs.</p>
<hr>
</section>
<section id="other-pitfalls" class="level2">
<h2 class="anchored" data-anchor-id="other-pitfalls">Other Pitfalls</h2>
<p>We did not have time to cover every pitfall. Here is a speed run through the rest.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_34.png" class="img-fluid"></p>
<p>Misusing similarity scores. Asking the judge vague questions like “is it helpful?” Making annotators read raw JSON. Reporting uncalibrated scores without confidence intervals. Data drift, overfitting, not sampling correctly, dashboards that don’t make sense.</p>
<hr>
</section>
<section id="the-mapping" class="level2">
<h2 class="anchored" data-anchor-id="the-mapping">The Mapping</h2>
<p>If you zoom out, every pitfall above has the same root cause: missing a data science fundamental.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_35.png" class="img-fluid"></p>
<p>Reading traces and categorizing failures is Exploratory Data Analysis. Validating an LLM judge against human labels is Model Evaluation. Building representative test sets from production data is Experimental Design. Getting domain experts to label outputs is Data Collection. Monitoring whether your product works in production is Production ML. None of this is new. The names changed, the work did not.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_36.png" class="img-fluid"></p>
<p>This is a Python conference, so: Python remains the best toolset for looking at your data and dealing with data.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_37.png" class="img-fluid"></p>
<p>I built an <a href="https://github.com/hamelsmu/evals-skills">open-source plugin</a> that goes into more depth. Point it at your eval pipeline and it will tell you what you are doing wrong, or try its best to.</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/slide_38.png" class="img-fluid"></p>
<p>Always look at the data.</p>
<p>If you enjoyed the memes in this talk, there are <a href="../../../notes/llm/evals/memes/index.html#meme-images">many more on my website</a>.</p>
<p>If you want to go deeper on any of these topics, the slides and video are below.</p>
<p><em>Thanks to <a href="https://www.sh-reya.com/">Shreya Shankar</a> and <a href="https://x.com/BEBischof">Bryan Bischof</a> for many conversations that shaped this talk.</em></p>
<hr>
</section>
<section id="video-slides" class="level2">
<h2 class="anchored" data-anchor-id="video-slides">Video &amp; Slides</h2>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/lA4MfpgF91Y" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="https://docs.google.com/presentation/d/1Q7F7cr5PthTmsl6RQCofyBBlH23fh4FlaO_5tbWxbTs/edit?usp=sharing">Link to the slides</a></p>
<hr>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>https://hbr.org/2012/10/data-scientist-the-sexiest-job-of-the-21st-century↩︎</p></li>
<li id="fn2"><p>https://www.forbes.com/sites/louiscolumbus/2018/01/29/data-scientist-is-the-best-job-in-america-according-glassdoors-2018-rankings/↩︎</p></li>
<li id="fn3"><p>https://www.mckinsey.com/about-us/new-at-mckinsey-blog/ai-reinvents-tech-talent-opportunities↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/</guid>
  <pubDate>Thu, 26 Mar 2026 07:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/revenge/images/ABL.jpeg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Evals Skills for Coding Agents</title>
  <dc:creator>Hamel Husain</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-skills/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->




<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-skills/cover-original.png" class="img-fluid"></p>
<p>Today, I’m publishing <a href="https://github.com/hamelsmu/evals-skills">evals-skills</a>, a set of skills for AI product evals<sup>1</sup>. They guard against common mistakes I’ve seen helping 50+ companies and teaching 4,000+ students in our <a href="https://maven.com/parlance-labs/evals">course</a>.</p>
<section id="why-skills-for-evals" class="level2">
<h2 class="anchored" data-anchor-id="why-skills-for-evals">Why Skills for Evals</h2>
<p>Coding agents now instrument applications, run experiments, analyze data, and build interfaces. I’ve been pointing them at evals.</p>
<p>OpenAI’s Harness Engineering <a href="https://openai.com/index/harness-engineering/">article</a> makes the case well. They built a product entirely with Codex agents — three engineers, five months, ~1 million lines of code — and found that <strong>improving the infrastructure around the agent</strong> mattered more than improving the model. The agents queried traces to verify their own work. Documentation tells the agent what to do. Telemetry tells it whether it worked. Evals tell it whether the output is good.</p>
<p>All major eval vendors now ship an MCP server<sup>2</sup>. The tedious parts: instrumenting your app, orchestrating experiments and building annotation tools now fall to coding agents.</p>
<p>But an agent with an eval platform still needs to know what to do with it. Say a support bot tells a customer “your plan includes free returns” when it doesn’t. Another says “I’ve canceled your order” when nobody asked. Both are hallucinations, but one gets a fact wrong and the other makes up a user action. If you lump them together in a generic “hallucination score,” you’ll miss errors.</p>
<p>These skills fill the gaps. They complement the vendor MCP servers: those give your agent access to traces and experiments, these teach it what to do with them.</p>
</section>
<section id="the-skills" class="level2">
<h2 class="anchored" data-anchor-id="the-skills">The Skills</h2>
<p>If you’re new to evals or inheriting an existing eval pipeline, start with <strong>eval-audit</strong>. It inspects your current setup (or lack of one), runs diagnostic checks across six areas, and produces a prioritized list of problems with next steps. Install the skills or give your agent this prompt:</p>
<blockquote class="blockquote">
<p>Install the eval skills plugin from https://github.com/hamelsmu/evals-skills, then run /evals-skills:eval-audit on my eval pipeline. Investigate each diagnostic area using a separate subagent in parallel, then synthesize the findings into a single report. Use other skills in the plugin as recommended by the audit.</p>
</blockquote>
<p>If you’re experienced with evals, you can skip the audit and pick the skill you need:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 35%">
<col style="width: 65%">
</colgroup>
<thead>
<tr class="header">
<th>Skill</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>error-analysis</td>
<td>Read traces, categorize failures, build a vocabulary of what’s broken</td>
</tr>
<tr class="even">
<td>generate-synthetic-data</td>
<td>Create diverse test inputs when real data is sparse</td>
</tr>
<tr class="odd">
<td>write-judge-prompt</td>
<td>Design binary Pass/Fail LLM-as-Judge evaluators</td>
</tr>
<tr class="even">
<td>validate-evaluator</td>
<td>Calibrate judges against human labels using TPR/TNR and bias correction</td>
</tr>
<tr class="odd">
<td>evaluate-rag</td>
<td>Evaluate retrieval and generation quality separately</td>
</tr>
<tr class="even">
<td>build-review-interface</td>
<td>Generate annotation interfaces for human trace review</td>
</tr>
</tbody>
</table>
<p><br></p>
<p>These skills are a starting point and only encode common mistakes that generalize across projects. Skills grounded in your stack, your domain, and your data will outperform them. Start here, then write your own.</p>
<p>👉 The repo is here: <a href="https://github.com/hamelsmu/evals-skills">github.com/hamelsmu/evals-skills</a> 👈</p>
<p>If these skills help you, I’d love to hear from you! You can find me on <a href="https://x.com/hamelhusain">X</a> or email me through my <a href="https://ai.hamel.dev">newsletter</a>.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Not foundation model benchmarks like MMLU or HELM that measure general LLM capabilities. Product evals measure whether <em>your</em> pipeline works on <em>your</em> task with <em>your</em> data. If you aren’t familiar with product-specific AI evals, check out my <a href="../../../blog/posts/evals-faq/index.html">AI Evals FAQ</a>.↩︎</p></li>
<li id="fn2"><p><a href="https://www.braintrust.dev/docs/reference/mcp">Braintrust</a>, <a href="https://github.com/langchain-ai/langsmith-mcp-server">LangSmith</a>, <a href="https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-mcp">Phoenix</a>, <a href="https://truesight.goodeyelabs.com/docs/mcp-integration">Truesight</a>, and others.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>AI</category>
  <category>Evals</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-skills/</guid>
  <pubDate>Mon, 02 Mar 2026 08:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-skills/cover-gradient.png" medium="image" type="image/png" height="81" width="144"/>
</item>
<item>
  <title>Q: What’s your favorite eval vendor?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/whats-your-favorite-eval-vendor.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Eval tools are in an intensely competitive space. It would be futile to compare their features. If I tried to do such an analysis, it would be invalidated in a week! Vendors I encounter the most organically in my work are: <a href="https://www.langchain.com/langsmith" target="_blank">Langsmith</a>, <a href="https://arize.com/" target="_blank">Arize</a> and <a href="https://www.braintrust.dev/" target="_blank">Braintrust</a>.</p>
<p>When I help clients with vendor selection, the decision weighs heavily towards who can offer the best support, as opposed to purely features. This changes depending on size of client, use case, etc. Yes - it’s mainly the human factor that matters, and dare I say, vibes.</p>
<p>I have no favorite vendor. At the core, their features are very similar - and I often build <a href="https://hamel.dev/blog/posts/evals/#q-should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf" target="_blank">custom tools</a> on top of them to fit my needs.</p>
<p>Here is a <a href="../../../blog/posts/eval-tools/">video series</a> that has a live commentary on the relative strengths and weaknesses of the three aforementioned vendors.</p>
<p><a href="../../../blog/posts/evals-faq/#q-seriously-hamel-stop-the-bullshit-whats-your-favorite-eval-vendor" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/whats-your-favorite-eval-vendor.html</guid>
  <pubDate>Wed, 11 Feb 2026 18:35:26 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Why I Stopped Using nbdev</title>
  <dc:creator>Hamel Husain</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/ai-stack/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->




<p>Programmers love to proclaim they’ve found the best tool. Paul Graham called Lisp his “<a href="https://paulgraham.com/avg.html">secret weapon</a>.” DHH described Ruby as “<a href="https://rubyonrails.org/doctrine">a magical glove that just fit my brain perfectly</a>.” Pieter Levels ships million-dollar products with <a href="https://lexfridman.com/pieter-levels-transcript/">vanilla PHP and jQuery</a>.</p>
<p>These declarations aren’t about the languages themselves. They’re about developers finding tools that fit how they think. When the environment clicks, you move fast.</p>
<p>I had that experience with <a href="https://www.fast.ai/posts/2022-07-28-nbdev2.html">nbdev</a>, a development environment for literate programming that I helped build and maintain<sup>1</sup>. I created hundreds of projects with it and was one of its <a href="https://www.youtube.com/watch?v=rX1yGxJijsI">biggest</a> <a href="https://github.blog/developer-skills/programming-languages-and-frameworks/nbdev-a-literate-programming-environment-that-democratizes-software-engineering-best-practices/">proponents</a>.</p>
<p>Today, I no longer use it. AI coding tools changed the trade-offs.</p>
<section id="fighting-the-ai" class="level2">
<h2 class="anchored" data-anchor-id="fighting-the-ai">Fighting the AI</h2>
<p>The beauty of nbdev is its workflow. You write code, documentation and tests in one source of truth: Jupyter notebooks. Afterwards, these notebooks are transpiled into a Python library and documentation website.</p>
<p>This workflow is idiosyncratic. AI coding tools, trained on vast amounts of conventional source code, get confused. They struggle to differentiate between editing the notebook and editing the final source code. It feels like fighting the AI instead of working with it.</p>
<p>I write software to solve problems, not to write code. I want to work in an environment where AI has the highest chance of success. With nbdev, I was swimming upstream.</p>
<p>Some argue that AI tools encourage lazy thinking: that without guardrails, developers skip the hard work of breaking problems into steps. But thinking step-by-step is a human skill. Notebooks don’t force you to write clean code. AI tools don’t force you to think carefully. Discipline comes from the developer, not the environment.</p>
</section>
<section id="tools-dont-matter-as-much-as-i-thought" class="level2">
<h2 class="anchored" data-anchor-id="tools-dont-matter-as-much-as-i-thought">Tools Don’t Matter As Much As I Thought</h2>
<p>A central promise of literate programming is better documentation. By keeping code and docs in one place, you reduce the chance they become stale.</p>
<p>Strangely, many nbdev projects lacked sufficient documentation for my taste. Sometimes, this helped me learn a codebase by <a href="https://fastpages.fast.ai/fastcore/">contributing</a> to the docs. Other times, it was frustrating. This reinforced my belief that good documentation comes from effort, not tooling.</p>
<p>This workflow is also less compelling now. AI can read a codebase without documentation and give you an overview on the fly. It can help maintain documentation that is separate from the code, handling the tedious parts. Keeping code and docs together isn’t the selling point it used to be.</p>
</section>
<section id="collaboration-and-adoption" class="level2">
<h2 class="anchored" data-anchor-id="collaboration-and-adoption">Collaboration and Adoption</h2>
<p>nbdev asks developers to adopt a different system. It does not meet them where they are. Cursor won because it felt familiar and let developers change their habits slowly, rather than demanding a new workflow on day one.</p>
<p>I didn’t worry about collaboration as much before. But collaborating with AI is table stakes. The same impediments that get in the way of collaborating with humans tend to get in the way of collaborating with AI.</p>
<p>Today, developers increasingly span greater scope. Backend people do frontend. PMs create prototypes. Everyone is more polyglot. Idiosyncratic frameworks isolate you from your team to a greater extent than ever before. Idiosyncratic tooling once had a hidden upside: it filtered for a certain kind of contributor. Now I believe it’s more of a liability.</p>
</section>
<section id="what-im-using-now" class="level2">
<h2 class="anchored" data-anchor-id="what-im-using-now">What I’m Using Now</h2>
<p>Because I have invested thousands of hours into nbdev, it’s difficult to admit there are better tools for the outcomes I want. But I must check my ego at the door<sup>2</sup>.</p>
<p>My daily drivers now include Amp, Cursor, and Claude Code. I still enjoy notebooks, but only for data analysis, machine learning, or other exploratory workflows where the iterative, visual nature of notebooks shines.</p>
<p>More importantly, AI has nudged me out of my previous “Python for everything” mindset. I now use different languages for different tasks. For web development, I prefer the Next.js stack. Using a notebook for web development (even with specialized tooling) adds unnecessary complexity for me.</p>
<p>This isn’t arbitrary preference. AI performs best on code with abundant training data for specific domains. <a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/">TypeScript recently overtook both Python and JavaScript on GitHub</a>, driven partly by the fact that typed languages make AI-generated code more reliable in production<sup>3</sup>. Even Jane Street, famous for its OCaml-heavy infrastructure, <a href="https://www.efinancialcareers.com/news/python-ocaml-jane-street-ai">now uses Python</a> for machine learning and data work.</p>
</section>
<section id="a-place-for-joy" class="level2">
<h2 class="anchored" data-anchor-id="a-place-for-joy">A Place for Joy</h2>
<p>None of this means idiosyncratic tools are worthless. Lisp, Haskell, and APL each teach you something different about computing. Joy is a valid reason to choose a language, even with less AI support. It’s just not my focus right now. My joy resides in solving problems, and I want tools that maximize my leverage. For that, conventional wins.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>I <a href="https://github.blog/developer-skills/programming-languages-and-frameworks/nbdev-a-literate-programming-environment-that-democratizes-software-engineering-best-practices/">joined the nbdev project in 2020</a> while at GitHub. That same year I built <a href="https://www.youtube.com/watch?v=cduXZwZaBbM">fastpages</a>, a notebook blogging system that informed nbdev’s documentation approach. Along the way I contributed to tools like <a href="https://github.blog/developer-skills/programming-languages-and-frameworks/learn-about-ghapi-a-new-third-party-python-client-for-the-github-api/">ghapi</a> and <a href="https://fastpages.fast.ai/fastcore/">fastcore</a>. In 2022, I helped lead a <a href="https://www.fast.ai/posts/2022-07-28-nbdev2.html">complete rewrite</a> of nbdev. I discussed the philosophy behind this work on the <a href="https://vanishinggradients.fireside.fm/9">Vanishing Gradients Podcast</a> and at <a href="https://www.youtube.com/watch?v=rX1yGxJijsI">Data Council</a>. I was briefly interested in commercializing nbdev, but <a href="../../../blog/posts/nbdev/">decided not to pursue it</a>.↩︎</p></li>
<li id="fn2"><p>Other top nbdev maintainers and power users like <a href="https://github.com/sgugger">Sylvain Gugger</a>, <a href="https://github.com/seeM">Wasim Lorgat</a>, <a href="https://github.com/isaac-flath">Isaac Flath</a>, <a href="https://github.com/muellerzr">Zach Mueller</a>, and <a href="https://ohmeow.com/">Wayde Gilliam</a> have made similar moves.↩︎</p></li>
<li id="fn3"><p>Research shows 94% of LLM compilation errors in TypeScript come from type violations, suggesting type systems can guide better code generation. See <a href="https://arxiv.org/abs/2504.09246">Mündler et al., 2025</a>.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>AI</category>
  <category>nbdev</category>
  <category>Tools</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/ai-stack/</guid>
  <pubDate>Sun, 18 Jan 2026 08:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/ai-stack/nbdev2-cover.png" medium="image" type="image/png" height="81" width="144"/>
</item>
<item>
  <title>LLM Evals: Everything You Need to Know</title>
  <dc:creator>Hamel Husain</dc:creator>
  <dc:creator>Shreya Shankar</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>This document curates the most common questions Shreya and I received while <a href="https://bit.ly/evals-ai" target="_blank">teaching</a> 700+ engineers &amp; PMs AI Evals. <em>Warning: These are sharp opinions about what works in most cases. They are not universal truths. Use your judgment.</em></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>
<section id="listen-to-the-audio-version-of-this-faq" class="level1">
<h1>Listen to the audio version of this FAQ</h1>
<p>If you prefer to listen to the audio version (narrated by AI), you can play it <a href="https://soundcloud.com/hamel-husain/llm-evals-faq">here</a>.</p>
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/2138083206&amp;color=%23447099&amp;auto_play=false&amp;hide_related=true&amp;show_comments=false&amp;show_user=true&amp;show_reposts=false&amp;show_teaser=true">
</iframe>
<div style="font-size: 10px; color: #cccccc;line-break: anywhere;word-break: normal;overflow: hidden;white-space: nowrap;text-overflow: ellipsis; font-family: Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif;font-weight: 100;">
<a href="https://soundcloud.com/hamel-husain" title="Hamel Husain" target="_blank" style="color: #cccccc; text-decoration: none;">Hamel Husain</a> · <a href="https://soundcloud.com/hamel-husain/llm-evals-faq" title="LLM Evals FAQ" target="_blank" style="color: #cccccc; text-decoration: none;">LLM Evals FAQ</a>
</div>
</section>
<section id="getting-started-fundamentals" class="level1">
<h1>Getting Started &amp; Fundamentals</h1>
<section id="q-what-are-llm-evals" class="level2">
<h2 class="anchored" data-anchor-id="q-what-are-llm-evals">Q: What are LLM Evals?</h2>
<p>If you are completely new to product-specific LLM evals (not foundation model benchmarks), see these posts: <a href="../../../blog/posts/evals/index.html" target="_blank">part 1</a>, <a href="../../../blog/posts/llm-judge/index.html" target="_blank">part 2</a> and <a href="../../../blog/posts/field-guide/index.html" target="_blank">part 3</a>. Otherwise, keep reading.</p>
<div class="grid">
<div class="g-col-4">
<p><a href="https://hamel.dev/evals" target="_blank"><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals/images/diagram-cover.png" class="img-fluid"></a></p>
<p><a href="https://hamel.dev/evals" target="_blank"><strong>Your AI Product Needs Eval (Evaluation Systems)</strong></a></p>
<p><strong>Contents:</strong></p>
<ol type="1">
<li>Motivation</li>
<li>Iterating Quickly == Success<br>
</li>
<li>Case Study: Lucy, A Real Estate AI Assistant</li>
<li>The Types Of Evaluation
<ol type="a">
<li>Level 1: Unit Tests</li>
<li>Level 2: Human &amp; Model Eval</li>
<li>Level 3: A/B Testing</li>
<li>Evaluating RAG</li>
</ol></li>
<li>Eval Systems Unlock Superpowers For Free
<ol type="a">
<li>Fine-Tuning</li>
<li>Data Synthesis &amp; Curation</li>
<li>Debugging</li>
</ol></li>
</ol>
</div>
<div class="g-col-4">
<p><a href="https://hamel.dev/llm-judge/" target="_blank"><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/llm-judge/images/cover_img.png" class="img-fluid"></a></p>
<p><a href="https://hamel.dev/llm-judge/" target="_blank"><strong>Creating a LLM-as-a-Judge That Drives Business Results</strong></a></p>
<p><strong>Contents:</strong></p>
<ol type="1">
<li>The Problem: AI Teams Are Drowning in Data</li>
<li>Step 1: Find The Principal Domain Expert</li>
<li>Step 2: Create a Dataset</li>
<li>Step 3: Direct The Domain Expert to Make Pass/Fail Judgments with Critiques</li>
<li>Step 4: Fix Errors</li>
<li>Step 5: Build Your LLM as A Judge, Iteratively</li>
<li>Step 6: Perform Error Analysis</li>
<li>Step 7: Create More Specialized LLM Judges (if needed)</li>
<li>Recap of Critique Shadowing</li>
<li>Resources</li>
</ol>
</div>
<div class="g-col-4">
<p><a href="https://hamel.dev/field-guide" target="_blank"><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/field-guide/images/field_guide_2.png" class="img-fluid"></a></p>
<p><a href="https://hamel.dev/field-guide" target="_blank"><strong>A Field Guide to Rapidly Improving AI Products</strong></a></p>
<p><strong>Contents:</strong></p>
<ol type="1">
<li>How error analysis consistently reveals the highest-ROI improvements</li>
<li>Why a simple data viewer is your most important AI investment</li>
<li>How to empower domain experts (not just engineers) to improve your AI</li>
<li>Why synthetic data is more effective than you think</li>
<li>How to maintain trust in your evaluation system</li>
<li>Why your AI roadmap should count experiments, not features</li>
</ol>
</div>
</div>
<p><a href="../../../blog/posts/evals-faq/what-are-llm-evals.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-what-is-a-trace" class="level2">
<h2 class="anchored" data-anchor-id="q-what-is-a-trace">Q: What is a trace?</h2>
<p>A trace is the complete record of all actions, messages, tool calls, and data retrievals from a single initial user query through to the final response. It includes every step across all agents, tools, and system components in a session: multiple user messages, assistant responses, retrieved documents, and intermediate tool interactions.</p>
<p><strong>Note on terminology:</strong> Different observability vendors use varying definitions of traces and spans. <a href="https://mlops.systems/posts/2025-06-04-instrumenting-an-agentic-app-with-arize-phoenix-and-litellm.html#llm-tracing-tools-naming-conventions-june-2025">Alex Strick van Linschoten’s analysis</a> highlights these differences (screenshot below):</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/alex.jpeg" class="img-fluid figure-img"></p>
<figcaption>Vendor differences in trace definitions as of 2025-07-02</figcaption>
</figure>
</div>
<p><a href="../../../blog/posts/evals-faq/what-is-a-trace.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-whats-a-minimum-viable-evaluation-setup" class="level2">
<h2 class="anchored" data-anchor-id="q-whats-a-minimum-viable-evaluation-setup">Q: What’s a minimum viable evaluation setup?</h2>
<p>Start with error analysis, not infrastructure. Spend 30 minutes manually reviewing 20-50 LLM outputs whenever you make significant changes. Use one domain expert who understands your users as your quality decision maker (a “benevolent dictator”).</p>
<p><strong>Use a notebook</strong> to review traces and analyze data, or build your own custom annotation interface with an AI coding assistant like Claude or Codex. Either way, you can write arbitrary code, visualize data, and iterate quickly. The <a href="https://youtu.be/aqKUwPKBkB0?si=5KDmMQnRzO_Ce9xH" target="_blank">video</a> below shows a simple annotation interface built inside a notebook.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/aqKUwPKBkB0" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="../../../blog/posts/evals-faq/whats-a-minimum-viable-evaluation-setup.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-much-of-my-development-budget-should-i-allocate-to-evals" class="level2">
<h2 class="anchored" data-anchor-id="q-how-much-of-my-development-budget-should-i-allocate-to-evals">Q: How much of my development budget should I allocate to evals?</h2>
<p>It’s important to recognize that evaluation is part of the development process rather than a distinct line item, similar to how debugging is part of software development.</p>
<p>You should always be doing <a href="https://www.youtube.com/watch?v=qH1dZ8JLLdU" target="_blank">error analysis</a>. When you discover issues through error analysis, many will be straightforward bugs you’ll fix immediately. These fixes don’t require separate evaluation infrastructure as they’re just part of development.</p>
<p>The decision to build automated evaluators comes down to cost-benefit analysis. If you can catch an error with a simple assertion or regex check, the cost is minimal and probably worth it. But if you need to align an LLM-as-judge evaluator, consider whether the failure mode warrants that investment.</p>
<p>In the projects we’ve worked on, <strong>we’ve spent 60-80% of our development time on error analysis and evaluation</strong>. Expect most of your effort to go toward understanding failures (i.e.&nbsp;looking at data) rather than building automated checks.</p>
<p>Be <a href="https://ai-execs.com/2_intro.html#a-case-study-in-misleading-ai-advice" target="_blank">wary of optimizing for high eval pass rates</a>. If you’re passing 100% of your evals, you’re likely not challenging your system enough. A 70% pass rate might indicate a more meaningful evaluation that’s actually stress-testing your application. Focus on evals that help you catch real issues, not ones that make your metrics look good.</p>
<p><a href="../../../blog/posts/evals-faq/how-much-of-my-development-budget-should-i-allocate-to-evals.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-will-todays-evaluation-methods-still-be-relevant-in-5-10-years-given-how-fast-ai-is-changing" class="level2">
<h2 class="anchored" data-anchor-id="q-will-todays-evaluation-methods-still-be-relevant-in-5-10-years-given-how-fast-ai-is-changing">Q: Will today’s evaluation methods still be relevant in 5-10 years given how fast AI is changing?</h2>
<p>Yes. Even with perfect models, you still need to verify they’re solving the right problem. The need for systematic error analysis, domain-specific testing, and monitoring will still be important.</p>
<p>Today’s prompt engineering tricks might become obsolete, but you’ll still need to understand failure modes. Additionally, a LLM cannot read your mind, and <a href="https://arxiv.org/abs/2404.12272" target="_blank">research shows</a> that people need to observe the LLM’s behavior in order to properly externalize their requirements.</p>
<p>For deeper perspective on this debate, see these two viewpoints: <a href="https://m.youtube.com/watch?si=qknrtQeITqJ7VsJH&amp;v=4dUFIRj-BWo&amp;feature=youtu.be" target="_blank">“The model is the product”</a> versus <a href="https://www.youtube.com/watch?v=EEw2PpL-_NM" target="_blank">“The model is NOT the product”</a>.</p>
<p><strong>“The model is the product”:</strong> </p><div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/4dUFIRj-BWo" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div><p></p>
<p><strong>“The model is NOT the product”:</strong> </p><div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/EEw2PpL-_NM" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div><p></p>
<p><a href="../../../blog/posts/evals-faq/will-these-evaluation-methods-still-be-relevant-in-5-10-years-given-how-fast-ai-is-changing.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-i-make-the-case-for-investing-in-evaluations-to-my-team" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-make-the-case-for-investing-in-evaluations-to-my-team">Q: How do I make the case for investing in evaluations to my team?</h2>
<p>Don’t try to sell your team on “evals”. Instead, show them what you find when you look at the data.</p>
<p>Start by doing the error analysis yourself. Look at 50 to 100 real user conversations and find the most common ways the product is failing. Use these findings to tell a story with data.</p>
<p>Present your team with:</p>
<ul>
<li>A list of the top failure modes you discovered.</li>
<li>Metrics showing how often high-impact errors are happening.</li>
<li>Surprising ways that users are interacting with the product.</li>
<li>Reports on the bugs you found and fixed, framed as “prevented production issues”.</li>
</ul>
<p>This approach builds trust. Don’t just show dashboards and metrics; tell the story of what you’re finding in the data. By narrating your findings, you teach the team what you’re learning, providing immediate value. When you fix an issue, show how the error rate for that specific problem went down. Soon, your team will see the progress and ask how you’re doing it. Let results instead of methods lead the conversation.</p>
<p>This is similar to classic machine learning projects, where outcomes are speculative and progress is bounded by <a href="https://hamel.dev/blog/posts/field-guide/#your-ai-roadmap-should-count-experiments-not-features" target="_blank">iterating on experiments</a>. In this situation, it’s important that you share the learnings from each experiment to show progress and encourage investment.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-make-the-case-for-investing-in-evaluations-to-my-team.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="error-analysis-data-collection" class="level1">
<h1>Error Analysis &amp; Data Collection</h1>
<section id="q-why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed" class="level2">
<h2 class="anchored" data-anchor-id="q-why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed">Q: Why is "error analysis" so important in LLM evals, and how is it performed?</h2>
<p>Error analysis is <strong>the most important activity in evals</strong>. Error analysis helps you decide what evals to write in the first place. It allows you to identify failure modes unique to your application and data. The process involves:</p>
<section id="creating-a-dataset" class="level3">
<h3 class="anchored" data-anchor-id="creating-a-dataset">1. Creating a Dataset</h3>
<p>Gathering representative traces of user interactions with the LLM. If you do not have any data, you can generate synthetic data to get started.</p>
</section>
<section id="open-coding" class="level3">
<h3 class="anchored" data-anchor-id="open-coding">2. Open Coding</h3>
<p>Human annotator(s) (ideally a benevolent dictator) review and write open-ended notes about traces, noting any issues. This process is akin to “journaling” and is adapted from qualitative research methodologies. When beginning, it is recommended to focus on noting the first failure observed in a trace, as upstream errors can cause downstream issues, though you can also tag all independent failures if feasible. A <a href="https://hamel.dev/blog/posts/llm-judge/#step-1-find-the-principal-domain-expert" target="_blank">domain expert</a> should be performing this step.</p>
</section>
<section id="axial-coding" class="level3">
<h3 class="anchored" data-anchor-id="axial-coding">3. Axial Coding</h3>
<p>Categorize the open-ended notes into a “failure taxonomy.”. In other words, group similar failures into distinct categories. This is the most important step. At the end, count the number of failures in each category. You can use a LLM to help with this step.</p>
</section>
<section id="iterative-refinement" class="level3">
<h3 class="anchored" data-anchor-id="iterative-refinement">4. Iterative Refinement</h3>
<p>Keep iterating on more traces until you reach <a href="https://delvetool.com/blog/theoreticalsaturation" target="_blank">theoretical saturation</a>, meaning new traces do not seem to reveal new failure modes or information to you. As a rule of thumb, you should aim to review at least 100 traces. My rough heuristic is if ~20 traces don’t turn up a new category, you can stop (but review at least 100 to start). Remember, the goal is to prioritize the failures that actually happen the most, not catch every possible failure (evals are not free, so we need to be pragmatic).</p>
<p>You should frequently revisit this process. There are advanced ways to <a href="how-can-i-efficiently-sample-production-traces-for-review.html" target="_blank">sample data more efficiently</a>, like clustering, sorting by user feedback, and sorting by high probability failure patterns. Over time, you’ll develop a “nose” for where to look for failures in your data.</p>
<p>Do not skip error analysis. It ensures that the evaluation metrics you develop are supported by real application behaviors instead of counter-productive generic metrics (which most platforms nudge you to use). For examples of how error analysis can be helpful, see <a href="https://www.youtube.com/watch?v=e2i6JbU2R-s" target="_blank">this video</a>, or this <a href="https://hamel.dev/blog/posts/field-guide/" target="_blank">blog post</a>.</p>
<p>Here is a visualization of the error analysis process by one of our students, <a href="https://www.linkedin.com/in/pawel-huryn/" target="_blank">Pawel Huryn</a> - including how it fits into the overall evaluation process:</p>
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/pawel-error-analysis.png" class="img-fluid"></p>
<p><a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-how-do-i-surface-problematic-traces-for-review-beyond-user-feedback" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-surface-problematic-traces-for-review-beyond-user-feedback">Q: How do I surface problematic traces for review beyond user feedback?</h2>
<p>While user feedback is a good way to narrow in on problematic traces, other methods are also useful. Here are three complementary approaches:</p>
<section id="start-with-random-sampling" class="level3">
<h3 class="anchored" data-anchor-id="start-with-random-sampling">Start with random sampling</h3>
<p>The simplest approach is reviewing a random sample of traces. If you find few issues, escalate to stress testing: create queries that deliberately test your prompt constraints to see if the AI follows your rules.</p>
</section>
<section id="use-evals-for-initial-screening" class="level3">
<h3 class="anchored" data-anchor-id="use-evals-for-initial-screening">Use evals for initial screening</h3>
<p>Use existing evals to find problematic traces and potential issues. Once you’ve identified these, you can proceed with the typical evaluation process starting with error analysis.</p>
</section>
<section id="leverage-efficient-sampling-strategies" class="level3">
<h3 class="anchored" data-anchor-id="leverage-efficient-sampling-strategies">Leverage efficient sampling strategies</h3>
<p>For more sophisticated trace discovery, use outlier detection, metric-based sorting, and stratified sampling to find interesting traces. Generic metrics can serve as exploration signals to identify traces worth reviewing, even if they don’t directly measure quality.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-surface-problematic-traces-for-review-beyond-user-feedback.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-how-often-should-i-re-run-error-analysis-on-my-production-system" class="level2">
<h2 class="anchored" data-anchor-id="q-how-often-should-i-re-run-error-analysis-on-my-production-system">Q: How often should I re-run error analysis on my production system?</h2>
<p>Re-run error analysis when making significant changes: new features, prompt updates, model switches, or major bug fixes. A useful heuristic is to set a goal for reviewing <em>at least</em> 100+ fresh traces each review cycle. Typical review cycles we’ve seen range from 2-4 weeks. See this FAQ on how to sample traces effectively.</p>
<p>Between major analyses, review 10-20 traces weekly, focusing on outliers: unusually long conversations, sessions with multiple retries, or traces flagged by automated monitoring. Adjust frequency based on system stability and usage growth. New systems need weekly analysis until failure patterns stabilize. Mature systems might need only monthly analysis unless usage patterns change. Always analyze after incidents, user complaint spikes, or metric drift. Scaling usage introduces new edge cases.</p>
<p><a href="../../../blog/posts/evals-faq/how-often-should-i-re-run-error-analysis-on-my-production-system.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-what-is-the-best-approach-for-generating-synthetic-data" class="level2">
<h2 class="anchored" data-anchor-id="q-what-is-the-best-approach-for-generating-synthetic-data">Q: What is the best approach for generating synthetic data?</h2>
<p>A common mistake is prompting an LLM to <code>"give me test queries"</code> without structure, resulting in generic, repetitive outputs. A structured approach using dimensions produces far better synthetic data for testing LLM applications.</p>
<p><strong>Start by defining dimensions</strong>: categories that describe different aspects of user queries. Each dimension captures one type of variation in user behavior. For example:</p>
<ul>
<li>For a recipe app, dimensions might include Dietary Restriction (<em>vegan</em>, <em>gluten-free</em>, <em>none</em>), Cuisine Type (<em>Italian</em>, <em>Asian</em>, <em>comfort food</em>), and Query Complexity (<em>simple request</em>, <em>multi-step</em>, <em>edge case</em>).</li>
<li>For a customer support bot, dimensions could be Issue Type (<em>billing</em>, <em>technical</em>, <em>general</em>), Customer Mood (<em>frustrated</em>, <em>neutral</em>, <em>happy</em>), and Prior Context (<em>new issue</em>, <em>follow-up</em>, <em>resolved</em>).</li>
</ul>
<p><strong>Start with failure hypotheses</strong>. If you lack intuition about failure modes, use your application extensively or recruit friends to use it. Then choose dimensions targeting those likely failures.</p>
<p><strong>Create tuples manually first</strong>: Write 20 tuples by hand—specific combinations selecting one value from each dimension. Example: (<em>Vegan</em>, <em>Italian</em>, <em>Multi-step</em>). This manual work helps you understand your problem space.</p>
<p><strong>Scale with two-step generation</strong>:</p>
<ol type="1">
<li><strong>Generate structured tuples</strong>: Have the LLM create more combinations like (<em>Gluten-free</em>, <em>Asian</em>, <em>Simple</em>)</li>
<li><strong>Convert tuples to queries</strong>: In a separate prompt, transform each tuple into natural language</li>
</ol>
<p>This separation avoids repetitive phrasing. The (<em>Vegan</em>, <em>Italian</em>, <em>Multi-step</em>) tuple becomes: <code>"I need a dairy-free lasagna recipe that I can prep the day before."</code></p>
<section id="generation-approaches" class="level3">
<h3 class="anchored" data-anchor-id="generation-approaches">Generation approaches</h3>
<p>You can generate tuples two ways:</p>
<p><strong>Cross product then filter</strong>: Generate all dimension combinations, then filter with an LLM. Guarantees coverage including edge cases. Use when most combinations are valid.</p>
<p><strong>Direct LLM generation</strong>: Ask the LLM to generate tuples directly. More realistic but tends toward generic outputs and misses rare scenarios. Use when many dimension combinations are invalid.</p>
<p><strong>Fix obvious problems first</strong>: Don’t generate synthetic data for issues you can fix immediately. If your prompt doesn’t mention dietary restrictions, fix the prompt rather than generating specialized test queries.</p>
<p>After iterating on your tuples and prompts, <strong>run these synthetic queries through your actual system to capture full traces</strong>. Sample 100 traces for error analysis. This number provides enough traces to manually review and identify failure patterns without being overwhelming.</p>
<p><a href="../../../blog/posts/evals-faq/what-is-the-best-approach-for-generating-synthetic-data.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-are-there-scenarios-where-synthetic-data-may-not-be-reliable" class="level2">
<h2 class="anchored" data-anchor-id="q-are-there-scenarios-where-synthetic-data-may-not-be-reliable">Q: Are there scenarios where synthetic data may not be reliable?</h2>
<p>Yes: synthetic data can mislead or mask issues. For guidance on generating synthetic data when appropriate, see What is the best approach for generating synthetic data?</p>
<p>Common scenarios where synthetic data fails:</p>
<ol type="1">
<li><p><strong>Complex domain-specific content</strong>: LLMs often miss the structure, nuance, or quirks of specialized documents (e.g., legal filings, medical records, technical forms). Without real examples, critical edge cases are missed.</p></li>
<li><p><strong>Low-resource languages or dialects</strong>: For low-resource languages or dialects, LLM-generated samples are often unrealistic. Evaluations based on them won’t reflect actual performance.</p></li>
<li><p><strong>When validation is impossible</strong>: If you can’t verify synthetic sample realism (due to domain complexity or lack of ground truth), real data is important for accurate evaluation.</p></li>
<li><p><strong>High-stakes domains</strong>: In high-stakes domains (medicine, law, emergency response), synthetic data often lacks subtlety and edge cases. Errors here have serious consequences, and manual validation is difficult.</p></li>
<li><p><strong>Underrepresented user groups</strong>: For underrepresented user groups, LLMs may misrepresent context, values, or challenges. Synthetic data can reinforce biases in the training data of the LLM.</p></li>
</ol>
<p><a href="../../../blog/posts/evals-faq/are-there-scenarios-where-synthetic-data-may-not-be-reliable.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries">Q: How do I approach evaluation when my system handles diverse user queries?</h2>
<blockquote class="blockquote">
<p>Complex applications often support vastly different query patterns—from “What’s the return policy?” to “Compare pricing trends across regions for products matching these criteria.” Each query type exercises different system capabilities, leading to confusion on how to design eval criteria.</p>
</blockquote>
<p><strong><em><a href="https://youtu.be/e2i6JbU2R-s?si=8p5XVxbBiioz69Xc" target="_blank">Error Analysis</a> is all you need.</em></strong> Your evaluation strategy should emerge from observed failure patterns (e.g.&nbsp;error analysis), not predetermined query classifications. Rather than creating a massive evaluation matrix covering every query type you can imagine, let your system’s actual behavior guide where you invest evaluation effort.</p>
<p>During error analysis, you’ll likely discover that certain query categories share failure patterns. For instance, all queries requiring temporal reasoning might struggle regardless of whether they’re simple lookups or complex aggregations. Similarly, queries that need to combine information from multiple sources might fail in consistent ways. These patterns discovered through error analysis should drive your evaluation priorities. It could be that query category is a fine way to group failures, but you don’t know that until you’ve analyzed your data.</p>
<p>To see an example of basic error analysis in action, <a href="https://youtu.be/e2i6JbU2R-s?si=8p5XVxbBiioz69Xc" target="_blank">see this video</a>.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/e2i6JbU2R-s" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="../../../blog/posts/evals-faq/how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-can-i-efficiently-sample-production-traces-for-review" class="level2">
<h2 class="anchored" data-anchor-id="q-how-can-i-efficiently-sample-production-traces-for-review">Q: How can I efficiently sample production traces for review?</h2>
<p>It can be cumbersome to review traces randomly, especially when most traces don’t have an error. These sampling strategies help you find traces more likely to reveal problems:</p>
<ul>
<li><strong>Outlier detection:</strong> Sort by any metric (response length, latency, tool calls) and review extremes.</li>
<li><strong>User feedback signals:</strong> Prioritize traces with negative feedback, support tickets, or escalations.</li>
<li><strong>Metric-based sorting:</strong> Generic metrics can serve as exploration signals to find interesting traces. Review both high and low scores and treat them as exploration clues. Based on what you learn, you can build custom evaluators for the failure modes you find.</li>
<li><strong>Stratified sampling:</strong> Group traces by key dimensions (user type, feature, query category) and sample from each group.</li>
<li><strong>Embedding clustering:</strong> Generate embeddings of queries and cluster them to reveal natural groupings. Sample proportionally from each cluster, but oversample small clusters for edge cases. There’s no right answer for clustering—it’s an exploration technique to surface patterns you might miss manually.</li>
</ul>
<p>As you get more sophisticated with how you sample, you can incorporate these tactics into the design of your annotation tools.</p>
<p><a href="../../../blog/posts/evals-faq/how-can-i-efficiently-sample-production-traces-for-review.html" class="faq-individual-link">↗ Focus view</a></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>
</section>
</section>
<section id="evaluation-design-methodology" class="level1">
<h1>Evaluation Design &amp; Methodology</h1>
<section id="q-why-do-you-recommend-binary-passfail-evaluations-instead-of-1-5-ratings-likert-scales" class="level2">
<h2 class="anchored" data-anchor-id="q-why-do-you-recommend-binary-passfail-evaluations-instead-of-1-5-ratings-likert-scales">Q: Why do you recommend binary (pass/fail) evaluations instead of 1-5 ratings (Likert scales)?</h2>
<blockquote class="blockquote">
<p>Engineers often believe that Likert scales (1-5 ratings) provide more information than binary evaluations, allowing them to track gradual improvements. However, this added complexity often creates more problems than it solves in practice.</p>
</blockquote>
<p>Binary evaluations force clearer thinking and more consistent labeling. Likert scales introduce significant challenges: the difference between adjacent points (like 3 vs 4) is subjective and inconsistent across annotators, detecting statistical differences requires larger sample sizes, and annotators often default to middle values to avoid making hard decisions.</p>
<p>Having binary options forces people to make a decision rather than hiding uncertainty in middle values. Binary decisions are also faster to make during error analysis - you don’t waste time debating whether something is a 3 or 4.</p>
<p>For tracking gradual improvements, consider measuring specific sub-components with their own binary checks rather than using a scale. For example, instead of rating factual accuracy 1-5, you could track “4 out of 5 expected facts included” as separate binary checks. This preserves the ability to measure progress while maintaining clear, objective criteria.</p>
<p>Start with binary labels to understand what ‘bad’ looks like. Numeric labels are advanced and usually not necessary.</p>
<p><a href="../../../blog/posts/evals-faq/why-do-you-recommend-binary-passfail-evaluations-instead-of-1-5-ratings-likert-scales.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-should-i-practice-eval-driven-development" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-practice-eval-driven-development">Q: Should I practice eval-driven development?</h2>
<p><strong>Generally no.</strong> Eval-driven development (writing evaluators before implementing features) sounds appealing but creates more problems than it solves. Unlike traditional software where failure modes are predictable, LLMs have infinite surface area for potential failures. You can’t anticipate what will break.</p>
<p>A better approach is to start with error analysis. Write evaluators for errors you discover, not errors you imagine. This avoids getting blocked on what to evaluate and prevents wasted effort on metrics that have no impact on actual system quality.</p>
<p><strong>Exception:</strong> Eval-driven development may work for specific constraints where you know exactly what success looks like. If adding “never mention competitors,” writing that evaluator early may be acceptable.</p>
<p>Most importantly, always do a cost-benefit analysis before implementing an eval. Ask whether the failure mode justifies the investment. Error analysis reveals which failures actually matter for your users.</p>
<p><a href="../../../blog/posts/evals-faq/should-i-practice-eval-driven-development.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-should-i-build-automated-evaluators-for-every-failure-mode-i-find" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-build-automated-evaluators-for-every-failure-mode-i-find">Q: Should I build automated evaluators for every failure mode I find?</h2>
<p>Focus automated evaluators on failures that persist after fixing your prompts. Many teams discover their LLM doesn’t meet preferences they never actually specified - like wanting short responses, specific formatting, or step-by-step reasoning. Fix these obvious gaps first before building complex evaluation infrastructure.</p>
<p>Consider the cost hierarchy of different evaluator types. Simple assertions and reference-based checks (comparing against known correct answers) are cheap to build and maintain. LLM-as-Judge evaluators require 100+ labeled examples, ongoing weekly maintenance, and coordination between developers, PMs, and domain experts. This cost difference should shape your evaluation strategy.</p>
<p>Only build expensive evaluators for problems you’ll iterate on repeatedly. Since LLM-as-Judge comes with significant overhead, save it for persistent generalization failures - not issues you can fix trivially. Start with cheap code-based checks where possible: regex patterns, structural validation, or execution tests. Reserve complex evaluation for subjective qualities that can’t be captured by simple rules.</p>
<p><a href="../../../blog/posts/evals-faq/should-i-build-automated-evaluators-for-every-failure-mode-i-find.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-should-i-use-ready-to-use-evaluation-metrics" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-use-ready-to-use-evaluation-metrics">Q: Should I use "ready-to-use" evaluation metrics?</h2>
<p><strong>No.&nbsp;Generic evaluations waste time and create false confidence.</strong> (Unless you’re using them for exploration).</p>
<p>One instructor noted:</p>
<blockquote class="blockquote">
<p>“All you get from using these prefab evals is you don’t know what they actually do and in the best case they waste your time and in the worst case they create an illusion of confidence that is unjustified.”<sup>1</sup></p>
</blockquote>
<p>Generic evaluation metrics are everywhere. Eval libraries contain scores like helpfulness, coherence, quality, etc. promising easy evaluation. These metrics measure abstract qualities that may not matter for your use case. Good scores on them don’t mean your system works.</p>
<p>Instead, conduct error analysis to understand failures. Define binary failure modes based on real problems. Create custom evaluators for those failures and validate them against human judgment. Essentially, the entire evals process.</p>
<p>Experienced practitioners may still use these metrics, just not how you’d expect. As Picasso said: “Learn the rules like a pro, so you can break them like an artist.” Once you understand why generic metrics fail as evaluations, you can repurpose them as exploration tools to find interesting traces (explained in the next FAQ).</p>
<p><a href="../../../blog/posts/evals-faq/should-i-use-ready-to-use-evaluation-metrics.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-are-similarity-metrics-bertscore-rouge-etc.-useful-for-evaluating-llm-outputs" class="level2">
<h2 class="anchored" data-anchor-id="q-are-similarity-metrics-bertscore-rouge-etc.-useful-for-evaluating-llm-outputs">Q: Are similarity metrics (BERTScore, ROUGE, etc.) useful for evaluating LLM outputs?</h2>
<p>Generic metrics like BERTScore, ROUGE, cosine similarity, etc. are not useful for evaluating LLM outputs in most AI applications. Instead, we recommend using error analysis to identify metrics specific to your application’s behavior. We recommend designing binary pass/fail.) evals (using LLM-as-judge) or code-based assertions.</p>
<p>As an example, consider a real estate CRM assistant. Suggesting showings that aren’t available (can be tested with an assertion) or confusing client personas (can be tested with a LLM-as-judge) is problematic . Generic metrics like similarity or verbosity won’t catch this. A relevant quote from the course:</p>
<blockquote class="blockquote">
<p>“The abuse of generic metrics is endemic. Many eval vendors promote off the shelf metrics, which ensnare engineers into superfluous tasks.”</p>
</blockquote>
<p>Similarity metrics aren’t always useless. They have utility in domains like search and recommendation (and therefore can be useful for optimizing and debugging retrieval for RAG). For example, cosine similarity between embeddings can measure semantic closeness in retrieval systems, and average pairwise similarity can assess output diversity (where lower similarity indicates higher diversity).</p>
<p><a href="../../../blog/posts/evals-faq/are-similarity-metrics-bertscore-rouge-etc-useful-for-evaluating-llm-outputs.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-can-i-use-the-same-model-for-both-the-main-task-and-evaluation" class="level2">
<h2 class="anchored" data-anchor-id="q-can-i-use-the-same-model-for-both-the-main-task-and-evaluation">Q: Can I use the same model for both the main task and evaluation?</h2>
<p>For LLM-as-Judge selection, using the same model is usually fine because the judge is doing a different task than your main LLM pipeline. While <a href="https://arxiv.org/pdf/2508.06709">research has shown</a> that models can exhibit bias when evaluating their own outputs, what ultimately matters is how well your judge aligns with human judgments. The judges we recommend building do scoped binary classification tasks. We’ve found that iterative alignment with human labels is usually achievable on this constrained task.</p>
<p>Focus on achieving high True Positive Rate (TPR) and True Negative Rate (TNR) with your judge on a held out labeled test set. If you struggle to achieve good alignment with human scores, then consider trying a different model. However onboarding new model providers may involve non-trivial effort in some organizations, which is why we don’t advocate for using different models by default unless there’s a specific alignment issue.</p>
<p>When selecting judge models, start with the most capable models available to establish strong alignment with human judgments. You can optimize for cost later once you’ve established reliable evaluation criteria.</p>
<p><a href="../../../blog/posts/evals-faq/can-i-use-the-same-model-for-both-the-main-task-and-evaluation.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-we-evaluate-a-models-ability-to-express-uncertainty-or-know-what-it-doesnt-know" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-we-evaluate-a-models-ability-to-express-uncertainty-or-know-what-it-doesnt-know">Q: How do we evaluate a model’s ability to express uncertainty or "know what it doesn’t know"?</h2>
<p>Many applications require a model that can refuse to answer a question when it lacks sufficient information. To evaluate whether this refusal behavior is well-calibrated, you need to test if the model refuses at the appropriate times without refusing to answer questions it <em>should</em> be able to answer.</p>
<p>To do this effectively, you should construct an evaluation set that has the following components:</p>
<ol type="1">
<li><strong>Answerable Questions:</strong> Scenarios where a correct, verifiable answer is present in the model’s provided context or general knowledge.</li>
<li><strong>Unanswerable Questions:</strong> Scenarios designed to tempt the model to hallucinate. These include questions with false premises, queries about information explicitly missing from context, or topics far outside its knowledge base.</li>
</ol>
<p>While the exact proportion isn’t critical, a balanced set with a roughly equal number of answerable and unanswerable questions is a good starting point. The diversity and difficulty of the questions are more important than the precise ratio.</p>
<p>The evaluation itself is a binary (Pass/Fail) check of the model’s judgment. A “Pass” requires the model to satisfy two conditions: it must answer the answerable questions while also refusing to answer the unanswerable ones. A failure is defined as providing a fabricated answer to an unanswerable question, which indicates poor calibration.</p>
<p>In the research literature, this capability is known as “Abstention Ability.” To improve this behavior, it is worth <a href="https://arxiv.org/search/?query=Abstention+Ability&amp;searchtype=all">searching for this term on Arxiv</a> to understand the latest techniques.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-we-evaluate-a-models-ability-to-express-uncertainty-or-know-what-it-doesnt-know.html" class="faq-individual-link">↗ Focus view</a></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>
</section>
</section>
<section id="human-annotation-process" class="level1">
<h1>Human Annotation &amp; Process</h1>
<section id="q-how-many-people-should-annotate-my-llm-outputs" class="level2">
<h2 class="anchored" data-anchor-id="q-how-many-people-should-annotate-my-llm-outputs">Q: How many people should annotate my LLM outputs?</h2>
<p>For most small to medium-sized companies, appointing a single domain expert as a “benevolent dictator” is the most effective approach. This person—whether it’s a psychologist for a mental health chatbot, a lawyer for legal document analysis, or a customer service director for support automation—becomes the definitive voice on quality standards.</p>
<p>A single expert eliminates annotation conflicts and prevents the paralysis that comes from “too many cooks in the kitchen”. The benevolent dictator can incorporate input and feedback from others, but they drive the process. If you feel like you need five subject matter experts to judge a single interaction, it’s a sign your product scope might be too broad.</p>
<p>However, larger organizations or those operating across multiple domains (like a multinational company with different cultural contexts) may need multiple annotators. When you do use multiple people, you’ll need to measure their agreement using metrics like Cohen’s Kappa, which accounts for agreement beyond chance. However, use your judgment. Even in larger companies, a single expert is often enough.</p>
<p>Start with a benevolent dictator whenever feasible. Only add complexity when your domain demands it.</p>
<p><a href="../../../blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-should-product-managers-and-engineers-collaborate-on-error-analysis-how" class="level2">
<h2 class="anchored" data-anchor-id="q-should-product-managers-and-engineers-collaborate-on-error-analysis-how">Q: Should product managers and engineers collaborate on error analysis? How?</h2>
<p>At the outset, collaborate to establish shared context. Engineers catch technical issues like retrieval issues and tool errors. PMs identify product failures like unmet user expectations, confusing responses, or missing features users expect.</p>
<p>As time goes on you should lean towards a benevolent dictator for error analysis: a domain expert or PM who understands user needs. Empower domain experts to evaluate actual outcomes rather than technical implementation. Ask “Has an appointment been made?” not “Did the tool call succeed?” The best way to empower the domain expert is to give them custom annotation tools that display system outcomes alongside traces. Show the confirmation, generated email, or database update that validates goal completion. Keep all context on one screen so non-technical reviewers focus on results.</p>
<p><a href="../../../blog/posts/evals-faq/should-product-managers-and-engineers-collaborate-on-error-analysis-how.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-should-i-outsource-annotation-labeling-to-a-third-party" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-outsource-annotation-labeling-to-a-third-party">Q: Should I outsource annotation &amp; labeling to a third party?</h2>
<p>Outsourcing error analysis is usually a big mistake (with some exceptions). The core of evaluation is building the product intuition that only comes from systematically analyzing your system’s failures. You should be extremely skeptical of this process being delegated.</p>
<section id="the-dangers-of-outsourcing" class="level3">
<h3 class="anchored" data-anchor-id="the-dangers-of-outsourcing"><strong>The Dangers of Outsourcing</strong></h3>
<p>When you outsource annotation, you often break the feedback loop between observing a failure and understanding how to improve the product. Problems with outsourcing include:</p>
<ul>
<li>Superficial Labeling: Even well-defined metrics require nuanced judgment that external teams lack. A critical misstep in error analysis is excluding domain experts from the labeling process. Outsourcing this task to those without domain expertise, like general developers or IT staff, often leads to superficial or incorrect labeling.<br>
</li>
<li>Loss of Unspoken Knowledge: A principal domain expert possesses tacit knowledge and user understanding that cannot be fully captured in a rubric. Involving these experts helps uncover their preferences and expectations, which they might not be able to fully articulate upfront.<br>
</li>
<li>Annotation Conflicts and Misalignment: Without a shared context, external annotators can create more disagreement than they resolve. Achieving alignment is a challenge even for internal teams, which means you will spend even more time on this process.</li>
</ul>
</section>
<section id="the-recommended-approach-build-internal-capability" class="level3">
<h3 class="anchored" data-anchor-id="the-recommended-approach-build-internal-capability"><strong>The Recommended Approach: Build Internal Capability</strong></h3>
<p>Instead of outsourcing, focus on building an efficient internal evaluation process.</p>
<p>1. Appoint a “Benevolent Dictator”. For most teams, the most effective strategy is to appoint a single, internal domain expert as the final decision-maker on quality. This individual sets the standard, ensures consistency, and develops a sense of ownership.</p>
<p>2. Use a collaborative workflow for multiple annotators. If multiple annotators are necessary, follow a structured process to ensure alignment: * Draft an initial rubric with clear Pass/Fail definitions and examples. * Have each annotator label a shared set of traces independently to surface differences in interpretation. * Measure Inter-Annotator Agreement (IAA) using a chance-corrected metric like Cohen’s Kappa. * Facilitate alignment sessions to discuss disagreements and refine the rubric. * Iterate on this process until agreement is consistently high.</p>
</section>
<section id="how-to-handle-capacity-constraints" class="level3">
<h3 class="anchored" data-anchor-id="how-to-handle-capacity-constraints"><strong>How to Handle Capacity Constraints</strong></h3>
<p>Building internal capacity does not mean you have to label every trace. Use these strategies to manage the workload:</p>
<ul>
<li>Smart Sampling: Review a small, representative sample of traces thoroughly. It is more effective to analyze 100 diverse traces to find patterns than to superficially label thousands.<br>
</li>
<li>The “Think-Aloud” Protocol: To make the most of limited expert time, use this technique from usability testing. Ask an expert to verbalize their thought process while reviewing a handful of traces. This method can uncover deep insights in a single one-hour session.<br>
</li>
<li>Build Lightweight Custom Tools: Build custom annotation tools to streamline the review process, increasing throughput.</li>
</ul>
</section>
<section id="exceptions-for-external-help" class="level3">
<h3 class="anchored" data-anchor-id="exceptions-for-external-help"><strong>Exceptions for External Help</strong></h3>
<p>While outsourcing the core error analysis process is not recommended, there are some scenarios where external help is appropriate:</p>
<ul>
<li>Purely Mechanical Tasks: For highly objective, unambiguous tasks like identifying a phone number or validating an email address, external annotators can be used after a rigorous internal process has defined the rubric.<br>
</li>
<li>Tasks Without Product Context: Well-defined tasks that don’t require understanding your product’s specific requirements can be outsourced. Translation is a good example: it requires linguistic expertise but not deep product knowledge.<br>
</li>
<li>Engaging Subject Matter Experts: Hiring external SMEs to act as your internal domain experts is not outsourcing; it is bringing the necessary expertise into your evaluation process. For example, <a href="https://www.ankihub.net/" target="_blank">AnkiHub</a> hired 4th-year medical students to evaluate their RAG systems for medical content rather than outsourcing to generic annotators.</li>
</ul>
<p><a href="../../../blog/posts/evals-faq/should-i-outsource-annotation-and-labeling-to-a-third-party.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-what-parts-of-evals-can-be-automated-with-llms" class="level2">
<h2 class="anchored" data-anchor-id="q-what-parts-of-evals-can-be-automated-with-llms">Q: What parts of evals can be automated with LLMs?</h2>
<p>LLMs can speed up parts of your eval workflow, but they can’t replace human judgment where your expertise is essential. For example, if you let an LLM handle all of <a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" target="_blank">error analysis</a> (i.e., reviewing and annotating traces), you might overlook failure cases that matter for your product. Suppose users keep mentioning “lag” in feedback, but the LLM lumps these under generic “performance issues” instead of creating a “latency” category. You’d miss a recurring complaint about slow response times and fail to prioritize a fix.</p>
<p>That said, LLMs are valuable tools for accelerating certain parts of the evaluation workflow <em>when used with oversight</em>.</p>
<section id="here-are-some-areas-where-llms-can-help" class="level3">
<h3 class="anchored" data-anchor-id="here-are-some-areas-where-llms-can-help">Here are some areas where LLMs can help:</h3>
<ul>
<li><strong>First-pass axial coding:</strong> After you’ve open coded 30–50 traces yourself, use an LLM to organize your raw failure notes into proposed groupings. This helps you quickly spot patterns, but always review and refine the clusters yourself. <em>Note: If you aren’t familiar with axial and open coding, see <a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" target="_blank">this faq</a>.</em></li>
<li><strong>Mapping annotations to failure modes:</strong> Once you’ve defined failure categories, you can ask an LLM to suggest which categories apply to each new trace (e.g., “Given this annotation: [open_annotation] and these failure modes: [list_of_failure_modes], which apply?”).<br>
</li>
<li><strong>Suggesting prompt improvements:</strong> When you notice recurring problems, have the LLM propose concrete changes to your prompts. Review these suggestions before adopting any changes.<br>
</li>
<li><strong>Analyzing annotation data:</strong> Use LLMs or AI-powered notebooks to find patterns in your labels, such as “reports of lag increase 3x during peak usage hours” or “slow response times are mostly reported from users on mobile devices.”</li>
</ul>
</section>
<section id="however-you-shouldnt-outsource-these-activities-to-an-llm" class="level3">
<h3 class="anchored" data-anchor-id="however-you-shouldnt-outsource-these-activities-to-an-llm">However, you shouldn’t outsource these activities to an LLM:</h3>
<ul>
<li><strong>Initial open coding:</strong> Always read through the raw traces yourself at the start. This is how you discover new types of failures, understand user pain points, and build intuition about your data. Never skip this or delegate it.<br>
</li>
<li><strong>Validating failure taxonomies:</strong> LLM-generated groupings need your review. For example, an LLM might group both “app crashes after login” and “login takes too long” under a single “login issues” category, even though one is a stability problem and the other is a performance problem. Without your intervention, you’d miss that these issues require different fixes.<br>
</li>
<li><strong>Ground truth labeling:</strong> For any data used for testing/validating LLM-as-Judge evaluators, hand-validate each label. LLMs can make mistakes that lead to unreliable benchmarks.<br>
</li>
<li><strong>Root cause analysis:</strong> LLMs may point out obvious issues, but only human review will catch patterns like errors that occur in specific workflows or edge cases—such as bugs that happen only when users paste data from Excel.</li>
</ul>
<p>In conclusion, start by examining data manually to understand what’s actually going wrong. Use LLMs to scale what you’ve learned, not to avoid looking at data.</p>
<p><a href="../../../blog/posts/evals-faq/what-parts-of-evals-can-be-automated-with-llms.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-should-i-stop-writing-prompts-manually-in-favor-of-automated-tools" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-stop-writing-prompts-manually-in-favor-of-automated-tools">Q: Should I stop writing prompts manually in favor of automated tools?</h2>
<p>Automating prompt engineering can be tempting, but you should be skeptical of tools that promise to optimize prompts for you, especially in early stages of development. When you write a prompt, you are forced to clarify your assumptions and externalize your requirements. Good writing is good thinking <sup>2</sup>. If you delegate this task to an automated tool too early, you risk never fully understanding your own requirements or the model’s failure modes.</p>
<p>This is because automated prompt optimization typically hill-climb a predefined evaluation metric. It can refine a prompt to perform better on known failures, but it cannot discover <em>new</em> ones. Discovering new errors requires error analysis. Furthermore, research shows that evaluation criteria tends to shift after reviewing a model’s outputs, a phenomenon known as “criteria drift” <sup>3</sup>. This means that evaluation is an iterative, human-driven sensemaking process, not a static target that can be set once and handed off to an optimizer.</p>
<p>A pragmatic approach is to use LLMs to improve your prompt based on open coding (open-ended notes about traces). This way, you maintain a human in the loop who is looking at the data and externalizing their requirements. Once you have a high-quality set of evals, prompt optimization can be effective for that last mile of performance.</p>
<p><a href="../../../blog/posts/evals-faq/should-i-stop-writing-prompts-manually-in-favor-of-automated-tools.html" class="faq-individual-link">↗ Focus view</a></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>
</section>
</section>
<section id="tools-infrastructure" class="level1">
<h1>Tools &amp; Infrastructure</h1>
<section id="q-should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf" class="level2">
<h2 class="anchored" data-anchor-id="q-should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf">Q: Should I build a custom annotation tool or use something off-the-shelf?</h2>
<p><strong>Build a custom annotation tool.</strong> This is the single most impactful investment you can make for your AI evaluation workflow. With AI-assisted development tools like Cursor or Lovable, you can build a tailored interface in hours. I often find that teams with custom annotation tools iterate ~10x faster.</p>
<p>Custom tools excel because:</p>
<ul>
<li>They show all your context from multiple systems in one place</li>
<li>They can render your data in a product specific way (images, widgets, markdown, buttons, etc.)</li>
<li>They’re designed for your specific workflow (custom filters, sorting, progress bars, etc.)</li>
</ul>
<p>Off-the-shelf tools may be justified when you need to coordinate dozens of distributed annotators with enterprise access controls. Even then, many teams find the configuration overhead and limitations aren’t worth it.</p>
<p><a href="https://youtu.be/fA4pe9bE0LY" target="_blank">Isaac’s Anki flashcard annotation app</a> shows the power of custom tools—handling 400+ results per query with keyboard navigation and domain-specific evaluation criteria that would be nearly impossible to configure in a generic tool.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/fA4pe9bE0LY" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="../../../blog/posts/evals-faq/should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-what-makes-a-good-custom-interface-for-reviewing-llm-outputs" class="level2">
<h2 class="anchored" data-anchor-id="q-what-makes-a-good-custom-interface-for-reviewing-llm-outputs">Q: What makes a good custom interface for reviewing LLM outputs?</h2>
<p>Great interfaces make human review fast, clear, and motivating. We recommend building your own annotation tool customized to your domain. The following features are possible enhancements we’ve seen work well, but you don’t need all of them. The screenshots shown are illustrative examples to clarify concepts. In practice, I rarely implement all these features in a single app. It’s ultimately a judgment call based on your specific needs and constraints.</p>
<section id="render-traces-intelligently-not-generically" class="level3">
<h3 class="anchored" data-anchor-id="render-traces-intelligently-not-generically"><strong>1. Render Traces Intelligently, Not Generically</strong>:</h3>
<p>Present the trace in a way that’s intuitive for the domain. If you’re evaluating generated emails, render them to look like emails. If the output is code, use syntax highlighting. Allow the reviewer to see the full trace (user input, tool calls, and LLM reasoning), but keep less important details in collapsed sections that can be expanded. Here is an example of a custom annotation tool for reviewing real estate assistant emails:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/emailinterface1.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>A custom interface for reviewing emails for a real estate assistant.</figcaption>
</figure>
</div>
</section>
<section id="show-progress-and-support-keyboard-navigation" class="level3">
<h3 class="anchored" data-anchor-id="show-progress-and-support-keyboard-navigation"><strong>2. Show Progress and Support Keyboard Navigation</strong>:</h3>
<p>Keep reviewers in a state of flow by minimizing friction and motivating completion. Include progress indicators (e.g., “Trace 45 of 100”) to keep the review session bounded and encourage completion. Enable hotkeys for navigating between traces (e.g., N for next), applying labels, and saving notes quickly. Below is an illustration of these features:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/hotkey.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>An annotation interface with a progress bar and hotkey guide</figcaption>
</figure>
</div>
</section>
<section id="trace-navigation-through-clustering-filtering-and-search" class="level3">
<h3 class="anchored" data-anchor-id="trace-navigation-through-clustering-filtering-and-search"><strong>3. Trace navigation through clustering, filtering, and search</strong>:</h3>
<p>Allow reviewers to filter traces by metadata or search by keywords. Semantic search helps find conceptually similar problems. Clustering similar traces (like grouping by user persona) lets reviewers spot recurring issues and explore hypotheses. Below is an illustration of these features:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/group1.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>Cluster view showing groups of emails, such as property-focused or client-focused examples. Reviewers can drill into a group to see individual traces.</figcaption>
</figure>
</div>
</section>
<section id="prioritize-labeling-traces-you-think-might-be-problematic" class="level3">
<h3 class="anchored" data-anchor-id="prioritize-labeling-traces-you-think-might-be-problematic"><strong>4. Prioritize labeling traces you think might be problematic</strong>:</h3>
<p>Surface traces flagged by guardrails, CI failures, or automated evaluators for review. Provide buttons to take actions like adding to datasets, filing bugs, or re-running pipeline tests. Display relevant context (pipeline version, eval scores, reviewer info) directly in the interface to minimize context switching. Below is an illustration of these ideas:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/ci.png" class="img-fluid figure-img" style="width:88.0%" target="_blank"></p>
<figcaption>A trace view that allows you to quickly see auto-evaluator verdict, add traces to dataset or open issues. Also shows metadata like pipeline version, reviewer info, and more.</figcaption>
</figure>
</div>
</section>
<section id="general-principle-keep-it-minimal" class="level3">
<h3 class="anchored" data-anchor-id="general-principle-keep-it-minimal">General Principle: Keep it minimal</h3>
<p>Keep your annotation interface minimal. Only incorporate these ideas if they provide a benefit that outweighs the additional complexity and maintenance overhead.</p>
<p><a href="../../../blog/posts/evals-faq/what-makes-a-good-custom-interface-for-reviewing-llm-outputs.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself" class="level2">
<h2 class="anchored" data-anchor-id="q-what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself">Q: What gaps in eval tooling should I be prepared to fill myself?</h2>
<p>Most eval tools handle the basics well: logging complete traces, tracking metrics, prompt playgrounds, and annotation queues. These are table stakes. Here are four areas where you’ll likely need to supplement existing tools.</p>
<p>Watch for vendors addressing these gaps: it’s a strong signal they understand practitioner needs.</p>
<section id="error-analysis-and-pattern-discovery" class="level3">
<h3 class="anchored" data-anchor-id="error-analysis-and-pattern-discovery">1. Error Analysis and Pattern Discovery</h3>
<p>After reviewing traces where your AI fails, can your tooling automatically cluster similar issues? For instance, if multiple traces show the assistant using casual language for luxury clients, you need something that recognizes this broader “persona-tone mismatch” pattern. We recommend building capabilities that use AI to suggest groupings, rewrite your observations into clearer failure taxonomies, help find similar cases through semantic search, etc.</p>
</section>
<section id="ai-powered-assistance-throughout-the-workflow" class="level3">
<h3 class="anchored" data-anchor-id="ai-powered-assistance-throughout-the-workflow">2. AI-Powered Assistance Throughout the Workflow</h3>
<p>The most effective workflows use AI to accelerate every stage of evaluation. During error analysis, you want an LLM helping categorize your open-ended observations into coherent failure modes. For example, you might annotate several traces with notes like “wrong tone for investor,” “too casual for luxury buyer,” etc. Your tooling should recognize these as the same underlying pattern and suggest a unified “persona-tone mismatch” category.</p>
<p>You’ll also want AI assistance in proposing fixes. After identifying 20 cases where your assistant omits pet policies from property summaries, can your workflow analyze these failures and suggest specific prompt modifications? Can it draft refinements to your SQL generation instructions when it notices patterns of missing WHERE clauses?</p>
<p>Good workflows also help you conduct data analysis of your annotations and traces. I like using notebooks with AI in-the-loop like <a href="https://julius.ai/" target="_blank">Julius</a> or <a href="https://hex.tech" target="_blank">Hex</a>. These help me discover insights like “location ambiguity errors spike 3x when users mention neighborhood names” or “tone mismatches occur 80% more often in email generation than other modalities.”</p>
</section>
<section id="custom-evaluators-over-generic-metrics" class="level3">
<h3 class="anchored" data-anchor-id="custom-evaluators-over-generic-metrics">3. Custom Evaluators Over Generic Metrics</h3>
<p>Be prepared to build most of your evaluators from scratch. Generic metrics like “hallucination score” or “helpfulness rating” rarely capture what actually matters for your application—like proposing unavailable showing times or omitting budget constraints from emails. In our experience, successful teams spend most of their effort on application-specific metrics.</p>
</section>
<section id="apis-that-support-custom-annotation-apps" class="level3">
<h3 class="anchored" data-anchor-id="apis-that-support-custom-annotation-apps">4. APIs That Support Custom Annotation Apps</h3>
<p>Custom annotation interfaces work best for most teams. This requires observability platforms with thoughtful APIs. I often have to build my own libraries and abstractions just to make bulk data export manageable. You shouldn’t have to paginate through thousands of requests or handle timeout-prone endpoints just to get your data. Look for platforms that provide true bulk export capabilities and, crucially, APIs that let you write annotations back efficiently.</p>
<p><a href="../../../blog/posts/evals-faq/what-gaps-in-eval-tooling-should-i-be-prepared-to-fill-myself.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-whats-your-favorite-eval-vendor" class="level2">
<h2 class="anchored" data-anchor-id="q-whats-your-favorite-eval-vendor">Q: What’s your favorite eval vendor?</h2>
<p>Eval tools are in an intensely competitive space. It would be futile to compare their features. If I tried to do such an analysis, it would be invalidated in a week! Vendors I encounter the most organically in my work are: <a href="https://www.langchain.com/langsmith" target="_blank">Langsmith</a>, <a href="https://arize.com/" target="_blank">Arize</a> and <a href="https://www.braintrust.dev/" target="_blank">Braintrust</a>.</p>
<p>When I help clients with vendor selection, the decision weighs heavily towards who can offer the best support, as opposed to purely features. This changes depending on size of client, use case, etc. Yes - it’s mainly the human factor that matters, and dare I say, vibes.</p>
<p>I have no favorite vendor. At the core, their features are very similar - and I often build <a href="https://hamel.dev/blog/posts/evals/#q-should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf" target="_blank">custom tools</a> on top of them to fit my needs.</p>
<p>Here is a <a href="../../../blog/posts/eval-tools/">video series</a> that has a live commentary on the relative strengths and weaknesses of the three aforementioned vendors.</p>
<p><a href="../../../blog/posts/evals-faq/whats-your-favorite-eval-vendor.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-should-i-version-and-manage-prompts" class="level2">
<h2 class="anchored" data-anchor-id="q-how-should-i-version-and-manage-prompts">Q: How should I version and manage prompts?</h2>
<p>There is an unavoidable tension between keeping prompts close to the code vs.&nbsp;an environment that non-technical stakeholders can access.</p>
<p><strong>My preferred approach is storing prompts in Git.</strong> This treats them as software artifacts that are versioned, reviewed, and deployed atomically with the application code. While the Git command line is unfriendly for non-technical folks, the <a href="https://github.com">GitHub</a> web interface and the GitHub <a href="https://desktop.github.com/">Desktop app</a> make it very approachable. When I was working at GitHub, I worked with many non-technical professionals, including lawyers and accountants, who used these tools effectively. Here is a <a href="https://ben.balter.com/2023/03/02/github-for-non-technical-roles/">blog post</a> aimed at non-technical folks to get started.</p>
<p>Alternatively, most vendors in the LLM tooling space, such as observability platforms like Arize, Braintrust, and LangSmith, offer dedicated prompt management tools. These are accessible for rapid iteration but risk creating additional layers of indirection.</p>
<p><strong>Why prompt management tools often fall short:</strong> AI products typically involve many moving parts: tools, RAG, agents, etc. Prompt management tools are inherently limiting because they can’t easily execute your application’s code. Even when they can, there’s often significant indirection involved, making it difficult to test prompts with your system’s capabilities.</p>
<p><strong>When possible, a notebook provides a great solution for prompt experimentation</strong> If you have Python entry points into your codebase or your codebase is written in Python, Jupyter notebooks are particularly powerful for this purpose. You can experiment with prompts and iterate on your actual AI agents with their full tool and RAG capabilities. This makes it much easier to understand how your system works in practice. Additionally, you can create widgets and small user interfaces within notebooks, giving you the best of both worlds for experimentation and iteration. To see what this looks like in practice, Teresa Torres gives a fantastic, hands-on walkthrough of how she, as a PM, used notebooks for the entire eval and experimentation lifecycle:</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/N-qAOv_PNPc" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p>If notebooks are not feasible for your code base, an <a href="../../../blog/posts/field-guide/#build-bridges-not-gatekeepers">​integrated prompt environment</a>​ can be effective for experimentation. Either way, I prefer to version and manage prompts in Git.</p>
<p><a href="../../../blog/posts/evals-faq/how-should-i-version-and-manage-prompts.html" class="faq-individual-link">↗ Focus view</a></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>
</section>
</section>
<section id="production-deployment" class="level1">
<h1>Production &amp; Deployment</h1>
<section id="q-how-are-evaluations-used-differently-in-cicd-vs.-monitoring-production" class="level2">
<h2 class="anchored" data-anchor-id="q-how-are-evaluations-used-differently-in-cicd-vs.-monitoring-production">Q: How are evaluations used differently in CI/CD vs.&nbsp;monitoring production?</h2>
<p>The most important difference between CI vs.&nbsp;production evaluation is the data used for testing.</p>
<p>Test datasets for CI are small (in many cases 100+ examples) and purpose-built. Examples cover core features, regression tests for past bugs, and known edge cases. Since CI tests are run frequently, the cost of each test has to be carefully considered (that’s why you carefully curate the dataset). Favor assertions or other deterministic checks over LLM-as-judge evaluators.</p>
<p>For evaluating production traffic, you can sample live traces and run evaluators against them asynchronously. Since you usually lack reference outputs on production data, you might rely more on on more expensive reference-free evaluators like LLM-as-judge. Additionally, track confidence intervals for production metrics. If the lower bound crosses your threshold, investigate further.</p>
<p>These two systems are complementary: when production monitoring reveals new failure patterns through error analysis and evals, add representative examples to your CI dataset. This mitigates regressions on new issues.</p>
<p><a href="../../../blog/posts/evals-faq/how-are-evaluations-used-differently-in-cicd-vs-monitoring-production.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-whats-the-difference-between-guardrails-evaluators" class="level2">
<h2 class="anchored" data-anchor-id="q-whats-the-difference-between-guardrails-evaluators">Q: What’s the difference between guardrails &amp; evaluators?</h2>
<p>Guardrails are <strong>inline safety checks</strong> that sit directly in the request/response path. They validate inputs or outputs <em>before</em> anything reaches a user, so they typically are:</p>
<ul>
<li><strong>Fast and deterministic</strong> – typically a few milliseconds of latency budget.</li>
<li><strong>Simple and explainable</strong> – regexes, keyword block-lists, schema or type validators, lightweight classifiers.</li>
<li><strong>Targeted at clear-cut, high-impact failures</strong> – PII leaks, profanity, disallowed instructions, SQL injection, malformed JSON, invalid code syntax, etc.</li>
</ul>
<p>If a guardrail triggers, the system can redact, refuse, or regenerate the response. Because these checks are user-visible when they fire, false positives are treated as production bugs; teams version guardrail rules, log every trigger, and monitor rates to keep them conservative.</p>
<p>On the other hand, evaluators typically run <strong>after</strong> a response is produced. Evaluators measure qualities that simple rules cannot, such as factual correctness, completeness, etc. Their verdicts feed dashboards, regression tests, and model-improvement loops, but they do not block the original answer.</p>
<p>Evaluators are usually run asynchronously or in batch to afford heavier computation such as a <a href="https://hamel.dev/blog/posts/llm-judge/" target="_blank">LLM-as-a-Judge</a>. Inline use of an LLM-as-Judge is possible <em>only</em> when the latency budget and reliability targets allow it. Slow LLM judges might be feasible in a cascade that runs on the minority of borderline cases.</p>
<p>Apply guardrails for immediate protection against objective failures requiring intervention. Use evaluators for monitoring and improving subjective or nuanced criteria. Together, they create layered protection.</p>
<p>Word of caution: Do not use llm guardrails off the shelf blindly. Always <a href="https://hamel.dev/blog/posts/prompt/" target="_blank">look at the prompt</a>.</p>
<p><a href="../../../blog/posts/evals-faq/whats-the-difference-between-guardrails-evaluators.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production" class="level2">
<h2 class="anchored" data-anchor-id="q-can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production">Q: Can my evaluators also be used to automatically <em>fix</em> or <em>correct</em> outputs in production?</h2>
<p>Yes, but only a specific subset of them. This is the distinction between an <strong>evaluator</strong> and a <strong>guardrail</strong> that we previously discussed. As a reminder:</p>
<ul>
<li><strong>Evaluators</strong> typically run <em>asynchronously</em> after a response has been generated. They measure quality but don’t interfere with the user’s immediate experience.<br>
</li>
<li><strong>Guardrails</strong> run <em>synchronously</em> in the critical path of the request, before the output is shown to the user. Their job is to prevent high-impact failures in real-time.</li>
</ul>
<p>There are two important decision criteria for deciding whether to use an evaluator as a guardrail:</p>
<ol type="1">
<li><p><strong>Latency &amp; Cost</strong>: Can the evaluator run fast enough and cheaply enough in the critical request path without degrading user experience?</p></li>
<li><p><strong>Error Rate Trade-offs</strong>: What’s the cost-benefit balance between false positives (blocking good outputs and frustrating users) versus false negatives (letting bad outputs reach users and causing harm)? In high-stakes domains like medical advice, false negatives may be more costly than false positives. In creative applications, false positives that block legitimate creativity may be more harmful than occasional quality issues.</p></li>
</ol>
<p>Most guardrails are designed to be <strong>fast</strong> (to avoid harming user experience) and have a <strong>very low false positive rate</strong> (to avoid blocking valid responses). For this reason, you would almost never use a slow or non-deterministic LLM-as-Judge as a synchronous guardrail. However, these tradeoffs might be different for your use case.</p>
<p><a href="../../../blog/posts/evals-faq/can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-much-time-should-i-spend-on-model-selection" class="level2">
<h2 class="anchored" data-anchor-id="q-how-much-time-should-i-spend-on-model-selection">Q: How much time should I spend on model selection?</h2>
<p>Many developers fixate on model selection as the primary way to improve their LLM applications. Start with error analysis to understand your failure modes before considering model switching. As Hamel noted in office hours, “I suggest not thinking of switching model as the main axes of how to improve your system off the bat without evidence. Does error analysis suggest that your model is the problem?”</p>
<p><a href="../../../blog/posts/evals-faq/how-much-time-should-i-spend-on-model-selection.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="domain-specific-applications" class="level1">
<h1>Domain-Specific Applications</h1>
<section id="q-is-rag-dead" class="level2">
<h2 class="anchored" data-anchor-id="q-is-rag-dead">Q: Is RAG dead?</h2>
<p>Question: Should I avoid using RAG for my AI application after reading that <a href="https://pashpashpash.substack.com/p/why-i-no-longer-recommend-rag-for" target="_blank">“RAG is dead”</a> for coding agents?</p>
<blockquote class="blockquote">
<p>Many developers are confused about when and how to use RAG after reading articles claiming “RAG is dead.” Understanding what RAG actually means versus the narrow marketing definitions will help you make better architectural decisions for your AI applications.</p>
</blockquote>
<p>The viral article claiming RAG is dead specifically argues against using <em>naive vector database retrieval</em> for autonomous coding agents, not RAG as a whole. This is a crucial distinction that many developers miss due to misleading marketing.</p>
<p>RAG simply means Retrieval-Augmented Generation - using retrieval to provide relevant context that improves your model’s output. The core principle remains essential: your LLM needs the right context to generate accurate answers. The question isn’t whether to use retrieval, but how to retrieve effectively.</p>
<p>For coding applications, naive vector similarity search often fails because code relationships are complex and contextual. Instead of abandoning retrieval entirely, modern coding assistants like Claude Code <a href="https://x.com/pashmerepat/status/1926717705660375463?s=46" target="_blank">still uses retrieval</a> —they just employ agentic search instead of relying solely on vector databases, similar to how human developers work.</p>
<p>You have multiple retrieval strategies available, ranging from simple keyword matching to embedding similarity to LLM-powered relevance filtering. The optimal approach depends on your specific use case, data characteristics, and performance requirements. Many production systems combine multiple strategies or use multi-hop retrieval guided by LLM agents.</p>
<p>Unfortunately, “RAG” has become a buzzword with no shared definition. Some people use it to mean any retrieval system, others restrict it to vector databases. Focus on the ultimate goal: getting your LLM the context it needs to succeed. Whether that’s through vector search, agentic exploration, or hybrid approaches is a product and engineering decision.</p>
<p>Rather than following categorical advice to avoid or embrace RAG, experiment with different retrieval approaches and measure what works best for your application. For more info on RAG evaluation and optimization, see <a href="../../../notes/llm/rag/not_dead.html">this series of posts</a>.</p>
<p><a href="../../../blog/posts/evals-faq/is-rag-dead.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-should-i-approach-evaluating-my-rag-system" class="level2">
<h2 class="anchored" data-anchor-id="q-how-should-i-approach-evaluating-my-rag-system">Q: How should I approach evaluating my RAG system?</h2>
<p>RAG systems have two distinct components that require different evaluation approaches: retrieval and generation.</p>
<p>The retrieval component is a search problem. Evaluate it using traditional information retrieval (IR) metrics. Common examples include Recall@k (of all relevant documents, how many did you retrieve in the top k?), Precision@k (of the k documents retrieved, how many were relevant?), or MRR (how high up was the first relevant document?). The specific metrics you choose depend on your use case. These metrics are pure search metrics that measure whether you’re finding the right documents (more on this below).</p>
<p>To evaluate retrieval, create a dataset of queries paired with their relevant documents. Generate this synthetically by taking documents from your corpus, extracting key facts, then generating questions those facts would answer. This reverse process gives you query-document pairs for measuring retrieval performance without manual annotation.</p>
<p>For the generation component—how well the LLM uses retrieved context, whether it hallucinates, whether it answers the question—use the same evaluation procedures covered throughout this course: error analysis to identify failure modes, collecting human labels, building LLM-as-judge evaluators, and validating those judges against human annotations.</p>
<p>Jason Liu’s <a href="https://jxnl.co/writing/2025/05/19/there-are-only-6-rag-evals/" target="_blank">“There Are Only 6 RAG Evals”</a> provides a framework that maps well to this separation. His Tier 1 covers traditional IR metrics for retrieval. Tiers 2 and 3 evaluate relationships between Question, Context, and Answer—like whether the context is relevant (C|Q), whether the answer is faithful to context (A|C), and whether the answer addresses the question (A|Q).</p>
<p>In addition to Jason’s six evals, error analysis on your specific data may reveal domain-specific failure modes that warrant their own metrics. For example, a medical RAG system might consistently fail to distinguish between drug dosages for adults versus children, or a legal RAG might confuse jurisdictional boundaries. These patterns emerge only through systematic review of actual failures. Once identified, you can create targeted evaluators for these specific issues beyond the general framework.</p>
<p>Finally, when implementing Jason’s Tier 2 and 3 metrics, don’t just use prompts off the shelf. The standard LLM-as-judge process requires several steps: error analysis, prompt iteration, creating labeled examples, and measuring your judge’s accuracy against human labels. Once you know your judge’s True Positive and True Negative rates, you can correct its estimates to determine the actual failure rate in your system. Skip this validation and your judges may not reflect your actual quality criteria.</p>
<p>In summary, debug retrieval first using IR metrics, then tackle generation quality using properly validated LLM judges.</p>
<p><a href="../../../blog/posts/evals-faq/how-should-i-approach-evaluating-my-rag-system.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks">Q: How do I choose the right chunk size for my document processing tasks?</h2>
<p>Unlike RAG, where chunks are optimized for retrieval, document processing assumes the model will see every chunk. The goal is to split text so the model can reason effectively without being overwhelmed. Even if a document fits within the context window, it might be better to break it up. Long inputs can degrade performance due to attention bottlenecks, especially in the middle of the context. Two task types require different strategies:</p>
<section id="fixed-output-tasks-large-chunks" class="level3">
<h3 class="anchored" data-anchor-id="fixed-output-tasks-large-chunks">1. Fixed-Output Tasks → Large Chunks</h3>
<p>These are tasks where the output length doesn’t grow with input: extracting a number, answering a specific question, classifying a section. For example:</p>
<ul>
<li>“What’s the penalty clause in this contract?”</li>
<li>“What was the CEO’s salary in 2023?”</li>
</ul>
<p>Use the largest chunk (with caveats) that likely contains the answer. This reduces the number of queries and avoids context fragmentation. However, avoid adding irrelevant text. Models are sensitive to distraction, especially with large inputs. The middle parts of a long input might be under-attended. Furthermore, if cost and latency are a bottleneck, you should consider preprocessing or filtering the document (via keyword search or a lightweight retriever) to isolate relevant sections before feeding a huge chunk.</p>
</section>
<section id="expansive-output-tasks-smaller-chunks" class="level3">
<h3 class="anchored" data-anchor-id="expansive-output-tasks-smaller-chunks">2. Expansive-Output Tasks → Smaller Chunks</h3>
<p>These include summarization, exhaustive extraction, or any task where output grows with input. For example:</p>
<ul>
<li>“Summarize each section”</li>
<li>“List all customer complaints”</li>
</ul>
<p>In these cases, smaller chunks help preserve reasoning quality and output completeness. The standard approach is to process each chunk independently, then aggregate results (e.g., map-reduce). When sizing your chunks, try to respect content boundaries like paragraphs, sections, or chapters. Chunking also helps mitigate output limits. By breaking the task into pieces, each piece’s output can stay within limits.</p>
</section>
<section id="general-guidance" class="level3">
<h3 class="anchored" data-anchor-id="general-guidance">General Guidance</h3>
<p>It’s important to recognize <strong>why chunk size affects results</strong>. A larger chunk means the model has to reason over more information in one go – essentially, a heavier cognitive load. LLMs have limited capacity to <strong>retain and correlate details across a long text</strong>. If too much is packed in, the model might prioritize certain parts (commonly the beginning or end) and overlook or “forget” details in the middle. This can lead to overly coarse summaries or missed facts. In contrast, a smaller chunk bounds the problem: the model can pay full attention to that section. You are trading off <strong>global context for local focus</strong>.</p>
<p>No rule of thumb can perfectly determine the best chunk size for your use case – <strong>you should validate with experiments</strong>. The optimal chunk size can vary by domain and model. I treat chunk size as a hyperparameter to tune.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-how-do-i-debug-multi-turn-conversation-traces" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-debug-multi-turn-conversation-traces">Q: How do I debug multi-turn conversation traces?</h2>
<p>Start simple. Check if the whole conversation met the user’s goal with a pass/fail judgment. Look at the entire trace and focus on the first upstream failure. Read the user-visible parts first to understand if something went wrong. Only then dig into the technical details like tool calls and intermediate steps.</p>
<section id="multi-agent-trace-logging" class="level3">
<h3 class="anchored" data-anchor-id="multi-agent-trace-logging">Multi-agent trace logging</h3>
<p>For multi-agent flows, assign a session or trace ID to each user request and log every message with its source (which agent or tool), trace ID, and position in the sequence. This lets you reconstruct the full path from initial query to final result across all agents.</p>
</section>
<section id="annotation-strategy" class="level3">
<h3 class="anchored" data-anchor-id="annotation-strategy">Annotation strategy</h3>
<p>Annotate only the first failure in the trace initially—don’t worry about downstream failures since these often cascade from the first issue. Fixing upstream failures often resolves dependent downstream failures automatically. As you gain experience, you can annotate independent failure modes within the same trace to speed up overall error analysis.</p>
</section>
<section id="simplify-when-possible" class="level3">
<h3 class="anchored" data-anchor-id="simplify-when-possible">Simplify when possible</h3>
<p>When you find a failure, reproduce it with the simplest possible test case. Here’s an example: suppose a shopping bot gives the wrong return policy on turn 4 of a conversation. Before diving into the full multi-turn complexity, simplify it to a single turn: “What is the return window for product X1000?” If it still fails, you’ve proven the error isn’t about conversation context - it’s likely a basic retrieval or knowledge issue you can debug more easily.</p>
</section>
<section id="test-case-generation" class="level3">
<h3 class="anchored" data-anchor-id="test-case-generation">Test case generation</h3>
<p>You have two main approaches. First, simulate users with another LLM to create realistic multi-turn conversations. Second, use “N-1 testing” where you provide the first N-1 turns of a real conversation and test what happens next. The N-1 approach often works better since it uses actual conversation prefixes rather than fully synthetic interactions, but is less flexible.</p>
<p>The key is balancing thoroughness with efficiency. Not every multi-turn failure requires multi-turn analysis.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-debug-multi-turn-conversation-traces.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
</section>
<section id="q-how-do-i-evaluate-sessions-with-human-handoffs" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-evaluate-sessions-with-human-handoffs">Q: How do I evaluate sessions with human handoffs?</h2>
<p>Capture the complete user journey in your traces, including human handoffs. The trace continues until the user’s need is resolved or the session ends, not when AI hands off to a human. Log the handoff decision, why it occurred, context transferred, wait time, human actions, final resolution, and whether the human had sufficient context. Many failures occur at handoff boundaries where AI hands off too early, too late, or without proper context.</p>
<p>Evaluate handoffs as potential failure modes during error analysis. Ask: Was the handoff necessary? Did the AI provide adequate context? Track both handoff quality and handoff rate. Sometimes the best improvement reduces handoffs entirely rather than improving handoff execution.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-evaluate-sessions-with-human-handoffs.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-i-evaluate-complex-multi-step-workflows" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-evaluate-complex-multi-step-workflows">Q: How do I evaluate complex multi-step workflows?</h2>
<p>Log the entire workflow from initial trigger to final business outcome. Include LLM calls, tool usage, human approvals, and database writes in your traces. You will need this visibility to properly diagnose failures.</p>
<p>Use both outcome and process metrics. Outcome metrics verify the final result meets requirements: Was the business case complete? Accurate? Properly formatted? Process metrics evaluate efficiency: step count, time taken, resource usage. Process failures are often easier to debug since they’re more deterministic, so tackle them first.</p>
<p>Segment your error analysis by workflow stages. Early stage failures (understanding user input) differ from middle stage failures (data processing) and late stage failures (formatting output). Early stage improvements have more impact since errors cascade in LLM chains.</p>
<p>Use transition failure matrices to analyze where workflows break. Create a matrix showing the last successful state versus where the first failure occurred. This reveals failure hotspots and guides where to invest debugging effort.</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-evaluate-complex-multi-step-workflows.html" class="faq-individual-link">↗ Focus view</a></p>
</section>
<section id="q-how-do-i-evaluate-agentic-workflows" class="level2">
<h2 class="anchored" data-anchor-id="q-how-do-i-evaluate-agentic-workflows">Q: How do I evaluate agentic workflows?</h2>
<p>We recommend evaluating agentic workflows in two phases:</p>
<p><strong>1. End-to-end task success.</strong> Treat the agent as a black box and ask “did we meet the user’s goal?”. Define a precise success rule per task (exact answer, correct side-effect, etc.) and measure with human or <a href="https://hamel.dev/blog/posts/llm-judge/" target="_blank">aligned LLM judges</a>. Take note of the first upstream failure when conducting error analysis.</p>
<p>Once error analysis reveals which workflows fail most often, move to step-level diagnostics to understand why they’re failing.</p>
<p><strong>2. Step-level diagnostics.</strong> Assuming that you have sufficiently <a href="https://hamel.dev/blog/posts/evals/#logging-traces" target="_blank">instrumented your system</a> with details of tool calls and responses, you can score individual components such as: - <em>Tool choice</em>: was the selected tool appropriate? - <em>Parameter extraction</em>: were inputs complete and well-formed? - <em>Error handling</em>: did the agent recover from empty results or API failures? - <em>Context retention</em>: did it preserve earlier constraints? - <em>Efficiency</em>: how many steps, seconds, and tokens were spent? - <em>Goal checkpoints</em>: for long workflows verify key milestones.</p>
<p>Example: “Find Berkeley homes under $1M and schedule viewings” breaks into: parameters extracted correctly, relevant listings retrieved, availability checked, and calendar invites sent. Each checkpoint can pass or fail independently, making debugging tractable.</p>
<p><strong>Use transition failure matrices to understand error patterns.</strong> Create a matrix where rows represent the last successful state and columns represent where the first failure occurred. This is a great way to understand where the most failures occur.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/shreya_matrix.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>Transition failure matrix showing hotspots in text-to-SQL agent workflow</figcaption>
</figure>
</div>
<p>Transition matrices transform overwhelming agent complexity into actionable insights. Instead of drowning in individual trace reviews, you can immediately see that GenSQL → ExecSQL transitions cause 12 failures while DecideTool → PlanCal causes only 2. This data-driven approach guides where to invest debugging effort. Here is another <a href="https://www.figma.com/deck/nwRlh5renu4s4olaCsf9lG/Failure-is-a-Funnel?node-id=2009-927&amp;t=GJlTtxQ8bLJaQ92A-1" target="_blank">example</a> from Bryan Bischof, that is also a text-to-SQL agent:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/bischof_matrix.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>Bischof, Bryan “Failure is A Funnel - Data Council, 2025”</figcaption>
</figure>
</div>
<p>In this example, Bryan shows variation in transition matrices across experiments. How you organize your transition matrix depends on the specifics of your application. For example, Bryan’s text-to-SQL agent has an inherent sequential workflow which he exploits for further analytical insight. You can watch his <a href="https://youtu.be/R_HnI9oTv3c?si=hRRhDiydHU5k6ikc" target="_blank">full talk</a> for more details.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/R_HnI9oTv3c" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><strong>Creating Test Cases for Agent Failures</strong></p>
<p>Creating test cases for agent failures follows the same principles as our previous FAQ on debugging multi-turn conversation traces (i.e.&nbsp;try to reproduce the error in the simplest way possible, only use multi-turn tests when the failure actually requires conversation context, etc.).</p>
<p><a href="../../../blog/posts/evals-faq/how-do-i-evaluate-agentic-workflows.html" class="faq-individual-link">↗ Focus view</a></p>
<hr>
<div class="cta" style="text-align: center;">
<p><strong>👉 <em>Want to learn more about AI Evals? Check out our <a href="https://bit.ly/evals-ai" target="_blank">AI Evals course</a></em></strong>. It’s a live cohort with hands on exercises and office hours. Here is a <a href="https://bit.ly/evals-ai" target="_blank">25% discount code</a> for readers. 👈</p>
</div>
<hr>


</section>
</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p><a href="https://www.linkedin.com/in/intellectronica/" target="_blank">Eleanor Berger</a>, our wonderful TA.↩︎</p></li>
<li id="fn2"><p>Paul Graham, <a href="https://paulgraham.com/writes.html" target="_blank">“Writes and Write-Nots”</a>↩︎</p></li>
<li id="fn3"><p>Shreya Shankar, et al., <a href="https://arxiv.org/abs/2404.12272" target="_blank">“Who Validates the Validators? Aligning LLM-Assisted Evaluation of LLM Outputs with Human Preferences”</a>↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/</guid>
  <pubDate>Thu, 15 Jan 2026 08:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: Can I use the same model for both the main task and evaluation?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/can-i-use-the-same-model-for-both-the-main-task-and-evaluation.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>For LLM-as-Judge selection, using the same model is usually fine because the judge is doing a different task than your main LLM pipeline. While <a href="https://arxiv.org/pdf/2508.06709">research has shown</a> that models can exhibit bias when evaluating their own outputs, what ultimately matters is how well your judge aligns with human judgments. The judges we recommend building do <a href="../../../blog/posts/evals-faq/why-do-you-recommend-binary-passfail-evaluations-instead-of-1-5-ratings-likert-scales.html" target="_blank">scoped binary classification tasks</a>. We’ve found that iterative alignment with human labels is usually achievable on this constrained task.</p>
<p>Focus on achieving high True Positive Rate (TPR) and True Negative Rate (TNR) with your judge on a held out labeled test set. If you struggle to achieve good alignment with human scores, then consider trying a different model. However onboarding new model providers may involve non-trivial effort in some organizations, which is why we don’t advocate for using different models by default unless there’s a specific alignment issue.</p>
<p>When selecting judge models, start with the most capable models available to establish strong alignment with human judgments. You can optimize for cost later once you’ve established reliable evaluation criteria.</p>
<p><a href="../../../blog/posts/evals-faq/#q-can-i-use-the-same-model-for-both-the-main-task-and-evaluation" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/can-i-use-the-same-model-for-both-the-main-task-and-evaluation.html</guid>
  <pubDate>Fri, 17 Oct 2025 20:29:48 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Selecting The Right AI Evals Tool</title>
  <dc:creator>Hamel Husain</dc:creator>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-tools/</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->




<p>Over the past year, I’ve focused heavily on <a href="../../../blog/posts/evals-faq/index.html">AI Evals</a>, both in my consulting work and teaching. A question I get constantly is, “What’s the best tool for evals?”. I’ve always resisted answering directly for two reasons. First, people focus too much on tools instead of the process, thinking the tool will be an off-the-shelf solution when it rarely is. Second, the tools change so quickly that comparisons become outdated immediately.</p>
<p>Having used many of the popular eval tools, I can genuinely say that no single one is superior in every dimension. The “best” tool depends on your team’s skillset, technical stack, and maturity.</p>
<p>Instead of a feature-by-feature comparison, I think it’s more valuable to show you <em>how</em> a panel of data scientists skilled in evals assesses these tools. As part of my AI Evals <a href="https://maven.com/parlance-labs/evals?promoCode=evals-info-url" target="_blank">course</a>, we had three of the most dominant vendors—Langsmith, Braintrust, and Arize Phoenix complete the same homework <a href="https://github.com/ai-evals-course/recipe-chatbot" target="_blank">assignment</a>. This gave us a unique opportunity to see how they tackle the exact same challenge.</p>
<p>We recorded the entire process and live commentary, which is available below. We think this might be helpful in learning about the kinds of things you should consider when selecting a tool for your team.</p>
<p><em>Thanks to <a href="https://www.sh-reya.com/">Shreya Shankar</a> and <a href="https://x.com/BEBischof">Bryan Bischof</a> for serving as the panelists (alongside me).</em></p>
<section id="langsmith" class="level2">
<h2 class="anchored" data-anchor-id="langsmith">Langsmith</h2>
<p>With <a href="https://x.com/hwchase17">Harrison Chase</a>, CEO of LangChain.</p>
<div style="width: 70%; margin: auto;">
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/y0vm_fjkejo" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
</div>
</section>
<section id="braintrust" class="level2">
<h2 class="anchored" data-anchor-id="braintrust">Braintrust</h2>
<p>With <a href="https://x.com/waydegilliam">Wayde Gilliam</a>, former developer relations at Braintrust.</p>
<div style="width: 70%; margin: auto;">
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/97iykOemOn4" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
</div>
</section>
<section id="arize-phoenix" class="level2">
<h2 class="anchored" data-anchor-id="arize-phoenix">Arize Phoenix</h2>
<p>With <a href="https://www.linkedin.com/in/sallyann-delucia-59a381172/">SallyAnn DeLucia</a>, Technical AI Product Leader at Arize.</p>
<div style="width: 70%; margin: auto;">
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/wcYnzHJlUR0" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
</div>
<hr>
</section>
<section id="criteria-for-assessing-ai-evals-tools" class="level2">
<h2 class="anchored" data-anchor-id="criteria-for-assessing-ai-evals-tools">Criteria for Assessing AI Evals Tools</h2>
<p>Here are themes that consistently surfaced during our review.</p>
<section id="workflow-and-developer-experience" class="level3">
<h3 class="anchored" data-anchor-id="workflow-and-developer-experience">1. Workflow and Developer Experience</h3>
<p>Reducing friction is more important than any single feature. Concretely, you should be mindful of the time it takes to go from observing a failure to iterating on a solution. For example, we appreciated the ability to go from viewing a single trace to experimenting with that same trace in a playground. For some teams with data-science backgrounds, a notebook-centric workflow is ideal as it provides transparency and control. This happens to be my preferred workflow as well.</p>
<p>When considering a notebook-centric workflow, its important to pay attention to the ergonmics of the sdk. This often boils down to the quality of the documentation and integration with existing data tools.</p>
</section>
<section id="human-in-the-loop-support" class="level3">
<h3 class="anchored" data-anchor-id="human-in-the-loop-support">2. Human-in-the-Loop Support</h3>
<p>The best tools don’t try to automate away the human; they empower them. Since error analysis is the highest ROI activity in AI engineering, a tool’s ability to support efficient human review is paramount. Prioritize tools with first-class support for manual annotation and error analysis. As of this writing, one thing that is missing from many tools is <a href="../../../blog/posts/evals-faq/index.html#q-why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed" target="_blank">axial coding</a>.</p>
</section>
<section id="transparency-and-control-vs.-magic" class="level3">
<h3 class="anchored" data-anchor-id="transparency-and-control-vs.-magic">3. Transparency and Control vs.&nbsp;“Magic”</h3>
<p>Be deeply skeptical of features that promise full automation without human validation, as these can create a powerful and dangerous illusion of confidence. For example, be wary of features where an AI agent both creates an evaluation rubric and then immediately scores the outputs. This “stacking of abstractions” often hides flaws behind a high score. Favor tools that give you control and visibility.</p>
</section>
<section id="ecosystem-integration-vs.-walled-gardens" class="level3">
<h3 class="anchored" data-anchor-id="ecosystem-integration-vs.-walled-gardens">4. Ecosystem Integration vs.&nbsp;Walled Gardens</h3>
<p>An eval tool should fit your stack, not force you to fit its stack. Assess how well a tool integrates with your existing technologies. Also, beware of proprietary DSLs as they can add friction. Finally, the ability to export data into common formats for analysis in a variety of environments is a must-have.</p>
</section>
<section id="conclusion" class="level3">
<h3 class="anchored" data-anchor-id="conclusion">Conclusion</h3>
<p>The right choice of tool depends on your team’s workflow, skillset, and specific needs. I hope seeing how our panel approached this evaluation provides a better framework for making your own decision.</p>
<p>As for me personally, I tend to use these tools as a backend data store and use Jupyter notebooks as well as my own <a href="../../../blog/posts/evals-faq/index.html#q-should-i-build-a-custom-annotation-tool-or-use-something-off-the-shelf" target="_blank">custom built</a> annotation interfaces for most of my needs.</p>
<hr>
<div class="callout callout-style-default callout-caution callout-titled">
<div class="callout-header d-flex align-content-center" data-bs-toggle="collapse" data-bs-target=".callout-1-contents" aria-controls="callout-1" aria-expanded="true" aria-label="Toggle callout">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Caution</span>Appendix: Vendor Snapshots (As of Mid-2025)
</div>
<div class="callout-btn-toggle d-inline-block border-0 py-1 ps-1 pe-0 float-end"><i class="callout-toggle"></i></div>
</div>
<div id="callout-1" class="callout-1-contents callout-collapse collapse show">
<div class="callout-body-container callout-body">
<p><strong>You should take these notes with a grain of salt. I recommend watch the videos above to get a sense of how we applied these criteria and where you might differ according to your neeeds.</strong></p>
<section id="langsmith-evaluation-notes" class="level3">
<h3 class="anchored" data-anchor-id="langsmith-evaluation-notes">Langsmith Evaluation Notes</h3>
<p><strong>Overall Sentiment</strong> The overall workflow is intuitive, especially for those new to formal evaluation processes. The UI guides you through creating datasets, running experiments, and annotating results.</p>
<p><strong>Positive Feedback / What We Liked</strong></p>
<ul>
<li><strong>Seamless Workflow from Trace to Playground:</strong> The transition from inspecting a trace to experimenting with it in the playground is very smooth.</li>
<li><strong>AI-Assisted Prompt Improvement:</strong> The “Prompt Canvas” feature is a powerful tool for prompt engineering.</li>
<li><strong>Dataset Creation and Management:</strong> You can easily create datasets by uploading files, and the schema detection helps structure the data correctly.</li>
<li><strong>Experimentation and Evaluation:</strong> The “Annotation Queue” is a dedicated interface for human review and labeling of traces, which is more efficient than using spreadsheets.</li>
</ul>
<p><strong>Critiques and Areas for Improvement</strong></p>
<ul>
<li><strong>Limited Side-by-Side Comparison:</strong> The UI doesn’t make it easy to see side-by-side comparisons of different prompt versions and their outputs.</li>
<li><strong>UI/UX Concerns:</strong> The UI can feel a bit cluttered, with a lot of options and information presented at once.</li>
<li><strong>Potential for Over-Automation:</strong> Features like AI-generated examples, while convenient, can lead to homogenous data.</li>
</ul>
</section>
<section id="braintrust-evaluation-notes" class="level3">
<h3 class="anchored" data-anchor-id="braintrust-evaluation-notes">Braintrust Evaluation Notes</h3>
<p><strong>Overall Sentiment</strong> The panel had a generally positive view of Braintrust, highlighting its clean UI and structured approach to evaluations. The tool’s emphasis on human-in-the-loop workflows was a significant strength.</p>
<p><strong>Positive Feedback / What We Liked</strong></p>
<ul>
<li><strong>Focus on a Structured Evals Process:</strong> The demonstration emphasized a solid, methodical approach, starting by involving subject-matter experts to create an initial dataset.</li>
<li><strong>Clean and Intuitive User Interface (UI):</strong> The panel found the UI to be clean and easier to navigate than other tools, with a particularly readable trace viewing screen.</li>
<li><strong>Strong Support for Human-in-the-Loop Workflows:</strong> The platform has dedicated UIs designed for human review and annotation, which is critical for creating high-quality datasets and performing error analysis.</li>
<li><strong>The “Money Table”:</strong> After annotating traces with failure modes, the final dataset view is an actionable output that allows teams to quickly sort, filter, and quantify the most common failure modes.</li>
</ul>
<p><strong>Critiques and Areas for Improvement</strong></p>
<ul>
<li><strong>The “Loop” AI Scorer:</strong> The most significant concern was the “Loop” feature, an AI agent that creates an evaluation rubric and then immediately scores the outputs, which could lead to a false sense of security.</li>
<li><strong>Reliance on a Proprietary Query Language (BTQL):</strong> The panel viewed the use of “BTQL” with mild skepticism, stating a preference for exporting data to a Jupyter notebook.</li>
<li><strong>Clunky Data Workflows:</strong> The process for generating and refining synthetic data seemed inefficient, requiring downloading and re-uploading data between steps.</li>
</ul>
</section>
<section id="arize-phoenix-evaluation-notes" class="level3">
<h3 class="anchored" data-anchor-id="arize-phoenix-evaluation-notes">Arize Phoenix Evaluation Notes</h3>
<p><strong>Overall Sentiment</strong> The panel had a generally positive view of Phoenix, with one panelist calling it one of his “favorite open source eval tools.” The tool is positioned as a developer-first, notebook-centric platform.</p>
<p><strong>Positive Feedback / What We Liked</strong></p>
<ul>
<li><strong>Notebook-Centric Workflow:</strong> The entire evaluation process was driven from a Jupyter notebook, giving the developer transparency and control. The ability to export annotated data back into a Pandas DataFrame was a powerful feature.</li>
<li><strong>UI &amp; Developer Experience:</strong> The prompt management UI was praised for being clear and easy to understand. The tight integration between traces and the “Playground” was also noted as a smooth workflow.</li>
<li><strong>Open Source &amp; Local-First Approach:</strong> Phoenix can be run entirely locally, providing a sense of control and transparency. As an open-source tool, it was noted for being “hackable.”</li>
</ul>
<p><strong>Critiques and Areas for Improvement</strong></p>
<ul>
<li><strong>UI Readability:</strong> The text in the output panes was difficult to read during the demonstration, with a possible lack of markdown rendering for model outputs.</li>
<li><strong>Metrics and Visualization:</strong> The tool displays point statistics for each run, but the panel found this of limited use and expressed a desire for aggregate visualizations like histograms to identify outliers.</li>
<li><strong>Prompt Management and Testing:</strong> The prompt editor treats the system prompt as one large, monolithic block of text. A more component-based approach where individual instructions could be toggled on and off (“ablated”) would be preferable for systematic testing.</li>
</ul>
</section>
</div>
</div>
</div>


</section>
</section>

 ]]></description>
  <category>AI</category>
  <category>Evals</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-tools/</guid>
  <pubDate>Wed, 01 Oct 2025 07:00:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/eval-tools/cover-img-2.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: Are similarity metrics (BERTScore, ROUGE, etc.) useful for evaluating LLM outputs?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/are-similarity-metrics-bertscore-rouge-etc-useful-for-evaluating-llm-outputs.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Generic metrics like BERTScore, ROUGE, cosine similarity, etc. are not useful for evaluating LLM outputs in most AI applications. Instead, we recommend using <a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" target="_blank">error analysis</a> to identify metrics specific to your application’s behavior. We recommend designing <a href="../../../blog/posts/evals-faq/why-do-you-recommend-binary-passfail-evaluations-instead-of-1-5-ratings-likert-scales.html" target="_blank">binary pass/fail</a>.) evals (using LLM-as-judge) or code-based assertions.</p>
<p>As an example, consider a real estate CRM assistant. Suggesting showings that aren’t available (can be tested with an assertion) or confusing client personas (can be tested with a LLM-as-judge) is problematic . Generic metrics like similarity or verbosity won’t catch this. A relevant quote from the course:</p>
<blockquote class="blockquote">
<p>“The abuse of generic metrics is endemic. Many eval vendors promote off the shelf metrics, which ensnare engineers into superfluous tasks.”</p>
</blockquote>
<p>Similarity metrics aren’t always useless. They have utility in domains like search and recommendation (and therefore can be useful for <a href="../../../blog/posts/evals-faq/how-should-i-approach-evaluating-my-rag-system.html" target="_blank">optimizing and debugging retrieval</a> for RAG). For example, cosine similarity between embeddings can measure semantic closeness in retrieval systems, and average pairwise similarity can assess output diversity (where lower similarity indicates higher diversity).</p>
<p><a href="../../../blog/posts/evals-faq/#q-are-similarity-metrics-bertscore-rouge-etc-useful-for-evaluating-llm-outputs" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/are-similarity-metrics-bertscore-rouge-etc-useful-for-evaluating-llm-outputs.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: Can my evaluators also be used to automatically fix or correct outputs in production?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Yes, but only a specific subset of them. This is the distinction between an <strong>evaluator</strong> and a <strong>guardrail</strong> that we <a href="../../../blog/posts/evals-faq/whats-the-difference-between-guardrails-evaluators.html" target="_blank">previously discussed</a>. As a reminder:</p>
<ul>
<li><strong>Evaluators</strong> typically run <em>asynchronously</em> after a response has been generated. They measure quality but don’t interfere with the user’s immediate experience.<br>
</li>
<li><strong>Guardrails</strong> run <em>synchronously</em> in the critical path of the request, before the output is shown to the user. Their job is to prevent high-impact failures in real-time.</li>
</ul>
<p>There are two important decision criteria for deciding whether to use an evaluator as a guardrail:</p>
<ol type="1">
<li><p><strong>Latency &amp; Cost</strong>: Can the evaluator run fast enough and cheaply enough in the critical request path without degrading user experience?</p></li>
<li><p><strong>Error Rate Trade-offs</strong>: What’s the cost-benefit balance between false positives (blocking good outputs and frustrating users) versus false negatives (letting bad outputs reach users and causing harm)? In high-stakes domains like medical advice, false negatives may be more costly than false positives. In creative applications, false positives that block legitimate creativity may be more harmful than occasional quality issues.</p></li>
</ol>
<p>Most guardrails are designed to be <strong>fast</strong> (to avoid harming user experience) and have a <strong>very low false positive rate</strong> (to avoid blocking valid responses). For this reason, you would almost never use a slow or non-deterministic LLM-as-Judge as a synchronous guardrail. However, these tradeoffs might be different for your use case.</p>
<p><a href="../../../blog/posts/evals-faq/#q-can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/can-my-evaluators-also-be-used-to-automatically-fix-or-correct-outputs-in-production.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How are evaluations used differently in CI/CD vs. monitoring production?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-are-evaluations-used-differently-in-cicd-vs-monitoring-production.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>The most important difference between CI vs.&nbsp;production evaluation is the data used for testing.</p>
<p>Test datasets for CI are small (in many cases 100+ examples) and purpose-built. Examples cover core features, regression tests for past bugs, and known edge cases. Since CI tests are run frequently, the cost of each test has to be carefully considered (that’s why you carefully curate the dataset). Favor assertions or other deterministic checks over LLM-as-judge evaluators.</p>
<p>For evaluating production traffic, you can sample live traces and run evaluators against them asynchronously. Since you usually lack reference outputs on production data, you might rely more on on more expensive reference-free evaluators like LLM-as-judge. Additionally, track confidence intervals for production metrics. If the lower bound crosses your threshold, investigate further.</p>
<p>These two systems are complementary: when production monitoring reveals new failure patterns through error analysis and evals, add representative examples to your CI dataset. This mitigates regressions on new issues.</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-are-evaluations-used-differently-in-cicd-vs-monitoring-production" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-are-evaluations-used-differently-in-cicd-vs-monitoring-production.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How can I efficiently sample production traces for review?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-can-i-efficiently-sample-production-traces-for-review.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>It can be cumbersome to review traces randomly, especially when most traces don’t have an error. These sampling strategies help you find traces more likely to reveal problems:</p>
<ul>
<li><strong>Outlier detection:</strong> Sort by any metric (response length, latency, tool calls) and review extremes.</li>
<li><strong>User feedback signals:</strong> Prioritize traces with negative feedback, support tickets, or escalations.</li>
<li><strong>Metric-based sorting:</strong> Generic metrics can serve as exploration signals to find interesting traces. Review both high and low scores and treat them as exploration clues. Based on what you learn, you can build custom evaluators for the failure modes you find.</li>
<li><strong>Stratified sampling:</strong> Group traces by key dimensions (user type, feature, query category) and sample from each group.</li>
<li><strong>Embedding clustering:</strong> Generate embeddings of queries and cluster them to reveal natural groupings. Sample proportionally from each cluster, but oversample small clusters for edge cases. There’s no right answer for clustering—it’s an exploration technique to surface patterns you might miss manually.</li>
</ul>
<p>As you get more sophisticated with how you sample, you can incorporate these tactics into the design of your <a href="../../../blog/posts/evals-faq/what-makes-a-good-custom-interface-for-reviewing-llm-outputs.html" target="_blank">annotation tools</a>.</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-can-i-efficiently-sample-production-traces-for-review" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-can-i-efficiently-sample-production-traces-for-review.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How do I approach evaluation when my system handles diverse user queries?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<blockquote class="blockquote">
<p>Complex applications often support vastly different query patterns—from “What’s the return policy?” to “Compare pricing trends across regions for products matching these criteria.” Each query type exercises different system capabilities, leading to confusion on how to design eval criteria.</p>
</blockquote>
<p><strong><em><a href="https://youtu.be/e2i6JbU2R-s?si=8p5XVxbBiioz69Xc" target="_blank">Error Analysis</a> is all you need.</em></strong> Your evaluation strategy should emerge from observed failure patterns (e.g.&nbsp;error analysis), not predetermined query classifications. Rather than creating a massive evaluation matrix covering every query type you can imagine, let your system’s actual behavior guide where you invest evaluation effort.</p>
<p>During error analysis, you’ll likely discover that certain query categories share failure patterns. For instance, all queries requiring temporal reasoning might struggle regardless of whether they’re simple lookups or complex aggregations. Similarly, queries that need to combine information from multiple sources might fail in consistent ways. These patterns discovered through error analysis should drive your evaluation priorities. It could be that query category is a fine way to group failures, but you don’t know that until you’ve analyzed your data.</p>
<p>To see an example of basic error analysis in action, <a href="https://youtu.be/e2i6JbU2R-s?si=8p5XVxbBiioz69Xc" target="_blank">see this video</a>.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/e2i6JbU2R-s" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><a href="../../../blog/posts/evals-faq/#q-how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-approach-evaluation-when-my-system-handles-diverse-user-queries.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How do I choose the right chunk size for my document processing tasks?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Unlike RAG, where chunks are optimized for retrieval, document processing assumes the model will see every chunk. The goal is to split text so the model can reason effectively without being overwhelmed. Even if a document fits within the context window, it might be better to break it up. Long inputs can degrade performance due to attention bottlenecks, especially in the middle of the context. Two task types require different strategies:</p>
<section id="fixed-output-tasks-large-chunks" class="level3">
<h3 class="anchored" data-anchor-id="fixed-output-tasks-large-chunks">1. Fixed-Output Tasks → Large Chunks</h3>
<p>These are tasks where the output length doesn’t grow with input: extracting a number, answering a specific question, classifying a section. For example:</p>
<ul>
<li>“What’s the penalty clause in this contract?”</li>
<li>“What was the CEO’s salary in 2023?”</li>
</ul>
<p>Use the largest chunk (with caveats) that likely contains the answer. This reduces the number of queries and avoids context fragmentation. However, avoid adding irrelevant text. Models are sensitive to distraction, especially with large inputs. The middle parts of a long input might be under-attended. Furthermore, if cost and latency are a bottleneck, you should consider preprocessing or filtering the document (via keyword search or a lightweight retriever) to isolate relevant sections before feeding a huge chunk.</p>
</section>
<section id="expansive-output-tasks-smaller-chunks" class="level3">
<h3 class="anchored" data-anchor-id="expansive-output-tasks-smaller-chunks">2. Expansive-Output Tasks → Smaller Chunks</h3>
<p>These include summarization, exhaustive extraction, or any task where output grows with input. For example:</p>
<ul>
<li>“Summarize each section”</li>
<li>“List all customer complaints”</li>
</ul>
<p>In these cases, smaller chunks help preserve reasoning quality and output completeness. The standard approach is to process each chunk independently, then aggregate results (e.g., map-reduce). When sizing your chunks, try to respect content boundaries like paragraphs, sections, or chapters. Chunking also helps mitigate output limits. By breaking the task into pieces, each piece’s output can stay within limits.</p>
</section>
<section id="general-guidance" class="level3">
<h3 class="anchored" data-anchor-id="general-guidance">General Guidance</h3>
<p>It’s important to recognize <strong>why chunk size affects results</strong>. A larger chunk means the model has to reason over more information in one go – essentially, a heavier cognitive load. LLMs have limited capacity to <strong>retain and correlate details across a long text</strong>. If too much is packed in, the model might prioritize certain parts (commonly the beginning or end) and overlook or “forget” details in the middle. This can lead to overly coarse summaries or missed facts. In contrast, a smaller chunk bounds the problem: the model can pay full attention to that section. You are trading off <strong>global context for local focus</strong>.</p>
<p>No rule of thumb can perfectly determine the best chunk size for your use case – <strong>you should validate with experiments</strong>. The optimal chunk size can vary by domain and model. I treat chunk size as a hyperparameter to tune.</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>


</section>

 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-choose-the-right-chunk-size-for-my-document-processing-tasks.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How do I debug multi-turn conversation traces?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-debug-multi-turn-conversation-traces.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>Start simple. Check if the whole conversation met the user’s goal with a pass/fail judgment. Look at the entire trace and focus on the first upstream failure. Read the user-visible parts first to understand if something went wrong. Only then dig into the technical details like tool calls and intermediate steps.</p>
<section id="multi-agent-trace-logging" class="level3">
<h3 class="anchored" data-anchor-id="multi-agent-trace-logging">Multi-agent trace logging</h3>
<p>For multi-agent flows, assign a session or trace ID to each user request and log every message with its source (which agent or tool), trace ID, and position in the sequence. This lets you reconstruct the full path from initial query to final result across all agents.</p>
</section>
<section id="annotation-strategy" class="level3">
<h3 class="anchored" data-anchor-id="annotation-strategy">Annotation strategy</h3>
<p>Annotate only the first failure in the trace initially—don’t worry about downstream failures since these often cascade from the first issue. Fixing upstream failures often resolves dependent downstream failures automatically. As you gain experience, you can annotate independent failure modes within the same trace to speed up overall error analysis.</p>
</section>
<section id="simplify-when-possible" class="level3">
<h3 class="anchored" data-anchor-id="simplify-when-possible">Simplify when possible</h3>
<p>When you find a failure, reproduce it with the simplest possible test case. Here’s an example: suppose a shopping bot gives the wrong return policy on turn 4 of a conversation. Before diving into the full multi-turn complexity, simplify it to a single turn: “What is the return window for product X1000?” If it still fails, you’ve proven the error isn’t about conversation context - it’s likely a basic retrieval or knowledge issue you can debug more easily.</p>
</section>
<section id="test-case-generation" class="level3">
<h3 class="anchored" data-anchor-id="test-case-generation">Test case generation</h3>
<p>You have two main approaches. First, simulate users with another LLM to create realistic multi-turn conversations. Second, use “N-1 testing” where you provide the first N-1 turns of a real conversation and test what happens next. The N-1 approach often works better since it uses actual conversation prefixes rather than fully synthetic interactions, but is less flexible.</p>
<p>The key is balancing thoroughness with efficiency. Not every multi-turn failure requires multi-turn analysis.</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-do-i-debug-multi-turn-conversation-traces" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>


</section>

 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-debug-multi-turn-conversation-traces.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How do I evaluate agentic workflows?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-evaluate-agentic-workflows.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>We recommend evaluating agentic workflows in two phases:</p>
<p><strong>1. End-to-end task success.</strong> Treat the agent as a black box and ask “did we meet the user’s goal?”. Define a precise success rule per task (exact answer, correct side-effect, etc.) and measure with human or <a href="https://hamel.dev/blog/posts/llm-judge/" target="_blank">aligned LLM judges</a>. Take note of the first upstream failure when conducting <a href="../../../blog/posts/evals-faq/why-is-error-analysis-so-important-in-llm-evals-and-how-is-it-performed.html" target="_blank">error analysis</a>.</p>
<p>Once error analysis reveals which workflows fail most often, move to step-level diagnostics to understand why they’re failing.</p>
<p><strong>2. Step-level diagnostics.</strong> Assuming that you have sufficiently <a href="https://hamel.dev/blog/posts/evals/#logging-traces" target="_blank">instrumented your system</a> with details of tool calls and responses, you can score individual components such as: - <em>Tool choice</em>: was the selected tool appropriate? - <em>Parameter extraction</em>: were inputs complete and well-formed? - <em>Error handling</em>: did the agent recover from empty results or API failures? - <em>Context retention</em>: did it preserve earlier constraints? - <em>Efficiency</em>: how many steps, seconds, and tokens were spent? - <em>Goal checkpoints</em>: for long workflows verify key milestones.</p>
<p>Example: “Find Berkeley homes under $1M and schedule viewings” breaks into: parameters extracted correctly, relevant listings retrieved, availability checked, and calendar invites sent. Each checkpoint can pass or fail independently, making debugging tractable.</p>
<p><strong>Use transition failure matrices to understand error patterns.</strong> Create a matrix where rows represent the last successful state and columns represent where the first failure occurred. This is a great way to understand where the most failures occur.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/shreya_matrix.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>Transition failure matrix showing hotspots in text-to-SQL agent workflow</figcaption>
</figure>
</div>
<p>Transition matrices transform overwhelming agent complexity into actionable insights. Instead of drowning in individual trace reviews, you can immediately see that GenSQL → ExecSQL transitions cause 12 failures while DecideTool → PlanCal causes only 2. This data-driven approach guides where to invest debugging effort. Here is another <a href="https://www.figma.com/deck/nwRlh5renu4s4olaCsf9lG/Failure-is-a-Funnel?node-id=2009-927&amp;t=GJlTtxQ8bLJaQ92A-1" target="_blank">example</a> from Bryan Bischof, that is also a text-to-SQL agent:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/bischof_matrix.png" class="img-fluid figure-img" style="width:75.0%" target="_blank"></p>
<figcaption>Bischof, Bryan “Failure is A Funnel - Data Council, 2025”</figcaption>
</figure>
</div>
<p>In this example, Bryan shows variation in transition matrices across experiments. How you organize your transition matrix depends on the specifics of your application. For example, Bryan’s text-to-SQL agent has an inherent sequential workflow which he exploits for further analytical insight. You can watch his <a href="https://youtu.be/R_HnI9oTv3c?si=hRRhDiydHU5k6ikc" target="_blank">full talk</a> for more details.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/R_HnI9oTv3c" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p><strong>Creating Test Cases for Agent Failures</strong></p>
<p>Creating test cases for agent failures follows the same principles as our previous FAQ on <a href="../../../blog/posts/evals-faq/how-do-i-debug-multi-turn-conversation-traces.html" target="_blank">debugging multi-turn conversation traces</a> (i.e.&nbsp;try to reproduce the error in the simplest way possible, only use multi-turn tests when the failure actually requires conversation context, etc.).</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-do-i-evaluate-agentic-workflows" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-do-i-evaluate-agentic-workflows.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
<item>
  <title>Q: How many people should annotate my LLM outputs?</title>
  <link>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html</link>
  <description><![CDATA[ 

<!-- Content inserted at the beginning of body tag -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PKGWQMKL" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
<script async="" data-uid="df8af2b6ed" src="https://hamel.kit.com/df8af2b6ed/index.js"></script>




<p>For most small to medium-sized companies, appointing a single domain expert as a “benevolent dictator” is the most effective approach. This person—whether it’s a psychologist for a mental health chatbot, a lawyer for legal document analysis, or a customer service director for support automation—becomes the definitive voice on quality standards.</p>
<p>A single expert eliminates annotation conflicts and prevents the paralysis that comes from “too many cooks in the kitchen”. The benevolent dictator can incorporate input and feedback from others, but they drive the process. If you feel like you need five subject matter experts to judge a single interaction, it’s a sign your product scope might be too broad.</p>
<p>However, larger organizations or those operating across multiple domains (like a multinational company with different cultural contexts) may need multiple annotators. When you do use multiple people, you’ll need to measure their agreement using metrics like Cohen’s Kappa, which accounts for agreement beyond chance. However, use your judgment. Even in larger companies, a single expert is often enough.</p>
<p>Start with a benevolent dictator whenever feasible. Only add complexity when your domain demands it.</p>
<p><a href="../../../blog/posts/evals-faq/#q-how-many-people-should-annotate-my-llm-outputs" class="faq-back-link" target="_blank">↩︎ Back to main FAQ</a></p>
<hr>
<p><em>This article is part of our AI Evals FAQ, a collection of common questions (and answers) about LLM evaluation. <a href="../../../blog/posts/evals-faq/">View all FAQs</a> or <a href="../../..">return to the homepage</a>.</em></p>



 ]]></description>
  <category>LLMs</category>
  <category>evals</category>
  <category>faq</category>
  <category>faq-individual</category>
  <guid>https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/how-many-people-should-annotate-my-llm-outputs.html</guid>
  <pubDate>Mon, 04 Aug 2025 19:13:00 GMT</pubDate>
  <media:content url="https://glittering-puffpuff-87102e.netlify.app/blog/posts/evals-faq/images/eval_faq.png" medium="image" type="image/png" height="96" width="144"/>
</item>
</channel>
</rss>
