AI Agents · Ai

Evaluating LLM Apps — A Complete Guide

Evaluating LLM Apps — A Complete Guide is the work that defines the next phase of enterprise software. ERP systems hold the most valuable business data in the company — customers,.

John Kihiu12 min read

Shipping an LLM feature without an eval harness is like shipping a payment integration without tests — it works in the demo and then quietly regresses the first time someone tweaks the prompt or the vendor rotates a model version underneath you. Evaluation for LLM apps splits into two distinct problems: catching regressions before you ship (offline) and knowing what's actually happening once real users hit it (online). Most teams build neither until something embarrassing happens in production.

Building a golden dataset

Start with 50-100 real examples, not synthetic ones — pull actual user queries, actual documents, actual edge cases from support tickets or logs, and hand-label the expected output. A golden set built from imagined inputs tends to be easier than production traffic, which means it stops catching regressions right around the time you need it most. Keep the set version-controlled next to the code, and grow it every time production surfaces a case the eval set didn't cover — a failure that isn't turned into a test case will recur.

Task-specific metrics beat generic ones

Match the metric to the task instead of reaching for a single "quality score." For structured extraction (invoice fields, entity extraction), use exact match or field-level accuracy against the labeled output — it's cheap, deterministic, and catches silent schema drift immediately. For an agent that calls tools, score tool-call correctness separately from the final answer: did it call the right tool, with the right arguments, in the right order — a wrong answer reached via the right tool calls is a different bug than a right answer reached via the wrong ones, and conflating them hides which one you actually have. For RAG systems, score groundedness/faithfulness — whether the answer's claims are actually supported by the retrieved context — separately from answer relevance, since a fluent, well-cited hallucination and an unsupported-but-correct guess fail your users differently.

LLM-as-judge, and why to distrust it a little

For open-ended outputs where exact match doesn't apply (summary quality, tone, helpfulness), using a second LLM call to score the first model's output is standard practice — but it has known failure modes worth designing around. Judges show position bias (favoring whichever answer appears first in a pairwise comparison), length bias (favoring longer answers regardless of quality), and self-preference bias (a model rating its own family's outputs more favorably). Mitigate by randomizing answer order in pairwise comparisons, using a different model family as judge than the one under test, and periodically spot-checking judge scores against human ratings to confirm they still agree — judge-human correlation drifts as both the judged model and the judge model get updated.

PYTHON · PYTEST-STYLE EVAL CASE
def test_invoice_vendor_extraction(golden_case):
    result = extract_invoice_fields(golden_case.input_pdf_text)
    assert result.vendor_name == golden_case.expected.vendor_name
    assert abs(result.total - golden_case.expected.total) < 0.01

def test_ap_agent_tool_call_sequence(golden_case):
    trace = run_agent(golden_case.prompt)
    calls = [c.tool_name for c in trace.tool_calls]
    assert calls == golden_case.expected.tool_sequence
    # Correct final answer via wrong tool path still fails this case
    assert trace.final_answer == golden_case.expected.answer

def test_rag_groundedness(golden_case, judge_model):
    answer = rag_pipeline(golden_case.query)
    score = judge_model.score_groundedness(answer, answer.sources)
    assert score >= 0.8  # threshold tuned against human-labeled sample

Regression testing on prompt and model changes

Every prompt edit and every model version bump is a deploy — run the full golden set against both before and after, and diff the results, not just the aggregate pass rate. A 2% drop in aggregate score can hide a 100% regression on one important subcategory (say, non-English invoices) that's small enough to wash out in the average. Tools like promptfoo exist specifically for this loop — define test cases and assertions in config, run them against multiple prompt/model variants side by side, and get a diff view rather than rebuilding the harness by hand each time. A custom pytest-style harness works just as well if you already have CI wired up and want evals to run alongside your normal test suite.

Online evaluation: what golden sets can't tell you

Offline evals only cover what you thought to test. Production traffic will find inputs you didn't imagine, so pair offline evals with lightweight online signals: thumbs up/down on responses, a sampled queue where a human reviews some percentage of live interactions weekly, and automatic flagging of low-confidence or tool-error traces for review. Feed anything a reviewer marks bad back into the golden dataset — that's how the offline eval set stays representative instead of stagnating at whatever it looked like on day one.

Track pass rate by segment, not just overall

An aggregate 95% pass rate is meaningless if the 5% failing are all your highest-value customers or a specific document type. Slice eval results by input category, language, or customer tier before deciding a change is safe to ship.

Wrapping up

None of this needs to be elaborate to be useful — a 50-example golden set checked in CI catches most embarrassing regressions, and a weekly human-review sample catches most of what the golden set missed. The mistake is treating evaluation as a one-time pre-launch activity instead of a permanent piece of infrastructure that grows every time production surprises you.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.