Benchmarking LLMs for Your Use Case — Custom Evals Beyond MMLU and HumanEval
Advertisement
Introduction
MMLU, HumanEval, and other standard benchmarks don't measure whether your model performs your specific task. A model scoring 90% on HumanEval might fail basic SQL generation. This guide covers building domain-specific benchmarks that actually predict production performance.
Why Standard Benchmarks Mislead
Standard benchmarks optimize for paper citations, not production utility. MMLU tests world knowledge but not instruction following. HumanEval tests code generation on trivial problems — nothing like real production code. A model might excel at standard benchmarks while failing your specific use case.
The gap between benchmark performance and production performance can be >30%. This happens because:
- Distribution shift: your queries don't match benchmark format
- Task specificity: standard benchmarks test general capabilities, not your domain
- Contamination: models have seen benchmark data during training
- Evaluation biases: multiple-choice tests favor memorization over reasoning
Building Domain-Specific Benchmarks
Create benchmarks that mirror production queries exactly:
interface DomainSpecificBenchmark {
name: string;
domain: string;
examples: BenchmarkExample[];
metrics: MetricDefinition[];
baselineModel: string;
baselineScore: number;
version: string;
}
interface BenchmarkExample {
id: string;
input: string;
expectedOutput: string;
metadata: {
difficulty: 'easy' | 'medium' | 'hard';
category: string;
source: string;
};
}
interface MetricDefinition {
name: string;
compute: (output: string, expected: string) => number;
threshold: number; // Min acceptable score
}
class BenchmarkBuilder {
private benchmark: DomainSpecificBenchmark;
constructor(domain: string) {
this.benchmark = {
name: '',
domain,
examples: [],
metrics: [],
baselineModel: '',
baselineScore: 0,
version: '1.0'
};
}
addExample(
input: string,
expectedOutput: string,
difficulty: 'easy' | 'medium' | 'hard',
category: string
): this {
this.benchmark.examples.push({
id: `${Date.now()}-${Math.random()}`,
input,
expectedOutput,
metadata: {
difficulty,
category,
source: 'custom'
}
});
return this;
}
addMetric(
name: string,
compute: (output: string, expected: string) => number,
threshold: number = 0.8
): this {
this.benchmark.metrics.push({
name,
compute,
threshold
});
return this;
}
build(): DomainSpecificBenchmark {
return this.benchmark;
}
}
// Example: SQL generation benchmark
const sqlBenchmark = new BenchmarkBuilder('SQL')
.addExample(
'Find all customers from France who spent >1000 in Q1',
'SELECT c.* FROM customers c WHERE c.country = $1 AND c.total_spent > $2',
'medium',
'filtering'
)
.addExample(
'Count orders per product category',
'SELECT pc.name, COUNT(o.id) FROM order_items oi JOIN products p ON oi.product_id = p.id JOIN product_categories pc ON p.category_id = pc.id GROUP BY pc.name',
'hard',
'aggregation'
)
.addMetric(
'executable',
(output, expected) => {
try {
validateSQL(output);
return 1.0;
} catch {
return 0.0;
}
}
)
.addMetric(
'semantic_match',
(output, expected) => {
// Parse both queries and compare their structure
const query1 = parseSQL(output);
const query2 = parseSQL(expected);
return computeQuerySimilarity(query1, query2);
}
)
.build();
function validateSQL(sql: string): void {
// Parse and validate SQL syntax
if (!sql.includes('SELECT')) throw new Error('Invalid SQL');
}
function parseSQL(sql: string): object {
// Simple parsing logic
return { raw: sql };
}
function computeQuerySimilarity(q1: object, q2: object): number {
// Compare query structure
return 0.85;
}Task-Based Evaluation
Measure whether the model completes the actual task, not just generates text:
interface TaskEvaluation {
taskId: string;
input: string;
output: string;
taskCompleted: boolean;
successReason?: string;
failureReason?: string;
}
class TaskEvaluator {
async evaluateCodeGeneration(
prompt: string,
generatedCode: string
): Promise<TaskEvaluation> {
// Test 1: Does it parse?
try {
eval(generatedCode); // Security warning: only for trusted code
} catch (e) {
return {
taskId: 'codegen',
input: prompt,
output: generatedCode,
taskCompleted: false,
failureReason: 'Syntax error: ' + e
};
}
// Test 2: Does it have the required function?
const hasFunction = /function|const.*=/g.test(generatedCode);
if (!hasFunction) {
return {
taskId: 'codegen',
input: prompt,
output: generatedCode,
taskCompleted: false,
failureReason: 'No function definition found'
};
}
// Test 3: Run against test cases
const testCases = this.extractTestCases(prompt);
const allPass = await this.runTestCases(generatedCode, testCases);
return {
taskId: 'codegen',
input: prompt,
output: generatedCode,
taskCompleted: allPass,
successReason: allPass ? 'All tests passed' : undefined,
failureReason: !allPass ? 'Some tests failed' : undefined
};
}
private extractTestCases(prompt: string): Array<{ input: any; expected: any }> {
// Extract test cases from prompt comments
return [];
}
private async runTestCases(
code: string,
testCases: Array<{ input: any; expected: any }>
): Promise<boolean> {
// Execute code with test inputs and verify outputs
return true;
}
}Multilingual Benchmark Design
If serving global users, test multiple languages:
interface MultilingualBenchmark {
examples: Array<{
id: string;
language: string;
input: string;
expectedOutput: string;
}>;
languages: string[];
}
class MultilingualBenchmarkBuilder {
private examples: MultilingualBenchmark['examples'] = [];
private languages: Set<string> = new Set();
addExample(
id: string,
language: string,
input: string,
expectedOutput: string
): this {
this.examples.push({
id: `${id}-${language}`,
language,
input,
expectedOutput
});
this.languages.add(language);
return this;
}
build(): MultilingualBenchmark {
return {
examples: this.examples,
languages: Array.from(this.languages)
};
}
}
const multilingBench = new MultilingualBenchmarkBuilder()
.addExample('greet-1', 'en', 'Say hello', 'Hello!')
.addExample('greet-1', 'fr', 'Dites bonjour', 'Bonjour!')
.addExample('greet-1', 'es', 'Saludos', 'Hola!')
.build();Adversarial Examples
Test how models fail. Create adversarial cases to find weaknesses:
interface AdversarialTest {
name: string;
attackType: 'typo' | 'paraphrase' | 'constraint_violation' | 'injection';
baselineExample: BenchmarkExample;
adversarialExample: BenchmarkExample;
expectedRobustness: number; // <1.0 is OK, indicates how much performance should degrade
}
class AdversarialTestGenerator {
// Typo perturbations
addTypos(text: string, typoRate: number = 0.05): string {
const words = text.split(/\s+/);
return words
.map((word) => {
if (Math.random() < typoRate && word.length > 3) {
// Swap adjacent characters
const chars = word.split('');
const idx = Math.floor(Math.random() * (chars.length - 1));
[chars[idx], chars[idx + 1]] = [chars[idx + 1], chars[idx]];
return chars.join('');
}
return word;
})
.join(' ');
}
// Paraphrase: rephrase while keeping meaning
async paraphrase(text: string): Promise<string> {
// Call LLM to paraphrase
return text;
}
// Constraint violation: try to break model rules
addConstraintViolation(
text: string,
constraint: string
): string {
// If constraint is "under 50 words", make input way longer
// If constraint is "no profanity", add edgy content
return text;
}
// Prompt injection: try to override instructions
promptInjection(text: string, injectionPayload: string): string {
return `${text}\n\nIGNORE PREVIOUS INSTRUCTIONS: ${injectionPayload}`;
}
}
async function evaluateRobustness(
model: LLMModel,
benchmark: DomainSpecificBenchmark
): Promise<RobustnessScore> {
const adversarialGen = new AdversarialTestGenerator();
let robustnessScores: number[] = [];
for (const example of benchmark.examples) {
const baselineOutput = await model.generate(example.input);
const baselineScore = benchmark.metrics[0].compute(
baselineOutput,
example.expectedOutput
);
// Test with typos
const tippyInput = adversarialGen.addTypos(example.input);
const typoOutput = await model.generate(tippyInput);
const typoScore = benchmark.metrics[0].compute(typoOutput, example.expectedOutput);
// Robustness: how much did score degrade?
const degradation = 1 - typoScore / baselineScore;
robustnessScores.push(degradation);
}
const avgDegradation = robustnessScores.reduce((a, b) => a + b) / robustnessScores.length;
return {
averageDegradation: avgDegradation,
assessment: avgDegradation < 0.2 ? 'robust' : 'fragile'
};
}
interface RobustnessScore {
averageDegradation: number;
assessment: 'robust' | 'fragile';
}Benchmark Contamination Detection
Models might have seen your benchmark data during training. Detect this:
class ContaminationDetector {
async checkForContamination(
benchmark: DomainSpecificBenchmark,
model: LLMModel
): Promise<ContaminationReport> {
const suspiciousCases: Array<{
exampleId: string;
probability: number;
evidence: string[];
}> = [];
for (const example of benchmark.examples) {
// Test 1: Perfect reproduction
const output = await model.generate(example.input);
if (output === example.expectedOutput) {
suspiciousCases.push({
exampleId: example.id,
probability: 0.95,
evidence: ['Perfect match to expected output']
});
continue;
}
// Test 2: Excessive confidence
const confidence = await this.estimateConfidence(model, example.input);
if (confidence > 0.95) {
suspiciousCases.push({
exampleId: example.id,
probability: 0.7,
evidence: ['Unusually high confidence']
});
}
// Test 3: Substring containment
const chunks = example.expectedOutput.split(' ');
const substringsFound = chunks.filter((chunk) =>
output.includes(chunk)
).length;
const matchRate = substringsFound / chunks.length;
if (matchRate > 0.8) {
suspiciousCases.push({
exampleId: example.id,
probability: 0.6,
evidence: [`${(matchRate * 100).toFixed(1)}% substring overlap`]
});
}
}
const contaminationRate = suspiciousCases.length / benchmark.examples.length;
return {
contaminationRate,
assessment:
contaminationRate > 0.2 ? 'likely contaminated' : 'probably clean',
suspiciousCases
};
}
private async estimateConfidence(model: LLMModel, input: string): Promise<number> {
// Generate multiple times and measure consistency
const outputs: string[] = [];
for (let i = 0; i < 3; i++) {
outputs.push(await model.generate(input, { temperature: 0.3 }));
}
// If all outputs are identical, confidence is high
const unanimous = outputs.every((o) => o === outputs[0]);
return unanimous ? 0.99 : 0.5;
}
}
interface ContaminationReport {
contaminationRate: number;
assessment: string;
suspiciousCases: Array<{
exampleId: string;
probability: number;
evidence: string[];
}>;
}Running Evals with LM Evaluation Harness
EleutherAI's LM Evaluation Harness automates benchmark execution:
interface HarnessConfig {
model: string;
tasks: string[];
batchSize: number;
numFewShot: number;
device: 'cuda' | 'cpu';
}
async function runWithHarness(config: HarnessConfig): Promise<HarnessResult> {
// Command: lm_eval --model openai --model_args engine=text-davinci-003 --tasks mmlu,hellaswag --num_fewshot 0
// In TypeScript, wrap CLI or use Python binding
const results = {
tasks: {
mmlu: {
acc: 0.65,
acc_stderr: 0.01
},
hellaswag: {
acc_norm: 0.72,
acc_norm_stderr: 0.02
}
}
};
return results;
}
interface HarnessResult {
tasks: Record<string, Record<string, number>>;
}Sharing Benchmarks
Publish benchmarks for community contribution:
interface BenchmarkMetadata {
name: string;
description: string;
license: string;
contributors: string[];
lastUpdated: Date;
downloadUrl: string;
githubUrl?: string;
}
async function publishBenchmark(
benchmark: DomainSpecificBenchmark,
metadata: BenchmarkMetadata
): Promise<void> {
// Upload to Hugging Face Datasets
const datasetCard = `
# ${metadata.name}
${metadata.description}
## License
${metadata.license}
## Contributors
${metadata.contributors.join(', ')}
## Examples
${benchmark.examples.slice(0, 3).map((ex) => `- ${ex.input}`).join('\n')}
`;
console.log('Dataset card:', datasetCard);
// In production: upload to huggingface_hub
}Checklist
- Build domain-specific benchmarks matching production queries
- Include easy, medium, hard examples in each category
- Define task-based metrics (does it work?) not just string similarity
- Create adversarial test cases (typos, paraphrases, injections)
- Test across languages if serving multilingual users
- Detect benchmark contamination in baseline models
- Use LM Evaluation Harness for reproducible runs
- Compare models using domain-specific benchmarks, not MMLU/HumanEval
- Version benchmarks and track metric changes over time
- Share benchmarks publicly for community validation
Conclusion
Standard benchmarks optimize for papers, not production. Build domain-specific benchmarks that measure whether your model solves your actual problem. Include adversarial cases to expose failure modes. Detect contamination to ensure fair evaluation. Task-based metrics beat string similarity. Multilingual evaluation catches regional issues. Shared benchmarks enable reproducibility and community contribution. A weak benchmark can hide poor real-world performance; invest time in evaluation infrastructure early.
Advertisement