02 Model Landscape 9 min read 1,978 words
Capability Assessment
This chapter covers how to evaluate and compare model capabilities for your specific use case. Generic benchmarks rarely tell the full story; this guide helps you conduct meaningful assessments.
Why Benchmarks Are Not Enough
The Benchmark Problem
Public benchmarks (MMLU, HumanEval, GSM8K) have limitations:
| Issue | Impact |
|---|---|
| Training data contamination | Models may have seen test questions |
| Task mismatch | Benchmarks may not reflect your use case |
| Aggregate scores hide variance | Model A may beat B overall but lose on your domain |
| Gaming | Models optimized for benchmarks over real tasks |
| Outdated | Benchmarks lag behind model capabilities |
What Benchmarks Tell You
Benchmark results tell you: "Model X scored 88% on MMLU"
What you need to know: "Will Model X correctly answer my
customers' questions about our product documentation?"
Rule of thumb: Use benchmarks for initial filtering, then conduct your own evaluation.
Evaluation Dimensions
Dimension 1: Task Performance (Dec 2025)
| Task Type | Evaluation Approach | Key Metric |
|---|---|---|
| Autonomous Coding | CWE/SWE-bench (Verified) | % issues resolved autonomously |
| Long-Horizon Planning | Agentic Loop testing | Success rate on 10+ step plans |
| Reasoning Depth | Thinking mode analysis | Logic consistency across CoT steps |
| Long Context RAG | Needle-in-a-Haystack (2M+) | Recall efficiency at scale |
| Native Multimodal | Interleaved Vision/Voice/Text | Sync accuracy across modalities |
Dimension 2: Agentic Mastery
How well does the model use tools and follow multi-step instructions?
def evaluate_agentic_flow(agent, task_environment):
"""
Measure success on 'Autonomous Agent' tasks:
1. Plan generation
2. Tool selection accuracy
3. Error recovery
4. Feedback loop utilization
"""
results = []
for scenario in task_environment.scenarios:
traj = agent.run(scenario.goal)
results.append({
"success": traj.reached_goal,
"steps": len(traj.steps),
"tool_errors": traj.count_invalid_tool_calls()
})
return aggregate(results)
Dimension 3: Reasoning Reliability
Does the "Thinking" mode improve output accuracy vs standard generation?
| Mode | Accuracy (Math) | Accuracy (Code) | Avg Latency | Tokens / Output |
|---|---|---|---|---|
| Standard | 72% | 68% | 1.2s | 400 |
| Thinking | 94% | 89% | 12.5s | 2400 |
| Hybrid | Variable | Variable | User-defined | Configurable |
Reasoning Calibration
The "Over-Thinking" Problem: Models often spend 2000+ "thinking" tokens on a question that could be answered with 10 tokens (e.g., "What is 2+2?").
Principal-level Nuance:
Evaluate models based on Logic Efficiency: Accuracy / (Inference Tokens).
Production systems in 2025 use Model Arbitration: A small model (Gemini 3 Flash) detects if a query needs "Thinking" mode. This avoids the 10x latency/cost penalty for simple queries.
Internal Elo-based Evaluation
Moving beyond static rubrics. Rubrics (1-5 scales) are prone to "judge fatigue" and "score drifting." Late 2025 systems use Pairwise Elo for internal golden sets.
The Workflow:
- Blind Side-by-Side: Model A and Model B generate answers for the same query.
- The Judge: An "Ultra" model (GPT-5.2 or Human) selects the winner.
- Elo Update: Update the internal leaderboard.
def update_elo(winner_elo, loser_elo, k=32):
expected_winner = 1 / (1 + 10 ** ((loser_elo - winner_elo) / 400))
new_winner_elo = winner_elo + k * (1 - expected_winner)
new_loser_elo = loser_elo + k * (0 - (1 - expected_winner))
return new_winner_elo, new_loser_elo
Why it wins: It provides a relative ranking that is much more robust to changes in judge personality or model versioning.
Dimension 4: Context Recall (Dec 2025)
With 2M+ context windows, simple "needle-in-a-haystack" is no longer enough. We now measure Contextual Reasoning across the window.
| Metric | Measurement | Target |
|---|---|---|
| Window Recall | Factual recall at 90% window depth | > 98% |
| Cross-Doc Reasoning | Logic linking Doc A (pos 10k) to Doc B (pos 1M) | > 90% |
| Contextual Noise Resistance | Accuracy when 90% of window is irrelevant "filler" | > 95% |
Building Custom Evaluations
Step 1: Define Evaluation Criteria
evaluation_criteria = {
"correctness": {
"weight": 0.4,
"description": "Is the answer factually correct?",
"scale": [1, 2, 3, 4, 5],
"rubric": {
5: "Completely correct, no errors",
4: "Mostly correct, minor issues",
3: "Partially correct, some errors",
2: "Mostly incorrect",
1: "Completely wrong or nonsensical"
}
},
"relevance": {
"weight": 0.3,
"description": "Does the answer address the question?",
"scale": [1, 2, 3, 4, 5]
},
"completeness": {
"weight": 0.2,
"description": "Are all parts of the question addressed?",
"scale": [1, 2, 3, 4, 5]
},
"conciseness": {
"weight": 0.1,
"description": "Is the answer appropriately concise?",
"scale": [1, 2, 3, 4, 5]
}
}
Step 2: Create Test Set
test_set = [
{
"id": "q001",
"query": "What is the refund policy for subscription cancellation?",
"context": "[relevant documentation]",
"ground_truth": "Full refund within 30 days, prorated after",
"difficulty": "easy",
"category": "policy"
},
{
"id": "q002",
"query": "How do I integrate the API with a Python async application?",
"context": "[API documentation]",
"ground_truth": "[expected code pattern]",
"difficulty": "medium",
"category": "technical"
},
# ... 50-100+ test cases
]
Test set guidelines:
- Cover all major use cases
- Include easy, medium, hard examples
- Balance across categories
- Include edge cases
- Have clear ground truth answers
Step 3: Implement Evaluation
class ModelEvaluator:
def __init__(self, models: list[str], test_set: list[dict]):
self.models = models
self.test_set = test_set
self.results = {}
def evaluate_all(self):
for model in self.models:
self.results[model] = self.evaluate_model(model)
return self.results
def evaluate_model(self, model: str) -> dict:
scores = []
latencies = []
for case in self.test_set:
start = time.time()
response = self.generate(model, case)
latency = time.time() - start
latencies.append(latency)
# Score using LLM judge or human
score = self.score_response(case, response)
scores.append(score)
return {
"mean_score": mean(scores),
"score_by_category": self.group_by_category(scores),
"p50_latency": percentile(latencies, 50),
"p99_latency": percentile(latencies, 99)
}
def score_response(self, case: dict, response: str) -> float:
# Option 1: LLM-as-judge
return self.llm_judge(case, response)
# Option 2: Exact match
# return exact_match(response, case["ground_truth"])
# Option 3: Semantic similarity
# return cosine_sim(embed(response), embed(case["ground_truth"]))
Step 4: LLM-as-Judge
def llm_judge(case: dict, response: str) -> dict:
prompt = f"""Evaluate this response to a customer query.
Query: {case['query']}
Expected Answer: {case['ground_truth']}
Model Response: {response}
Rate the response on these criteria (1-5 scale):
1. Correctness: Is it factually accurate?
2. Relevance: Does it answer the question?
3. Completeness: Are all aspects covered?
4. Conciseness: Is it appropriately brief?
Output JSON:
{{"correctness": X, "relevance": X, "completeness": X, "conciseness": X, "reasoning": "..."}}
"""
result = judge_model.generate(prompt)
return parse_json(result)
Common Evaluation Pitfalls
Pitfall 1: Small Test Set
Problem: 20 test cases is not enough for reliable comparison.
Solution: Aim for 100+ cases, stratified by difficulty and category.
Pitfall 2: Ambiguous Ground Truth
Problem: "Reasonable" answers get marked wrong.
Query: "What is the capital of Australia?"
Ground truth: "Canberra"
Model answer: "The capital of Australia is Canberra."
Exact match: FAIL (but clearly correct)
Solution: Use semantic matching or LLM judge, not exact match.
Pitfall 3: Evaluation Set Leakage
Problem: Using same cases for development and evaluation.
Solution: Keep a held-out test set that you never use for prompt tuning.
Pitfall 4: Ignoring Variance
Problem: Running each test once ignores model randomness.
Solution: Run multiple times with temperature > 0, report confidence intervals.
Pitfall 5: Cost Blindness
Problem: Best model is 10x more expensive.
Solution: Always report quality-adjusted cost.
def quality_adjusted_cost(model_results):
return {
model: {
"quality": results["mean_score"],
"cost_per_1k": results["cost_per_1k_queries"],
"quality_per_dollar": results["mean_score"] / results["cost_per_1k"]
}
for model, results in model_results.items()
}
Practical Assessment Process
Week 1: Setup and Initial Filtering
Day 1-2: Define evaluation criteria and create test set
Day 3-4: Benchmark 4-6 candidate models
Day 5: Analyze results, filter to top 2-3
Week 2: Deep Evaluation
Day 1-2: Expand test set for top candidates
Day 3: Test edge cases and robustness
Day 4: Measure latency and throughput
Day 5: Calculate total cost of ownership
Week 3: Production Validation
Day 1-2: Shadow mode deployment
Day 3-4: A/B test if traffic allows
Day 5: Final decision and documentation
Decision Template
## Model Evaluation Report
### Candidates Evaluated
- Model A: GPT-4o
- Model B: Claude 3.5 Sonnet
- Model C: Llama 3.1 70B
### Evaluation Results
| Metric | Model A | Model B | Model C |
|--------|---------|---------|---------|
| Overall Score | 4.2/5 | 4.3/5 | 3.9/5 |
| Category 1 | ... | ... | ... |
| P50 Latency | 450ms | 520ms | 180ms |
| Cost/1K queries | $0.85 | $1.10 | $0.25 |
### Recommendation
Model B (Claude 3.5 Sonnet) for quality-critical paths
Model C (Llama 3.1 70B) for high-volume, cost-sensitive paths
### Rationale
[Detailed reasoning]
A/B Testing Models
When to A/B Test
- High traffic (1000+ queries/day)
- Clear success metrics
- Acceptable risk of quality variation
- Need production validation
A/B Test Design
class ModelABTest:
def __init__(self, model_a: str, model_b: str, traffic_split: float = 0.5):
self.model_a = model_a
self.model_b = model_b
self.traffic_split = traffic_split
self.results = {"a": [], "b": []}
def route_request(self, request_id: str) -> str:
# Deterministic routing for consistency
hash_val = hash(request_id) % 100
if hash_val < self.traffic_split * 100:
return self.model_a
return self.model_b
def record_outcome(self, request_id: str, metrics: dict):
model = self.route_request(request_id)
bucket = "a" if model == self.model_a else "b"
self.results[bucket].append(metrics)
def analyze(self):
return {
"model_a": {
"name": self.model_a,
"mean_score": mean([r["score"] for r in self.results["a"]]),
"sample_size": len(self.results["a"])
},
"model_b": {
"name": self.model_b,
"mean_score": mean([r["score"] for r in self.results["b"]]),
"sample_size": len(self.results["b"])
},
"p_value": self.calculate_significance()
}
Metrics to Track
| Metric Type | Examples |
|---|---|
| Quality | User ratings, expert review, LLM judge |
| Engagement | Click-through, time on page, follow-up queries |
| Business | Conversion, support escalation, resolution rate |
| Operational | Latency, errors, cost |
Interview Questions
Q: How would you evaluate models for a customer support chatbot?
Strong answer: I would structure evaluation in layers:
1. Offline evaluation (80% of effort):
- Create test set from real support tickets (200+ cases)
- Cover all categories: billing, technical, returns, general
- Include easy, medium, hard difficulty
- Measure: accuracy, helpfulness, safety
2. Evaluation method:
- Use LLM-as-judge for subjective metrics
- Human review for sample (20%)
- Track instruction following (format, length)
3. Metrics:
metrics = {
"resolution_accuracy": "Does answer solve the problem?",
"safety": "No harmful/wrong advice?",
"tone": "Professional and empathetic?",
"escalation_appropriate": "Knows when to involve human?"
}
4. Production validation:
- Shadow mode: run new model, compare outputs
- A/B test: 10% traffic to new model
- Monitor: CSAT, escalation rate, resolution time
Q: What is wrong with using MMLU to compare models for your use case?
Strong answer: MMLU has several problems for specific use cases:
1. Domain mismatch
MMLU tests academic knowledge. My customer support bot needs product knowledge.
2. Format mismatch
MMLU is multiple choice. My use case is free-form generation.
3. Contamination
Models may have trained on MMLU questions.
4. Aggregation hides variance
Model A might beat B on MMLU but lose on the specific categories I care about.
5. No context testing
MMLU does not test RAG or long-context abilities.
Better approach:
- Use MMLU for initial filtering (saves time)
- Build custom evaluation for final decision
- Test on actual use case data
- Include operational metrics (latency, cost)
References
- Zheng et al. "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena" (2023)
- LMSYS Chatbot Arena: https://chat.lmsys.org/ chat.lmsys.org
- HELM: https://crfm.stanford.edu/helm/ crfm.stanford.edu
- LMSys Evaluation: https://github.com/lm-sys/FastChat/tree/main/fastchat/llm_judge github.com
- OpenAI Evals: https://github.com/openai/evals github.com
Previous: Model Taxonomy | Next: Pricing and Costs
Key takeaways
01
Thinking mode costs ten times the latency
The mode comparison puts standard generation at 72 percent math accuracy in 1.2s and 400 tokens, against thinking mode's 94 percent in 12.5s and 2400 tokens.
02
Benchmarks filter, custom evals decide
Contamination, format mismatch and aggregate scores that hide per-category variance mean MMLU narrows the candidate list but never settles which model answers your users' questions.
03
Pairwise Elo beats a 1-5 rubric
Rubric scores drift as judges fatigue; blind side-by-side comparisons fed into an Elo table with k=32 give a relative ranking robust to judge and version changes.
04
Twenty test cases prove nothing
The pitfalls list asks for 100+ examples stratified by difficulty and category, each run several times at non-zero temperature so variance is reported rather than hidden.
05
Always report quality per dollar
Cost blindness is its own pitfall: pair mean score with cost per thousand queries so a ten-times-more-expensive model has to justify the margin, not just win.