Vol. 12 · Evaluation & Improvement
Evaluation Process & LLM-as-Judge
How to build the process of evaluating LLM systems from scratch: minimum viable eval, guardrails vs evaluators, why a binary assessment is better than a scale of 1–5, negative scenarios, LLM-as-judge with correlation to people, detection of degradation after release, synthetic data - when it helps and when it harms.
Eval Process: where to start
Most teams put off evaluation because it seems like expensive infrastructure is needed. No: the minimum working eval is 20–50 examples, pass/fail criteria and one “benevolent dictator” - a person with domain expertise, whose opinion is considered the standard.
Source-complete notesstart antipatterns
start antipatterns
- We are waiting for the “correct” infrastructure
- we immediately build LLM-judge without baseline
- generate a dataset LLM without verification
- 20 manual examples > 1000 synthetic ones
After each significant change to the system (prompt, model, chunking, retriever), be sure to read 20–50 outputs manually. Automated metrics hide error patterns that are instantly visible to the eye.
Source-complete noteserror analysis protocol · practice
sql# After any change:
sample = random_sample(outputs, n=30)
# or: all fails from eval for the period
# For each read:
✓ Is the answer correct in meaning?
✓ Is the answer based on context or fictitious?
✓ Is the formatting as expected?
✓ Is the answer too long/short/evasive?
✓ Does the model respond when it should fail?
✓ Does the model refuse when it should respond?
# Group errors by type:
hallucination | wrong_refusal
off_topic | format_error
incomplete | tone_mismatch
# Most common category → fix firstpractice
- read the fails yourself - don’t delegate to the junior
- 30 examples give 80% of the picture
- every bug → test case in the dataset
Key principle: a good bottom line metric doesn't tell you where it's broken. Measure each pipeline node separately - retrieval, rerank, prompt, generation - otherwise you won’t understand what to fix.
Source-complete notesrule
rule
- change one variable at a time
- record the dataset, model, index when comparing
- comparing on different datasets = pointless
Trace - a record of everything that happened in the pipeline for one request. Without traces, you cannot debug, build eval and detect degradation. The minimum set of fields is the one without which it is impossible to answer “why the answer is incorrect.”
Source-complete notesrequired trace fields · tools
json{
"trace_id": "uuid", # end-to-end ID
"timestamp": "ISO-8601",
"user_id": "...", # for user-level analysis
// Input
"query": "original query",
"query_rewrite":"after augmentation", # if any
// Retrieval
"retrieved_chunks": ["chunk_id", "score", "text"],
"reranked_chunks": ["chunk_id", "score"],
//Generation
"prompt_tokens": 1240,
"completion_tokens": 312,
"model": "claude-sonnet-4",
"response": "final response",
"latency_ms": 1840,
// Eval (async, after request)
"faithfulness": 0.92,
"refusal": false
}tools
- Langfuse - open-source, self-hosting
- Phoenix Arize - traces + eval
- the trace structure is more important than the tool
Binary vs Scale: why pass/fail wins
Intuition suggests: a scale of 1–5 is more accurate, because it contains more information. Experience suggests the opposite: numerical scales are unstable and there is little agreement between raters (humans and LLMs) on what a “3” vs “4” means.
Source-complete noteswhen the scale is still justified
when the scale is still justified
- ranking of multiple options (A/B/C/D)
- quality gradations with clear categories and examples
- only if κ > 0.6 on the pilot marking
- never for automation without verification
The criterion must be formulated so that any reasonable person (or LLM) would give the same answer. Test: give a criterion to two people without explanation - if they don’t match, the criterion is bad.
Source-complete notesgood criterion template · practice
sql# Structure: "The answer [does X]?" where X is specifically
✗ Bad (blurry):
"Is the answer quality?"
"Is the answer useful to the user?"
"Is the answer correct?"
✓ Good (specific):
"Does the answer contain at least one specific number?"
"The answer doesn't mention a competitor company?"
"Is the answer written in Russian?"
“The answer begins with a direct answer, not with introductory words?”
"Does the answer contain statements that are not in the context?"
# Criterion test:
sample = 20 examples
human_1_labels = [...]
human_2_labels = [...]
κ = cohens_kappa(human_1_labels, human_2_labels)
# κ > 0.6 → criterion is valid
# κ < 0.4 → reformulatepractice
- one criterion → one question
- formulation through specific behavior
- abstract words: “quality”, “useful”
These are two opposite types of errors. If the model is too careful, it refuses to answer when the answer would be correct (false refusal). If she is too bold, she makes up facts (hallucination). The balance depends on the domain and the cost of the error.
Source-complete notestypical thresholds
typical thresholds
- medicine: hallucination < 1%, refusal ok
- e-commerce: refusal < 5%, hallucination < 10%
- always measure separately - one final figure hides
LLM-as-Judge: correlation with people
LLM-judge is only useful if his scores match how people score. Validation of judge is a mandatory step before trusting automatic scores. The binary score gives a significantly better correlation than the numerical scale.
Source-complete notesknown problems of LLM-judge
known problems of LLM-judge
- self-enhancement bias: the model prefers its own answers
- verbose bias: long answer scores higher
- position bias: the first in a pair gets an advantage
- use a stronger model as judge
The quality of judge critically depends on the structure of its prompt. Template: clear criterion → CoT reasoning → final verdict. CoT before the verdict significantly improves quality and provides an explanation for the debug.
Source-complete notesjudge prompt template · how to increase stability
textSYSTEM:
You evaluate the quality of the AI assistant's answers.
Answer strictly according to the format below.
USER:
USER QUESTION: {question}
CONTEXT (sources): {context}
ASSISTANT'S REPLY: {answer}
CRITERIA:
Are all the facts in the answer supported by context?
A fact is considered confirmed if it is clearly
is present in one of the sources above.
Step 1. Explain: List all statements in your answer.
and for each, indicate whether it is in the context.
Step 2. Verdict: PASS or FAIL.
(PASS only if ALL statements are confirmed)
EXPECTED FORMAT:
Analysis: [chain of reasoning]
Verdict: PASS/FAILhow to increase stability
- Temperature=0 for reproducibility
- 3 runs → majority vote at T>0
- swap A/B in pairwise comparison
- CoT before the verdict - not after
Not every check needs to be done through an LLM call. Many criteria are checked cheaper and more reliably by deterministic code, regex or BERT classifier. LLM-judge - only where understanding of the meaning is needed.
Source-complete notestable · principle
| What we check | Best tool | Why |
|---|---|---|
| JSON validity | json.parse() | deterministic, 0ms |
| Response Length | len(text) | deterministic |
| Availability of keywords | regex / str.contains | quickly, accurately |
| PII (email, phone) | regex / spaCy NER | cheaper, more reliable |
| Toxicity | BERT classifier | fast, calibrated |
| Semantic similarity | cosine(embed_A, embed_B) | without LLM call |
| Faithfulness (facts vs source) | LLM-judge / NLI | need understanding |
| Tone, style, "Useful framing" | LLM-judge with rubric | only if κ>0.6 |
principle
- cheap first: regex → BERT → LLM
- LLM-judge everywhere = expensive and unstable
It would seem logical: put LLM-judge directly into the pipeline as a filter for each answer. In practice, this is almost always a bad idea - due to latency and instability.
Source-complete noteswhy not · exceptions
textProblem 1: Latency
Main call: 800ms
+ LLM-judge: +600ms ← +75% latency
= total: 1400ms ← user notices
Problem 2: Instability
The same request, judge in the pipeline:
Run 1: PASS → response sent
Run 2: FAIL → response blocked
← non-deterministic UX
Problem 3: False locks
judge is wrong → the correct answer is blocked
the user receives a refusal to a valid question
──────────────────────── ─────────────────────────
What to do instead:
In-pipeline (guardrail): regex + BERT (fast)
Async eval (evaluator): LLM-judge on sample
→ estimate 10% of traffic asynchronously
→ do not block the user
→ alert when metrics dropexceptions
- very high rates (medicine) → latency can be tolerated
- then: cache judge on similar queries
Guardrails vs Evaluators: different roles
Guardrail is part of the production pipeline, works synchronously, blocks or modifies output in real time. Evaluator is a separate quality assessment service that works asynchronously or offline and does not directly affect the user’s UX.
Source-complete noteskey rule
key rule
- Guardrail = security and format
- Evaluator = quality and improvement
- do not confuse the roles - LLM-judge is not guardrail
The rule of choice: the cheaper and more deterministic the tool, the better for guardrail. Layered approach: first cheap checks, then expensive ones - only if you pass the cheap ones.
Source-complete notesguardrails stack by cost
textLayer 1 - deterministic (0ms, $0)
regex: prohibited words, PII patterns
schema: JSON validation, data type
length: min/max tokens
format: expected response structure
Layer 2 - BERT/classifier (5–20ms, ~$)
toxicity: BERT toxicity classifier
PII: spaCy NER for email/phone/passport
topic: zero-shot classifier (not our topic?)
lang: detect_language(response) == "ru"
Layer 3 - Light LLM (50–200ms, $$)
safety: haiku/mini checks safety
schema: "response is valid JSON with fields X,Y?"
← only if the first layers were skipped
Layer 4 - heavy LLM (never in guardrail)
faithfulness, helpfulness → evaluator onlyEvaluator is an asynchronous service subscribed to a log queue. Evaluates a random sample of traffic and builds metrics. The main task: to give a signal that “the system has degraded” even before users start complaining.
Source-complete notesevaluator service architecture · what to monitor
python# Data stream
PROD TRAFFIC → message queue (Kafka/SQS)
│
sampling: 10% of requests
│
▼
EVALUATOR WORKER
for each trace:
faithfulness = llm_judge(...)
format_ok = regex_check(...)
refusal = detect_refusal(...)
│
▼
METRICS STORE (TimeSeries DB)
│
▼
ALERT if score_7d_avg drops > 5%what to monitor
- faithfulness_score (moving average)
- refusal_rate (growth = problem)
- latency p50/p95/p99
- alert when deviation > 2σ from baseline
Negative Scenarios: dataset for failures
Most teams only test the happy path: “ask a question, get an answer.” But the system must be able to intelligently refuse: when there is no information in the context, when the request is outside the domain, when the request is unsafe. Each type requires a separate dataset.
Source-complete notesmetrics on negative dataset
metrics on negative dataset
- correct_refusal_rate → should be high
- false_answer_rate (hallucination) → should be low
- measure separately for each type
A special class of tests: queries that the model has no reason to answer. The goal is to check that the model is not making things up, but admitting ignorance. Pattern: correct question + obviously empty or irrelevant context.
Source-complete notesconstruction patterns
sql# Pattern 1: there is context, no answer
context = "WB commission for clothes: 12%. Shoes: 15%."
question = "What is the commission for electronics?"
expected = REFUSE # "The source contains no electronics data"
# Pattern 2: question about the future/non-existent
question = "What will the dollar exchange rate be in 2030?"
expected = REFUSE # "Can't predict the future"
# Pattern 3: Conflicting Context
context = "Commission 12%. Commission 18%."
question = "What is the commission?"
expected = REFUSE/CLARIFY # indicate a contradiction
# How to evaluate automatically:
judge_prompt = """
Did the assistant answer the question using
ONLY information from context?
If the context does not contain an answer, is it correct?
the assistant refused to answer? PASS/FAIL
"""Active testing: intentionally give the model a false or empty context and see if it uses its “prior knowledge” instead of the context. This test often reveals problems that are not visible in a normal eval.
Source-complete notesprobing techniques · starting frequency
text# Technique 1: False Fact Context
context = "The company was founded in 2031." # lies
question = "When was the company founded?"
✓ Correct: “In 2031” ← follows the context
✗ Problem: “In 1998” ← ignores context, returns prior
# Technique 2: empty context
context = ""
question = "What is the VAT rate in Russia?"
✓ Correct: "There is no data in the provided context"
✗ Problem: “VAT in Russia is 20%” ← hallucination
#Technique 3: Irrelevant context
context = "Borscht recipe: beets, potatoes..."
question = "What are the delivery terms?"
✓ Correct: refusal / clarification
✗ Problem: invents delivery termsstarting frequency
- every time the prompt changes - mandatory
- when changing model - required
- ~10% of the dataset = hallucination probes
Drift Detection: degradation after release
The system can degrade without a single code change - due to changes in the external environment: new documents in the knowledge base, changes in user request patterns, changes in the API of the model provider. Understanding the sources of drift is critical to proper diagnosis.
Source-complete notestable · degradation signals that are visible in metrics
| Drift type | Reason | Detector | Treatment |
|---|---|---|---|
| Data drift | New documents in KB do not match the old chunking | retrieval recall@k crashes | re-embed new docs |
| Query drift | Users started asking about new topics | out-of-domain rate is growing | extend KB / update prompt |
| Model drift | The provider has changed the model (silent update) | response format/tone changes | pin version of the model |
| Concept drift | Reality has changed (new law, prices) | faithfulness falls due to new facts | update KB, add date |
| Prompt drift | The prompt was changed and eval did not run | pass_rate for golden set | CI for prompts |
textEarly signals (fast): rising → bad sign
↑ refusal_rate — the model refuses more often
↑ response_length — “inflates” responses
↑ latency_p99 - something in the pipeline has slowed down
↑ token_count - context is growing (context pollution)
Delayed signals (via eval): falling → bad sign
↓ faithfulness_score
↓ recall@k (retriever)
↓ task_success_rate (agent)
↓ user_thumbs_up rateGolden dataset is a fixed set of examples with known expected answers. Run regularly (daily) on a production system without changes - any drop in pass_rate signals drift.
Source-complete notesmonitoring structure · practice
python# Daily scheduled job
for example in golden_set:
response = system.run(example.query)
score = evaluate(response, example.criteria)
metrics_store.record(score, timestamp=today)
# Comparison with baseline (release day)
delta = today_pass_rate - baseline_pass_rate
if delta < -0.05: # fell by 5%
alert(channel="#ml-alerts",
msg=f"Golden set: {delta:+.1%}")
# What should be in the golden set:
# - typical cases (50%)
# - edge cases (30%)
# - negative scenarios (20%)practice
- 50–200 examples are enough for a signal
- update golden set when changing domain
- don’t update with every improvement - you lose baseline
The purity of the experiment is the main condition for meaningful comparison. Change one variable at a time. Without this, you won’t understand what exactly improved or broke the system.
Source-complete notescontrolled experiment protocol
text# Fix (do not change):
FIXED = {
"eval_dataset": "v3.1", # one dataset
"vector_index": "2025-06-01", # index snapshot
"embed_model": "text-emb-3",
"llm_model": "claude-sonnet-4-6",
"temperature": 0.0, # reproducible
"k": 5,
}
# Change (one at a time):
VARIABLE = "chunk_size" #512 -> 256
# Run both options on the same dataset
results_A = run(system_A, FIXED, dataset)
results_B = run(system_B, FIXED, dataset)
# Compare metrics
# Make sure the difference is statistically significant
# p-value < 0.05 for n >= 100Synthetic Data: when it helps, when it harms
A naive approach—asking an LLM to generate 100 questions—yields a homogeneous, biased dataset. Structural approach: first a taxonomy of complexity and types, then generation for each matrix cell, then verification.
Source-complete notespractice
practice
- matrix approach → diversity
- sample verification is mandatory
- 100% LLM without verification → confirmation bias
- start with real examples anyway
Synthetic data carries risks that are easy to overlook, especially when the same model is used for both dataset generation and evaluation. This is called model collapse or circular evaluation.
Source-complete notescases when synthetic is a bad choice
text1. Circular eval (main problem)
GPT-4 generates QA → GPT-4 evaluates QA
← the model evaluates “itself”, there will always be a good score
← does not detect model system errors
2. Distribution mismatch
Synthetic questions - literary Russian
Real users - colloquial, with typos
← eval shows good, cont shows bad
3. Coverage gaps
LLM does not know what NOT to generate (edge cases in prod)
← does not cover real problem queries
4. Incorrect reference answers
LLM generates "correct answer" with error
Let's use it as ground truth for eval
← testing on obviously incorrect examples
When synthetic is acceptable:
✓ expansion of a small dataset of real examples
✓ type coverage (not replacing real examples)
✓ with verification by an expertThe Golden dataset is becoming obsolete: the business domain is changing, new types of questions appear, the product is changing. An updating process is needed - without it, eval begins to measure something else.
Source-complete noteslifecycle dataset · practice
text# Update triggers:
→ A bug was found in the product → add as a fail test
→ Product has changed → update expected
→ New feature → add new cases
→ Once a quarter → review of outdated examples
# Versioning:
datasets/
golden_v1.jsonl # ← do not delete! baseline for comparison
golden_v2.jsonl
golden_current → golden_v2.jsonl # symlink
# Metadata for each example:
{
"id": "tc_087",
"added": "2025-04-12",
"source": "prod_bug_#2341",
"last_reviewed": "2025-06-01",
"tags": ["refusal", "no-context"]
}practice
- every product bug → test case
- versioning the dataset as code
- keep old versions for comparison
- update golden set without committing baseline
No dead end
Keep moving through the map.
Continue in sequence, switch to a related guide, or return to the seven-track learning map.