IA Jul 6, 2026 · 8 min read

Evals for LLM features: how I know I didn't break anything

Yohangel Ramos

Yohangel Ramos

Tech Lead · Senior Fullstack Developer

The first time I changed a prompt in production at JXBS, I did it like I'd change anything else: tried it against two or three hand-picked examples, decided it looked better, and shipped. A week later a user reported that matching returned garbage on a case that used to work. I hadn't broken the code: I'd broken the model's behavior, and I had no way of knowing because I wasn't measuring anything. Evals are the piece that turns "working with LLMs" from superstitious craft into engineering. This article is the minimum viable set of evals I use today on every feature that has a model inside it.

An LLM has no unit tests, and that's the problem

When you write a normal function, a unit test pins down its contract: this goes in, that comes out, and if someone breaks it the pipeline warns you. With an LLM-based feature that contract blurs. The same prompt with the same input can give different answers, and a seemingly harmless change —reordering two sentences of the system prompt, bumping from one model version to the next— can degrade cases you didn't even know depended on that detail.

The mental mistake is treating the prompt as stable code and the model as a fixed dependency. They're neither. The prompt is a parameter you tune constantly, and the model is a dependency that shifts under you without warning. Without a battery of cases you run on every change, you're shipping blind and discovering regressions through your users. Which is the worst way to discover them.

The minimum eval: a golden dataset and a metric you care about

You don't need an eval platform to start. You need a file with representative cases and a function that scores. A case is a real input, the expected output (or a property the output must satisfy) and optionally why it's there. I pull them from three places: examples I know work, edge cases that worry me, and above all the real bugs people report —every production regression becomes an eval case so it can't happen again.

type EvalCase = {
  name: string;
  input: MatchInput;
  // An assertion about the output, not exact equality:
  // with LLMs, comparing strings word by word is useless.
  expect: (output: MatchOutput) => boolean;
};

const cases: EvalCase[] = [
  {
    name: 'senior candidate with exact stack ranks first',
    input: seniorReactExact,
    expect: (out) => out.ranked[0].id === 'cand_42',
  },
  {
    name: 'does not invent skills absent from the CV',
    input: cvWithoutKubernetes,
    expect: (out) => !out.ranked[0].reasons.includes('Kubernetes'),
  },
];

Notice I don't compare the whole output character by character. With an LLM that's doomed to fail over irrelevant wording differences. What I check are properties: that the right candidate lands on top, that no hallucinated skill shows up, that the format is parseable. Each assertion encodes something I genuinely care about in the behavior, not the exact shape of the text.

Scoring the subjective: when I use an LLM as a judge

Many outputs can't be verified with an if. "Is this explanation of the match clear and well-grounded?" is not an obvious boolean property. For that I use a second LLM as an evaluator: I pass it the input, the output and a concrete rubric, and ask for a score with justification. It works surprisingly well for catching large degradations, and it's far cheaper than reviewing hundreds of outputs by hand.

But I chose to use LLM-as-judge with my eyes open, not as a silver bullet. It has known biases: it tends to reward long answers, favors style over substance, and if you ask it to score its own model it turns lenient. I treat it as a noisy filter, not the truth: it's good for catching big quality drops between versions, not for telling whether something went from 8.2 to 8.4. For the properties I can verify with code, I use code, which is deterministic and free.

💡 Every LLM bug that reaches production is an eval case you were missing. Before fixing the prompt, add the failing case to your dataset. That way the fix is locked in and that specific regression can never come back.

Running evals where it hurts: before you deploy

An eval dataset you run when you remember is worthless. The value shows up when it runs automatically on every change that touches the prompt, the model or the orchestration logic. I wired it into the pipeline as one more step: if the pass rate over the dataset falls below a threshold, the deploy stops just as it would if a test failed.

async function runEvals(cases: EvalCase[]) {
  const results = await Promise.all(
    cases.map(async (c) => ({
      name: c.name,
      passed: c.expect(await runFeature(c.input)),
    })),
  );

  const passed = results.filter((r) => r.passed).length;
  const rate = passed / results.length;

  console.table(results);
  if (rate < 0.9) {
    throw new Error(`Evals below threshold: ${rate}`);
  }
}

The threshold isn't 100% on purpose. With LLMs there are genuinely ambiguous cases where an occasional miss is tolerable, and demanding a perfect green pushes you to overfit the prompt to your dataset. I prefer a high but realistic threshold and I watch the trend: if the rate has been dropping for three versions, there's a problem even if each version cleared the bar.

Where I put the effort today

If I had to give a single piece of advice to someone shipping their first LLM into a product, it would be this: build the evals before polishing the prompt, not after. It's tempting to spend the time on the creative part of writing clever instructions, but without a way to measure you're optimizing blind. I chose to invest in the evaluation harness before the perfect prompt because the harness pays for itself on the first regression it catches, and its value grows with every case you add. I'll rewrite the prompt ten times; the evals tell me which of those ten versions is better.

Yohangel Ramos

Written by Yohangel Ramos

Senior Fullstack Developer and Tech Lead. I build with React, Next.js, Nest.js and AWS — and I write about what I learn along the way.

Let's talk →

Keep reading

IA

AI-built startups in 2026: the real numbers behind the hype

IA

How to survive as a programmer in the AI era (without turning cynical or naive)