Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement comprehensive evaluation strategies for LLM applications using automated metrics, human feedback, and benchmarking. Use when testing LLM performance, measuring AI application quality, or establishing evaluation frameworks.
.claude/skills/microck-llm-evaluation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 156% | 0% |
| case-12 | ✓→✗ | ▼ Worse | 629% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 89% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 196% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 143% | 0% |
Master comprehensive evaluation strategies for LLM applications, from automated metrics to human evaluation and A/B testing.
Fast, repeatable, scalable evaluation using computed scores.
Text Generation:
Classification:
Retrieval (RAG):
Manual assessment for quality aspects difficult to automate.
Dimensions:
Use stronger LLMs to evaluate weaker model outputs.
Approaches:
pythonfrom llm_eval import EvaluationSuite, Metric # Define evaluation suite suite = EvaluationSuite([ Metric.accuracy(), Metric.bleu(), Metric.bertscore(), Metric.custom(name="groundedness", fn=check_groundedness) ]) # Prepare test cases test_cases = [ { "input": "What is the capital of France?", "expected": "Paris", "context": "France is a country in Europe. Paris is its capital." }, # ... more test cases ] # Run evaluation results = suite.evaluate( model=your_model, test_cases=test_cases ) print(f"Overall Accuracy: {results.metrics['accuracy']}") print(f"BLEU Score: {results.metrics['bleu']}")
pythonfrom nltk.translate.bleu_score import sentence_bleu, SmoothingFunction def calculate_bleu(reference, hypothesis): """Calculate BLEU score between reference and hypothesis.""" smoothie = SmoothingFunction().method4 return sentence_bleu( [reference.split()], hypothesis.split(), smoothing_function=smoothie ) # Usage bleu = calculate_bleu( reference="The cat sat on the mat", hypothesis="A cat is sitting on the mat" )
pythonfrom rouge_score import rouge_scorer def calculate_rouge(reference, hypothesis): """Calculate ROUGE scores.""" scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True) scores = scorer.score(reference, hypothesis) return { 'rouge1': scores['rouge1'].fmeasure, 'rouge2': scores['rouge2'].fmeasure, 'rougeL': scores['rougeL'].fmeasure }
pythonfrom bert_score import score def calculate_bertscore(references, hypotheses): """Calculate BERTScore using pre-trained BERT.""" P, R, F1 = score( hypotheses, references, lang='en', model_type='microsoft/deberta-xlarge-mnli' ) return { 'precision': P.mean().item(), 'recall': R.mean().item(), 'f1': F1.mean().item() }
pythondef calculate_groundedness(response, context): """Check if response is grounded in provided context.""" # Use NLI model to check entailment from transformers import pipeline nli = pipeline("text-classification", model="microsoft/deberta-large-mnli") result = nli(f"{context} [SEP] {response}")[0] # Return confidence that response is entailed by context return result['score'] if result['label'] == 'ENTAILMENT' else 0.0 def calculate_toxicity(text): """Measure toxicity in generated text.""" from detoxify import Detoxify results = Detoxify('original').predict(text) return max(results.values()) # Return highest toxicity score def calculate_factuality(claim, knowledge_base): """Verify factual claims against knowledge base.""" # Implementation depends on your knowledge base # Could use retrieval + NLI, or fact-checking API pass
pythondef llm_judge_quality(response, question): """Use GPT-5 to judge response quality.""" prompt = f"""Rate the following response on a scale of 1-10 for: 1. Accuracy (factually correct) 2. Helpfulness (answers the question) 3. Clarity (well-written and understandable) Question: {question} Response: {response} Provide ratings in JSON format: {{ "accuracy": <1-10>, "helpfulness": <1-10>, "clarity": <1-10>, "reasoning": "<brief explanation>" }} """ result = openai.ChatCompletion.create( model="gpt-5", messages=[{"role": "user", "content": prompt}], temperature=0 ) return json.loads(result.choices[0].message.content)
pythondef compare_responses(question, response_a, response_b): """Compare two responses using LLM judge.""" prompt = f"""Compare these two responses to the question and determine which is better. Question: {question} Response A: {response_a} Response B: {response_b} Which response is better and why? Consider accuracy, helpfulness, and clarity. Answer with JSON: {{ "winner": "A" or "B" or "tie", "reasoning": "<explanation>", "confidence": <1-10> }} """ result = openai.ChatCompletion.create( model="gpt-5", messages=[{"role": "user", "content": prompt}], temperature=0 ) return json.loads(result.choices[0].message.content)
pythonclass AnnotationTask: """Structure for human annotation task.""" def __init__(self, response, question, context=None): self.response = response self.question = question self.context = context def get_annotation_form(self): return { "question": self.question, "context": self.context, "response": self.response, "ratings": { "accuracy": { "scale": "1-5", "description": "Is the response factually correct?" }, "relevance": { "scale": "1-5", "description": "Does it answer the question?" }, "coherence": { "scale": "1-5", "description": "Is it logically consistent?" } }, "issues": { "factual_error": False, "hallucination": False, "off_topic": False, "unsafe_content": False }, "feedback": "" }
pythonfrom sklearn.metrics import cohen_kappa_score def calculate_agreement(rater1_scores, rater2_scores): """Calculate inter-rater agreement.""" kappa = cohen_kappa_score(rater1_scores, rater2_scores) interpretation = { kappa < 0: "Poor", kappa < 0.2: "Slight", kappa < 0.4: "Fair", kappa < 0.6: "Moderate", kappa < 0.8: "Substantial", kappa <= 1.0: "Almost Perfect" } return { "kappa": kappa, "interpretation": interpretation[True] }
pythonfrom scipy import stats import numpy as np class ABTest: def __init__(self, variant_a_name="A", variant_b_name="B"): self.variant_a = {"name": variant_a_name, "scores": []} self.variant_b = {"name": variant_b_name, "scores": []} def add_result(self, variant, score): """Add evaluation result for a variant.""" if variant == "A": self.variant_a["scores"].append(score) else: self.variant_b["scores"].append(score) def analyze(self, alpha=0.05): """Perform statistical analysis.""" a_scores = self.variant_a["scores"] b_scores = self.variant_b["scores"] # T-test t_stat, p_value = stats.ttest_ind(a_scores, b_scores) # Effect size (Cohen's d) pooled_std = np.sqrt((np.std(a_scores)**2 + np.std(b_scores)**2) / 2) cohens_d = (np.mean(b_scores) - np.mean(a_scores)) / pooled_std return { "variant_a_mean": np.mean(a_scores), "variant_b_mean": np.mean(b_scores), "difference": np.mean(b_scores) - np.mean(a_scores), "relative_improvement": (np.mean(b_scores) - np.mean(a_scores)) / np.mean(a_scores), "p_value": p_value, "statistically_significant": p_value < alpha, "cohens_d": cohens_d, "effect_size": self.interpret_cohens_d(cohens_d), "winner": "B" if np.mean(b_scores) > np.mean(a_scores) else "A" } @staticmethod def interpret_cohens_d(d): """Interpret Cohen's d effect size.""" abs_d = abs(d) if abs_d < 0.2: return "negligible" elif abs_d < 0.5: return "small" elif abs_d < 0.8: return "medium" else: return "large"
pythonclass RegressionDetector: def __init__(self, baseline_results, threshold=0.05): self.baseline = baseline_results self.threshold = threshold def check_for_regression(self, new_results): """Detect if new results show regression.""" regressions = [] for metric in self.baseline.keys(): baseline_score = self.baseline[metric] new_score = new_results.get(metric) if new_score is None: continue # Calculate relative change relative_change = (new_score - baseline_score) / baseline_score # Flag if significant decrease if relative_change < -self.threshold: regressions.append({ "metric": metric, "baseline": baseline_score, "current": new_score, "change": relative_change }) return { "has_regression": len(regressions) > 0, "regressions": regressions }
pythonclass BenchmarkRunner: def __init__(self, benchmark_dataset): self.dataset = benchmark_dataset def run_benchmark(self, model, metrics): """Run model on benchmark and calculate metrics.""" results = {metric.name: [] for metric in metrics} for example in self.dataset: # Generate prediction prediction = model.predict(example["input"]) # Calculate each metric for metric in metrics: score = metric.calculate( prediction=prediction, reference=example["reference"], context=example.get("context") ) results[metric.name].append(score) # Aggregate results return { metric: { "mean": np.mean(scores), "std": np.std(scores), "min": min(scores), "max": max(scores) } for metric, scores in results.items() }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | pass→pass | 17,560 | 17,606 | +0% | 1 | 1 | 0% | 3,466 | 6,556 | +89% | 0 | 0 | — |
case-01 | fail→pass | 12,177 | 10,289 | -16% | 1 | 1 | 0% | 2,113 | 5,405 | +156% | 0 | 0 | — |
case-02 | pass→pass | 8,382 | 6,958 | -17% | 1 | 1 | 0% | 1,647 | 4,870 | +196% | 0 | 0 | — |
case-03 | pass→pass | 10,558 | 7,173 | -32% | 1 | 1 | 0% | 1,957 | 4,759 | +143% | 0 | 0 | — |
case-05 | pass→pass | 16,189 | 14,101 | -13% | 1 | 1 | 0% | 2,897 | 5,983 | +107% | 0 | 0 | — |
case-06 | fail→fail | 13,794 | 11,298 | -18% | 1 | 1 | 0% | 2,336 | 5,323 | +128% | 0 | 0 | — |
case-07 | pass→pass | 11,181 | 11,451 | +2% | 1 | 1 | 0% | 1,849 | 5,347 | +189% | 0 | 0 | — |
case-08 | pass→pass | 5,689 | 7,165 | +26% | 1 | 1 | 0% | 1,107 | 4,884 | +341% | 0 | 0 | — |
case-09 | pass→pass | 14,059 | 11,559 | -18% | 1 | 1 | 0% | 2,681 | 5,714 | +113% | 0 | 0 | — |
case-10 | pass→pass | 11,354 | 9,279 | -18% | 1 | 1 | 0% | 2,109 | 5,265 | +150% | 0 | 0 | — |
case-11 | pass→pass | 3,823 | 4,686 | +23% | 1 | 1 | 0% | 643 | 4,292 | +567% | 0 | 0 | — |
case-12 | pass→fail | 3,211 | 3,827 | +19% | 1 | 1 | 0% | 558 | 4,068 | +629% | 0 | 0 | — |
case-13 | fail→fail | 18,786 | 19,877 | +6% | 1 | 1 | 0% | 4,005 | 6,762 | +69% | 0 | 0 | — |
case-14 | pass→pass | 17,009 | 24,591 | +45% | 1 | 1 | 0% | 3,155 | 8,729 | +177% | 0 | 0 | — |
case-15 | pass→pass | 13,689 | 8,958 | -35% | 1 | 1 | 0% | 2,462 | 4,971 | +102% | 0 | 0 | — |
case-16 | pass→pass | 9,158 | 5,409 | -41% | 1 | 1 | 0% | 1,438 | 4,302 | +199% | 0 | 0 | — |
case-17 | pass→pass | 20,056 | 11,522 | -43% | 1 | 1 | 0% | 3,442 | 5,569 | +62% | 0 | 0 | — |
case-18 | fail→fail | 9,295 | 6,200 | -33% | 1 | 1 | 0% | 1,536 | 4,477 | +191% | 0 | 0 | — |
case-19 | pass→pass | 7,051 | 5,090 | -28% | 1 | 1 | 0% | 1,150 | 4,230 | +268% | 0 | 0 | — |
case-20 | pass→pass | 15,532 | 16,409 | +6% | 1 | 1 | 0% | 2,938 | 6,683 | +127% | 0 | 0 | — |
case-21 | pass→pass | 10,198 | 9,000 | -12% | 1 | 1 | 0% | 1,988 | 5,375 | +170% | 0 | 0 | — |
case-22 | pass→pass | 15,040 | 10,604 | -29% | 1 | 1 | 0% | 2,776 | 5,571 | +101% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.