Build a Custom Eval Pipeline
Why public benchmarks do not transfer to your workload, how to build a model-selection eval, and why cost per successful task beats price per token.
Prerequisites
- Python 3.11+Required
python --version
- openaiRequired
pip install openai
Build an LLM evaluation harness
Create a reusable eval harness with LLM-as-judge scoring, deterministic checks, and result tracking.
Tasks
🎯 What You'll Learn
- ▸Write deterministic evals (exact match, regex, JSON schema)
- ▸Use LLM-as-judge for subjective quality scoring
- ▸Track eval results over time for regression detection
Code
1 import os 2 import json 3 import time 4 from dataclasses import dataclass, asdict 5 6 import openai 7 8 client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"]) 9 10 @dataclass 11 class EvalCase: 12 id: str 13 input: str 14 expected: str | None = None # For exact-match evals 15 criteria: str | None = None # For LLM-judge evals 16 17 @dataclass 18 class EvalResult: 19 case_id: str 20 model: str 21 response: str 22 score: float # 0.0–1.0 for exact, 1–5 for LLM judge 23 scorer: str 24 latency_ms: float 25 passed: bool 26 27 EVAL_DATASET = [ 28 EvalCase("q1", "What is 2+2?", expected="4"), 29 EvalCase("q2", "Name the capital of France.", expected="Paris"), 30 EvalCase( 31 "q3", 32 "Explain recursion to a 10-year-old.", 33 criteria="Is the explanation clear, uses an analogy, and avoids jargon?", 34 ), 35 ] 36 37 def exact_match(response: str, expected: str) -> tuple[float, bool]: 38 clean = response.strip().lower() 39 exp = expected.strip().lower() 40 score = 1.0 if exp in clean or clean == exp else 0.0 41 return score, score == 1.0 42 43 def llm_judge(response: str, criteria: str) -> tuple[float, bool]: 44 """Score 1-5 using an LLM judge.""" 45 prompt = f"""Score this response 1-5 based on the criteria. 46 Criteria: {criteria} 47 Response: {response} 48 Output JSON only: {{"score": <1-5>, "reason": "<brief reason>"}}""" 49 50 judge_response = client.chat.completions.create( 51 model="gpt-6-luna", reasoning_effort="none", 52 messages=[{"role": "user", "content": prompt}], 53 max_completion_tokens=100, 54 temperature=0, 55 response_format={"type": "json_object"}, 56 ) 57 result = json.loads(judge_response.choices[0].message.content) 58 score = result.get("score", 1) 59 return float(score), score >= 4 60 61 def run_evals(model: str, dataset: list[EvalCase]) -> list[EvalResult]: 62 results = [] 63 for case in dataset: 64 start = time.perf_counter() 65 response = client.chat.completions.create( 66 model=model, 67 messages=[{"role": "user", "content": case.input}], 68 max_completion_tokens=200, 69 reasoning_effort="low", 70 ) 71 latency = (time.perf_counter() - start) * 1000 72 text = response.choices[0].message.content 73 74 if case.expected: 75 score, passed = exact_match(text, case.expected) 76 scorer = "exact_match" 77 elif case.criteria: 78 score, passed = llm_judge(text, case.criteria) 79 scorer = "llm_judge" 80 else: 81 continue 82 83 results.append(EvalResult(case.id, model, text, score, scorer, latency, passed)) 84 return results 85 86 # Run evals 87 for model in ["gpt-6-luna"]: 88 results = run_evals(model, EVAL_DATASET) 89 passed = sum(1 for r in results if r.passed) 90 print(f"{model}: {passed}/{len(results)} passed") 91 92 # Save results 93 with open(f"evals_{model.replace('/', '-')}.jsonl", "w") as f: 94 for r in results: 95 f.write(json.dumps(asdict(r)) + "\n") 96
💡 Pro Tips
- ▸Keep evals repeatable: fix the model id and reasoning effort, and use temperature=0 where the model accepts it (GPT-6 Luna with reasoning off)
- ▸LLM judges are biased toward longer, more verbose responses — calibrate with human labels
Continue reading
All LLM Model Landscape articles →GPT: OpenAI's Model Family in Practice
The GPT-6 tiers, what reasoning effort replaced, when to use the Responses API over Chat Completions, how the bill is calculated, and what is different on Azure.
Claude: Anthropic's Model Family in Practice
The four Claude tiers, adaptive thinking and effort, the tool-use rules that differ from other providers, prompt caching, and where you can run it.
Llama: Open-Weight Models for Production
The Llama lineup and licence, serving with Ollama and vLLM, GPU memory sizing, and an honest look at when a hosted API is still the better choice.
Discussion
Sign in to share your feedback and join the discussion.

