BXUP Track progress & test yourself — free
Software Engineering · Playbook · Advanced · ~5 min read
SOFTWARE ENGINEERING · PLAYBOOK · ADVANCED

Eval Harness as a CI Gate: Golden Datasets, Assertions, and LLM-as-Judge

"How do you evaluate it?" is the most-cited differentiator in applied-LLM hiring, and for a concrete reason: without an eval gate, every prompt tweak and model upgrade ships on vibes, and regressions surface as customer complaints.

Tier: Advanced · Workflow area: Building LLM Features

This playbook builds the production norm — a golden dataset plus layered assertions that run like a test suite, wired into CI so a broken prompt change fails the pipeline before deploy, demonstrated red and green.

When to use

  • Any LLM feature from pb-44/46/47/49 is in front of users and changes to prompts, retrieval, or models still ship unreviewed by anything automated.
  • A model deprecation forces a migration and you need to prove the replacement is not worse.
  • Two engineers disagree about whether the new prompt is better — permanently end that class of argument.

Prerequisites

  • A deployed LLM feature with logged inputs/outputs (pb-44's logging).
  • pip install pytest anthropic — or npx promptfoo@latest init for the config-driven route.
  • 2–3 hours to curate the golden dataset. This is the actual work; the harness is plumbing.

The workflow

  1. Curate a 30-case golden dataset from reality. Pull from production logs (pb-51's traces make this trivial), not imagination. Composition that catches regressions: ~15 representative cases, ~8 known past failures (the bugs you already fixed — your regression suite), ~7 edge/adversarial (empty input, wrong language, injection attempts, off-topic). Each case: input, expected (exact output, key facts, or a rubric), and assert_type. Store as JSONL in the repo, versioned with the prompts it tests.

  2. Layer assertions cheapest-first. Deterministic checks catch most regressions for free; the judge handles what code cannot:

    • Tier 1 — format: parses as schema / valid JSON, required fields present, length bounds, refusal marker present where expected.
    • Tier 2 — content: must-contain / must-not-contain strings, cited passage IDs exist in retrieved set (pb-48), SQL result matches known-good (pb-49).
    • Tier 3 — LLM-as-judge: graded criteria (groundedness, tone, policy) only where tiers 1–2 cannot decide.
  3. Write the harness as a pytest suite — it is just tests.

    import json, pytest, anthropic
    from app.llm import triage          # the function under test (pb-44)
    
    CASES = [json.loads(l) for l in open("evals/golden.jsonl")]
    client = anthropic.Anthropic(timeout=30.0)
    
    @pytest.mark.parametrize("case", CASES, ids=lambda c: c["id"])
    def test_golden(case):
        out = triage(case["input"])
        if case["assert_type"] == "exact":
            assert out.category == case["expected"]["category"]
        elif case["assert_type"] == "contains":
            assert all(s in out.summary for s in case["expected"]["must_contain"])
        elif case["assert_type"] == "judge":
            assert judge(case, out) >= 4
    
    def judge(case, out) -> int:
        resp = client.messages.parse(
            model="claude-opus-4-8", max_tokens=512,
            system="Score the response 1-5 against the rubric. Justify, then score. "
                   "A 4 means: meets every rubric criterion with minor style issues only.",
            messages=[{"role": "user", "content":
                       f"Rubric: {case['rubric']}\nInput: {case['input']}\n"
                       f"Response: {out.model_dump_json()}"}],
            output_format=JudgeScore)          # {justification: str, score: int}
        return resp.parsed_output.score
    
  4. Calibrate the judge before trusting it. Hand-score 15 outputs yourself, run the judge on the same 15, and compare. Agreement below ~85% means the rubric is vague — tighten it with explicit criteria and an anchored example of a 3 vs a 5. An uncalibrated judge is noise with confidence. Re-calibrate whenever you change the judge's model or rubric.

  5. Wire it into CI with a threshold, not perfection.

    # .github/workflows/evals.yml
    on:
      pull_request:
        paths: ["app/prompts/**", "app/llm/**", "evals/**"]
    jobs:
      evals:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: pip install -r requirements.txt
          - run: pytest evals/ --tb=short -q
            env:
              ANTHROPIC_API_KEY: ${{ secrets.EVAL_API_KEY }}
    

    Policy: tier-1/2 failures block merge outright; judge-tier score drops >10% vs the base branch block; small judge wobbles warn. Path-scoping keeps eval cost off unrelated PRs.

  6. Prove red, then green. Deliberately break the prompt (delete the refusal instruction, or swap the schema) in a branch: CI must fail with a readable diff of which cases broke. Revert: green. Do this once in front of the team — it is the demo that makes evals-before-merge a norm instead of your hobby.

  7. Feed the dataset from production forever. Every user-reported bad output becomes a labeled case in the next PR. The dataset's growth rate is the health metric of your eval culture.

Key Takeaway:

The golden dataset is a spec that executes. Prompt engineering without it is refactoring without tests — you can do it, but you cannot know what you broke.

Tool variants

  • pytest harness (shown): zero new tools, lives beside your unit tests, maximum flexibility.
  • promptfoo (OSS): YAML-declared cases/assertions, side-by-side model comparison, built-in CI action — fastest route if your team prefers config to code.
  • Braintrust / Ragas: hosted experiment tracking and RAG-specific metrics (faithfulness, context precision) — Ragas pairs naturally with pb-47/48.
  • OpenAI-side judge:client.responses.parse(model="gpt-5.6", ..., text_format=JudgeScore) — some teams deliberately judge with a different provider than they generate with, to decorrelate errors.

Verification

  • The red/green demonstration is recorded in the PR history: one deliberately broken prompt caught, one fix passing.
  • Judge calibration ≥85% agreement with your hand scores, documented in the repo.
  • Full suite runs in <10 minutes and <$1 per CI run (cache retrieval-heavy fixtures; judge only tier-3 cases).
  • A real regression from the last quarter, replayed against the suite, is caught by at least one case — if not, add the case.

Pitfalls

  • Synthetic-only datasets. Invented cases test the failure modes you already imagined. Production logs contain the ones you didn't.
  • Judging everything with the judge. LLM-judging cases a string assertion could decide adds cost and flake. Cheapest tier that can decide, always.
  • A judge with no rubric anchors. "Rate quality 1–5" produces drift; per-criterion rubrics with anchored examples produce agreement.
  • 100% pass-rate worship. LLM outputs are nondeterministic at the margins; a threshold with trend tracking beats a brittle all-green requirement that teaches people to delete hard cases.
  • Letting the dataset go stale. An eval suite frozen at launch tests last year's product. Tie dataset additions to your incident/feedback process.

Restricted-environment variant

  • Client tenancyrun evals against Bedrock/Azure endpoints with in-tenant CI runners; golden datasets built from client data are client data — store them in the client's repo, not yours.
  • Local/cost-zerotier-1/2 assertions run against an Ollama-served model for structural regression checks on every commit; run the paid judge tier nightly instead of per-PR.
  • Production logs cannot leave the boundarycurate the dataset inside it and export only aggregate scores.

Success criteria

  • No prompt, retrieval, or model change merges without the suite passing — enforced by CI, not etiquette.
  • A demonstrated catch: at least one real regression stopped at PR time since adoption.
  • Calibrated judge, layered assertions, suite cost and runtime known and bounded.
  • The golden dataset grows monthly from production, and model migrations are now a measured comparison instead of a leap.
This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in