Tutorial 9: Cost-Aware Evaluation
- Contributor
- 4 days ago
- 3 min read
A 500-case eval, run on every PR, with LLM-as-judge x3 consensus = expensive. Same signal at 10x lower cost is achievable.
Step 1: Measure Current Eval Cost (10 min)
def eval_cost(num_cases, judges_per_case, model, judge_model):
# Output tokens per case
output_tokens = 500
judge_tokens = 200
case_cost = (output_tokens / 1000) * MODEL_COST[model]
judge_cost = (judge_tokens / 1000) * MODEL_COST[judge_model] * judges_per_case
total_per_case = case_cost + judge_cost
return total_per_case * num_cases
# Example
# 500 cases * 3 judges * $0.01 = $15/run
# Run on every PR (20/day) = $300/day = $9000/month
Quantify. Often surprises teams.
Step 2: Sample-Based Eval (10 min)
Statistical sampling for CI:
def stratified_sample(eval_set, n=20):
by_category = group_by_category(eval_set)
sample = []
per_category = n // len(by_category)
for cat, cases in by_category.items():
sample.extend(random.sample(cases, per_category))
return sample
20 stratified cases catch the same regressions as 500 random cases, most of the time.
CI: 20 cases. Nightly: full 500.
Step 3: Cheap Filter First (10 min)
Rules cost ~$0; LLM judge costs $0.01:
def grade(output, expected):
# Cheap rules first
rule_grade = grade_with_rules(output, expected)
if not rule_grade["pass"]:
return rule_grade # Failed rules; don't bother with LLM judge
# Only spend on LLM judge if rules passed
return grade_with_llm_judge(output, expected)
Don't pay for LLM judge to confirm what regex caught.
Step 4: Cache LLM Calls (10 min)
import hashlib
from pathlib import Path
CACHE_DIR = Path(".eval-cache")
CACHE_DIR.mkdir(exist_ok=True)
def cached_llm(model, prompt, input_text):
key = hashlib.sha256(f"{model}|{prompt}|{input_text}".encode()).hexdigest()
cache_file = CACHE_DIR / f"{key}.json"
if cache_file.exists():
return json.loads(cache_file.read_text())
result = call_llm(model, prompt, input_text)
cache_file.write_text(json.dumps(result))
return result
Hash on (model, prompt, input). Same combo = cached. Re-running an eval on the same prompt is free.
Step 5: Cheaper Judge Model (10 min)
Use a cheaper model for judge if it correlates with the expensive one:
# Test correlation on a sample
expensive_grades = [judge_expensive(c) for c in sample]
cheap_grades = [judge_cheap(c) for c in sample]
correlation = pearson(expensive_grades, cheap_grades)
# If > 0.85, cheap judge works
Haiku as judge for many tasks; Sonnet only for nuanced ones.
Step 6: Reduce Output Tokens (5 min)
LLM judge prompt asking for 5 scores + reasoning = many tokens.
# Verbose
JUDGE = "Evaluate the response and provide detailed reasoning for each metric..."
# Compact
JUDGE = """
Score on 1-5 scale. Output JSON only:
{"helpful": N, "accurate": N, "pass": true/false}
"""
Fewer tokens; same signal.
Step 7: Skip Eval on Trivial Changes (5 min)
on:
pull_request:
paths:
- 'prompts/**' # Run if prompt changed
- 'src/llm/**' # Run if LLM code changed
# NOT: docs/**, README.md, frontend/**
Saves runs that wouldn't have signal anyway.
Step 8: Quota Budget (5 min)
Set monthly budget for eval:
def check_budget():
spent = get_monthly_eval_spend()
budget = 500 # dollars
if spent > budget:
alert("Eval budget exceeded")
# Skip non-critical evals; full only weekly
Engineers see budget; helps catch runaway.
Step 9: Parallel Eval Runs (10 min)
from concurrent.futures import ThreadPoolExecutor
def run_eval_parallel(eval_set, prompt, workers=20):
with ThreadPoolExecutor(max_workers=workers) as executor:
results = list(executor.map(
lambda c: eval_case(c, prompt),
eval_set,
))
return results
100 cases run sequentially: 100 × 2s = 200 seconds. With 20 workers: 10 seconds.
Same cost, faster. Faster = less CI compute time.
Step 10: Trade Off Frequency vs. Depth (5 min)
Match eval depth to risk:
Every commit: rules only, sampled 10 cases (free, fast)
Every PR: rules + cheap LLM judge, 20 cases ($1)
Nightly: full eval + expensive judge, 500 cases ($15)
Pre-release: full eval + 3-judge consensus, all 500 cases ($45)
Cost scales with risk. Most changes don't need pre-release depth.
What You Just Did
Cut eval cost 10x+ while preserving signal. Caching, sampling, layered grading, model selection. Sustainable for high-velocity teams.
Common Failure Modes
Sample too small. Save money, lose signal. 5 cases isn't an eval.
Stratification wrong. Sample misses critical category.
Cache invalidation forgotten. Prompt changed; cache returns old answer.
Cheap judge with no validation. Saves money; produces wrong grades.
Skip CI gates. Save money on the eval that catches regressions.


