agentsope/agentsop-metric-design
>- Decomposed, multi-criteria metric design for LLM pipelines. The metric IS the model — change the metric and the optimizer changes behavior. Decompose by default; bool during LLM-as-judge, llm as judge, eval metric, evaluation score, scoring function, rubric, RAGAS, G-Eval, judge bias, verbosity bias, how to evaluate LLM output.
npx skills add https://github.com/agentsope/SkillAlchemy --skill agentsop-metric-design
> "It's unproductive to launch optimization runs using a poorly designed program or a bad metric."
> — DSPy core team [dspy.ai/learn/optimization/overview/]
> "LLM judges exhibit self-preference, recency, rubric-order, score-ID, and length biases."
> — Synthesized from [arxiv.org/pdf/2506.02592, arxiv.org/pdf/2509.26072]
This is a tool skill. It produces a metric function (and a calibration receipt) that other skills consume — DSPy compilers (MIPROv2 / GEPA / BootstrapFewShot), LlamaIndex FaithfulnessEvaluator/RelevancyEvaluator/RetrieverEvaluator, LangGraph eval judges, RAGAS, TruLens. The metric is the optimization target. Get it wrong and every downstream optimizer is theatre.
Activate when any of these is true:
metric(example, pred) -> bool|float. The metric drives gradient-free search; bias propagates into the artifact.OP-10 EvalLoop).Do NOT activate for:
dspy-sop will refuse to compile anyway; this skill cannot help).A DSPy/GEPA/MIPRO optimizer is a black-box search that maximizes metric(pred, example). Whatever the metric rewards, the compiled prompt will produce. If the metric prefers verbose, hedged answers (which an LLM judge will, by default — judges over-prefer length [arxiv.org/pdf/2506.02592]), the optimizer will produce verbose, hedged answers. A bad metric beats a good optimizer every time.
Three corollaries:
trace argument. Compile-time bool prevents the optimizer from chasing noise in the middle of the distribution; eval-time float gives gradient for reporting and debugging. [dspy.ai/learn/evaluation/metrics/]| Bias | What it does | Source |
|---|---|---|
| Length bias | Judges prefer longer answers regardless of quality | [arxiv.org/pdf/2506.02592] |
| Self-preference | A judge from family X prefers outputs from family X | [arxiv.org/pdf/2506.02592] |
| Recency / position | Last option in a pairwise rated higher | [arxiv.org/pdf/2509.26072] |
| Rubric-order | Criteria listed first weighted more | [arxiv.org/pdf/2509.26072] |
| Score-ID | "5" and "10" anchor differently across rubrics | [arxiv.org/pdf/2509.26072] |
| Provenance | Knowing the source model biases the rating | [arxiv.org/pdf/2506.02592] |
Full catalog in references/R2-judge-bias-catalog.md.
0. Confirm activation criteria (§1)
1. DECOMPOSE: list orthogonal criteria
2. CHOICE: bool vs float per criterion
3. WRITE: implement sub-judges + aggregator
4. BIAS-TEST: probe for length, position, self-preference
5. CALIBRATE: ≥20 human spot-checks; agreement ≥80%
6. SHIP: hand metric to optimizer / eval loop
Write down the criteria the user *actually* cares about. For an open-ended Q&A:
For RAG specifically, copy LlamaIndex's triad: Faithfulness (answer entailed by context), Relevancy (answer addresses query), Retriever quality (MRR / hit-rate on labeled QA pairs). See references/R1-source-evidence.md.
Output of this stage: a checklist of 3–6 sub-criteria, each with type (bool / scalar) and aggregation rule (AND for hard gates, weighted sum for soft).
Per [dspy.ai/learn/evaluation/metrics/]:
| Criterion shape | Return | Why |
|---|---|---|
| Hard requirement (factual, schema-valid, no PII) | bool | Optimizer should reject anything that fails, not partially credit |
| Soft preference (concise, fluent, on-tone) | float ∈ [0,1] | Optimizer benefits from gradient |
| Length | explicit scalar penalty, not LLM-rated | LLM judges over-prefer length; bake the penalty in deterministically |
Universal rule (DSPy OP-030): wrap the whole metric so it returns bool when trace is not None (compile mode) and float otherwise (eval mode). Same function, two semantics.
import dspy
class Assess(dspy.Signature):
"""Assess a single binary criterion. Reply yes/no."""
text_to_assess = dspy.InputField()
assessment_question = dspy.InputField()
assessment_answer: bool = dspy.OutputField()
def metric(example, pred, trace=None):
# Hard gates (bool sub-judges)
factual = dspy.Predict(Assess)(text_to_assess=pred.answer,
assessment_question="Is every factual claim supported by the context?").assessment_answer
on_topic = dspy.Predict(Assess)(text_to_assess=pred.answer,
assessment_question=f"Does this address the question: '{example.question}'?").assessment_answer
non_hedging = dspy.Predict(Assess)(text_to_assess=pred.answer,
assessment_question="Does this answer commit to a position (no 'it depends' / 'as an AI' hedging)?").assessment_answer
# Length penalty (deterministic — DO NOT delegate to judge)
over_budget = len(pred.answer.split()) > 150
length_score = 1.0 if not over_budget else max(0.0, 1.0 - (len(pred.answer.split()) - 150) / 150)
if trace is not None: # compile mode → strict bool
return factual and on_topic and non_hedging and not over_budget
# eval mode → float for reporting
return (factual + on_topic + non_hedging) / 3.0 * length_score
Rules:
Before calibration, run these probes on a 20-example dev set:
Document failures in references/R2-judge-bias-catalog.md for this project.
This is non-negotiable. Per DSPy Case C: "Never compile against a metric you haven't human-validated on ≥ 20 spot-checks." Garbage metric → garbage compiled program.
Hand off:
{n_spot_checks, per_judge_agreement, bias_probe_results, judge_model_id, task_model_id, date}. Stored next to the compiled artifact. Required for any later audit.dspy.GEPA for sample efficiency; otherwise MIPROv2.dspy.Predict(Assess) call per criterion. Aggregate with AND (hard) + weighted sum (soft).teleprompt.compile) and dspy.Evaluate.if trace is not None: return bool_aggregate; else: return float_aggregate.length_score = clip(1 - max(0, len - budget) / budget, 0, 1) multiplied into the final float. Do NOT delegate length judgment to the LLM.FaithfulnessEvaluator (answer entailed by retrieved context), RelevancyEvaluator (answer addresses query), RetrieverEvaluator(["mrr","hit_rate"]) (does retrieval pull the gold passage). Gate every PR.dspy.Prediction(score=float, feedback=str) from the metric. Pair with dspy.GEPA optimizer (not MIPROv2).{metric_version, n_spot_checks, per_judge_agreement, bias_probe_results, judge_model_id, task_model_id, date, criteria_list}.save(save_program=True) analog.困境: Team compiled an open-ended customer-support response program with MIPROv2(metric=llm_judge, auto="medium"). Auto-eval score climbed from 0.62 to 0.84. Production rolled out. Users complained answers were too long. Auditor traced it: the LLM judge gave +0.18 to answers >120 words across the eval set, including some objectively wrong ones. The optimizer faithfully chased that signal.
约束: Re-compile costs ~$30. Cannot re-label dataset. Cannot change judge model (vendor approval cycle).
决策步骤:
OP-M03) and decompose the holistic judge into factual / on-topic / non-hedging sub-judges (OP-M01). Length is now deterministic, not judge-rated.OP-M05). Agreement: was 64%, now 87%.结果: The "score regression" (0.84 → 0.79) was misleading — the old metric was the bug. Real quality improved because the metric finally matched the user.
可提取的操作: OP-M01 DecomposeMultiCriteria, OP-M03 ExplicitLengthPenalty, OP-M05 HumanSpotCheck20. Lesson: when a compiled program is "good on metric, bad in production", the metric is wrong. Don't tune harder — fix the metric.
困境: RAG pipeline reports Faithfulness = 1.0 across 200 eval QA pairs. Users complain answers don't address their questions. Team is confused: "but we're 100% faithful…"
约束: Cannot change evaluator vendor. Existing eval suite has only FaithfulnessEvaluator.
决策步骤:
FaithfulnessEvaluator only asks "is the answer entailed by retrieved context?" An answer of "The context discusses Q3 financials" is *faithful* to the context but does not *answer* "What was Q3 revenue?".RelevancyEvaluator (does the answer address the question?) and RetrieverEvaluator(["mrr", "hit_rate"]) (is the gold passage even retrieved?). [LlamaIndex OP-10]. This is OP-M06.结果: A single metric (faithfulness) created a blind spot. Decomposition exposed the actual failure (synthesizer hedging) which no single number could surface.
可提取的操作: OP-M01, OP-M06 RAGFaithfulnessRelevancyContext. Lesson: a single RAG metric, however precise, is structurally incomplete. Triad is the minimum.
困境: A vendor-tied cekura-metric-design skill (11 installs) wraps a single proprietary judge. Convenient, but: (a) cannot inspect the rubric, (b) cannot swap judge family, (c) cannot add length penalty, (d) calibration receipt does not include bias probes. Optimizer is chasing the vendor judge's blind spots.
决策步骤:
OP-M01). Add length penalty (OP-M03), non-hedging sub-judge, cross-family fact-check (OP-M04).OP-M05), not the vendor's alone.结果: Vendor metrics buy convenience but lose audit and bias control. Open, decomposed metrics dominate for any pipeline that will be optimized against.
可提取的操作: OP-M01, OP-M03, OP-M04, OP-M10. Lesson: a vendor-tied holistic metric is a managed optimizer target you cannot inspect. Decompose around it.
| # | Anti-pattern | Why it's wrong | Fix |
|---|---|---|---|
| AP-1 | Single holistic LLM-as-judge ("rate 1-5") as optimizer metric | Conflates orthogonal axes; inherits all judge biases; optimizer chases noise | Decompose into yes/no sub-judges (OP-M01) |
| AP-2 | No length penalty in a generation metric | Judges over-prefer length; optimizer learns to be verbose | Deterministic length term (OP-M03) |
| AP-3 | Same family for judge and task model | Self-preference bias inflates score 5–15pp | Cross-family judge (OP-M04) |
| AP-4 | Skipping human calibration | Optimizer learns the metric's bias, not the task | ≥20 spot-checks (OP-M05) |
| AP-5 | Throwing optimizers at a stalled compile | If metric is bad, no optimizer can save it | Return to metric design |
| AP-6 | Single-axis RAG metric (faithfulness only) | Misses query-relevance and retrieval-quality | Triad (OP-M06) |
| AP-7 | LLM-rated length ("is this concise?" as a judge call) | Inherits length bias; deterministic is free | Length is len(tokens), not a judge call |
| AP-8 | Hand-edited metric mid-experiment without re-calibration | Past compile artifacts now have invalid calibration receipts | Bump metric version; re-calibrate |
| AP-9 | Float metric in compile mode | Optimizer chases sub-percent noise; overfit | bool in compile, float in eval (OP-M02) |
| AP-10 | Compiling without a metric at all | Without a metric DSPy degenerates to verbose prompting | Refuse the compile (DSPy OP-024) |
scientific-critical-thinking for problem definition first.simpo / trl-fine-tuning skills, not this one.| Concept | DSPy | LlamaIndex | RAGAS | TruLens | This skill |
|---|---|---|---|---|---|
| Metric type | def metric(ex, pred, trace=None) -> float\|bool | EvaluationResult from BaseEvaluator | Metric class (Faithfulness, AnswerRelevancy, etc.) | Feedback function | metric function + calibration receipt |
| Multi-criteria | sub-judges via dspy.Predict(Assess) | stack of FaithfulnessEvaluator + RelevancyEvaluator + RetrieverEvaluator | metric ensemble | Feedback per criterion | OP-M01 DecomposeMultiCriteria |
| Mode switch (compile vs eval) | trace is not None returns bool | N/A (eval only) | N/A | N/A | OP-M02 |
| Faithfulness | sub-judge "every claim supported?" | FaithfulnessEvaluator (entailment-based) | Faithfulness metric (claim-by-claim NLI) | groundedness feedback | sub-judge or pre-built |
| Relevancy | sub-judge "addresses question?" | RelevancyEvaluator | AnswerRelevancy (embedding-based) | answer-relevance feedback | sub-judge |
| Retrieval quality | LlamaIndex RetrieverEvaluator MRR/hit-rate | RetrieverEvaluator(["mrr","hit_rate"]) | ContextPrecision, ContextRecall | context-relevance feedback | pre-built (use LlamaIndex / RAGAS) |
| Length penalty | manual scalar in metric | manual | manual | manual | OP-M03 (deterministic) |
| Textual feedback | dspy.Prediction(score, feedback) → GEPA | N/A | rationale strings | feedback rationale | OP-M08 |
| Cross-family judge | swap judge_lm in metric | swap service_context.llm of evaluator | swap evaluator LLM | swap feedback LLM | OP-M04 (always) |
Combination patterns:
FaithfulnessEvaluator + RelevancyEvaluator *as* the sub-judges inside a DSPy metric; aggregate to bool/float per OP-M02. (JetBlue/Databricks pattern.)Faithfulness / AnswerRelevancy / ContextPrecision as sub-scalars; pass into a DSPy metric for compile.Opinionated default: build the metric in pure Python with dspy.Predict(Assess) calls (transparent, version-controllable) and import LlamaIndex evaluators only for the RAG triad where the LlamaIndex evaluators are battle-tested. Avoid vendor SDKs whose rubrics you cannot inspect.
import dspy
class Assess(dspy.Signature):
"""One yes/no assessment."""
text = dspy.InputField()
question = dspy.InputField()
answer: bool = dspy.OutputField()
# Use a DIFFERENT family from the task model
judge_lm = dspy.LM("anthropic/claude-haiku-4") # task model is openai/gpt-4o
def make_metric(budget_words=150):
def metric(example, pred, trace=None):
with dspy.context(lm=judge_lm):
factual = dspy.Predict(Assess)(text=pred.answer,
question="Is every claim supported by the provided context?").answer
on_topic = dspy.Predict(Assess)(text=pred.answer,
question=f"Does this directly address: {example.question}?").answer
non_hedging = dspy.Predict(Assess)(text=pred.answer,
question="Does this commit to a position (no 'as an AI', no 'it depends')?").answer
n_words = len(pred.answer.split())
length_score = 1.0 if n_words <= budget_words else max(0.0, 1 - (n_words - budget_words)/budget_words)
if trace is not None:
return bool(factual and on_topic and non_hedging and n_words <= budget_words)
return (int(factual) + int(on_topic) + int(non_hedging)) / 3.0 * length_score
return metric
Then: bias-probe → human-calibrate → save receipt → ship.
references/R1-source-evidence.md — DSPy / LlamaIndex / LangGraph source quotesreferences/R2-judge-bias-catalog.md — Judge bias taxonomy + mitigationsintermediate/operation_candidates.json — Operation registryCitations: [dspy.ai/learn/evaluation/metrics/], [dspy.ai/learn/optimization/overview/], [dspy.ai/cheatsheet/], [dspy.ai/api/optimizers/GEPA/overview/], [arxiv.org/abs/2507.19457], [arxiv.org/pdf/2506.02592], [arxiv.org/pdf/2509.26072], [developers.llamaindex.ai/python/framework-api-reference/evaluation/], [cookbook.openai.com/examples/evaluation/evaluate_rag_with_llamaindex].
Take agentsope/agentsop-metric-design from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.