Metrics, judges & loss¶
A metric turns one answer into a number in [0, 1]. Optimization is only as
good as that number: if it rewards the wrong thing, the optimizer will find
prompts that do the wrong thing well. This page covers every metric, how LLM
judges work, how to combine them, and where each one plugs in.
Runnable companion: metrics_and_judges.py scores three fixed
answers with every metric below.
Three slots, one number each¶
| Slot | Set with | Used for |
|---|---|---|
| Reported metrics | agent.compile(metrics=[...]) |
Computed on train and validation every epoch (judge, val_judge, …), shown in History and the report. They do not steer anything. |
| Loss | agent.compile(loss=...) |
The number fit() records as loss / val_loss and EarlyStopping monitors. MetricLoss(metric) = 1 − score. |
| Objective | PromptFitterBridge(metric=...) or PromptFitter.fit(..., metric) |
What candidate prompts are selected on. |
Use the same underlying measure for the loss and the objective (for example
quality for both, as in the overview),
so the epoch curve and the selection agree. Without metric=, the bridge uses
your compiled loss's metric, else the first compiled metric.
Two metric families¶
Every API accepts either family; they are adapted to each other automatically.
agentomatic.agents (class agents) |
agentomatic.optimize (the fitter) |
|
|---|---|---|
| Signature | score(example, prediction) -> float (sync) |
await evaluate(query, response, expected, context) -> EvalResult |
| Sees | The whole AgentExample (input incl. context documents and other inputs, expected output, metadata, tags, rubric) and the output dict, the same in fit() and evaluate() |
The question, the answer text, the reference text, the context documents (or what the agent retrieved). Inside fit() / evaluate(), current_scoring_run() also exposes the example |
| Returns | A float | EvalResult(score, reason, metadata) — judges add dimensions, rationale; failures set metadata["evaluation_failed"] |
| Best for | Checks that need structured output or per-example metadata | Judges and text checks |
Adapters, if you need to cross explicitly:
from agentomatic.agents import OptimizeMetricAdapter, as_agent_metric
from agentomatic.optimize import as_optimize_metric
as_optimize_metric(ContainsTermsMetric(["30 days"])) # class metric → evaluate(...)
OptimizeMetricAdapter(judge, name="judge") # optimize metric → score(...), evaluate(...)
as_agent_metric(judge) # same, idempotent
Inside fit(), evaluate() and composite metrics, a class-agent metric
always receives the example's metadata and the agent's structured output,
whichever family it is wrapped in.
Deterministic metrics¶
Fast, free, reproducible. Prefer them wherever the answer has checkable facts or format.
| Metric | Family | Options | Scores |
|---|---|---|---|
ContainsTermsMetric(required_terms, case_sensitive=False, name=) |
agents | fixed term list | fraction of terms present in response |
ExactKeyMatchMetric(required_keys, name=) |
agents | output keys | fraction of keys present in the output dict |
ResponseSimilarityMetric(name=, fuzzy=True) |
agents | fuzzy similarity to expected_output["response"] |
|
CallableMetric(name, fn) |
agents | fn(example, prediction) -> float |
anything — e.g. example.metadata["must_include"] coverage |
ExactMatchMetric(fuzzy=True, threshold=0.8) |
optimize | fuzzy exact match vs the reference | |
ContainsMetric() |
optimize | reference = "kw1, kw2" |
fraction of comma-separated keywords present |
DeterministicMetric(name=, checks=[...]) |
optimize | check type: contains, not_contains, regex, min_length, max_length, json_valid, starts_with |
fraction of checks passed |
CustomMetric(fn, name=) |
optimize | fn(query, response, expected, context) -> float |
clamped to [0, 1]; NaN counts as a failure |
LatencyMetric(target_seconds=2.0, max_seconds=10.0) |
optimize | 1.0 under target, linear to 0 at max; uses the measured run time (fails outside a fit / evaluate run rather than guessing) | |
CostMetric(target_tokens=500, max_tokens=3000) |
optimize | token-usage penalty |
from agentomatic.agents import CallableMetric
def must_include(example, prediction):
facts = example.metadata.get("must_include") or []
answer = prediction.get("response", "").lower()
return sum(f.lower() in answer for f in facts) / len(facts) if facts else 0.0
facts = CallableMetric("facts", must_include)
LLM-as-judge¶
A judge asks a model to grade the answer against the reference. Use one when quality is open-ended (tone, completeness, grounding) and keep a deterministic metric next to it.
| Judge | Options | Returns |
|---|---|---|
LLMJudgeMetric(criteria, model, name="llm_judge", temperature=0.0, base_url=, api_key=) |
a rubric | one overall score + reason |
LocalJudgeMetric(name="local_judge", model, criteria, dimensions=[...], weight=1.0, temperature=0.0, base_url=, api_key=) |
a rubric + named dimensions | overall score, a score per dimension, feedback, motivation, what worked / failed, improvement hints |
MultiJudgePanel(judges=[...], aggregation="average", weights=None, name=) |
several LocalJudgeMetrics (different models or rubrics) |
one aggregated score + per-judge scores |
GEvalMetric, DeepEvalMetric, RedTeamMetric |
DeepEval metrics (pip install deepeval) |
DeepEval scores, oriented so higher is better |
from agentomatic.optimize import LocalJudgeMetric, MultiJudgePanel
judge = LocalJudgeMetric(
name="judge",
model="omlx/Qwen3.5-9B-MLX-4bit", # "provider/model"
criteria="Is the answer correct per the reference, grounded in the policy, "
"complete and concise? Score 0-1.",
dimensions=["correctness", "completeness", "concision"],
temperature=0.0, # same answer → same score every epoch
)
panel = MultiJudgePanel(
judges=[
judge,
LocalJudgeMetric(name="strict", model="openai/gpt-4.1-mini",
criteria="Penalise any claim not in the reference."),
],
aggregation="median", # "average" (weighted) | "median" | "min" | "max"
weights=None, # per judge, for "average"
)
How judges stay honest:
- Reference. Judges receive the example's expected answer (and, for
AgentExamples, a rich reference: expected output, rubric, required facts). Write expected answers that state the facts a correct answer must contain. - Context. Judges receive the row's context documents. If the row has
none, they get what the agent says it retrieved (
retrieval_context/citations/sourcesin its output). Insidefit()andevaluate()they also get the row's other inputs (a plan, a locale…), its metadata and its tags, in an "Example inputs, metadata and tags" section, so an answer is judged in that light. See what each stage sees. - Deterministic. Keep
temperature=0.0. A judge that scores the same answer differently each time makes every comparison noise; the significance rule exists because judges are noisy anyway. - Failures are failures. An unreachable judge or an unparsable reply
returns
score=0.0withmetadata["evaluation_failed"] = True, never a made-up 0.5. The fitter counts failed examples as 0 for every candidate, logs them, and flags an "evaluation blackout" when most calls fail. - Parsing. Replies are parsed tolerantly (JSON in prose or code fences,
score/overall_score/rating, 0–10 or 0–100 scales normalised). - Panels.
"median"is robust to one outlier judge;"min"is the strictest; failed judges are dropped, and the panel fails only if all do. - Endpoint.
model="omlx/…"routes tobase_url(or the process default set byLLMCaller.configure(base_url=...)/OMLX_BASE_URL).openai/…,ollama/…,gemini/…andlitellm/…are the other providers.
Combining metrics¶
Two equivalent tools; pick by family:
# optimize family — for PromptFitter
from agentomatic.optimize import CompositeMetric, WeightedMetric
objective = CompositeMetric(
[
WeightedMetric(name="judge", metric=judge, weight=0.6),
WeightedMetric(name="facts", metric=facts, weight=0.4), # either family
],
name="objective",
)
# agents family — for compile(metrics=..., loss=...) and PromptFitterBridge
from agentomatic.agents import WeightedMetric as AgentWeightedMetric
quality = AgentWeightedMetric([("judge", judge, 0.6), ("facts", facts, 0.4)], name="quality")
- Weights are normalised; negative weights are rejected.
- Each component is reported as a dimension, in the report and in
EvalResult.metadata["dimensions"]. - A failed component scores 0 (the denominator stays fixed, so a dead judge lowers the score instead of silently re-weighting the rest); the composite is marked failed when at least half its weight failed.
CompositeMetricalso hasscore(example, prediction), so it can sit incompile(metrics=[...])directly.
Losses¶
| Loss | Meaning |
|---|---|
MetricLoss(metric, name=None) |
1 − metric.score(...); accepts either family |
CallableLoss(fn, name="loss") |
fn(example, prediction) -> float, lower is better |
any object with compute(example, prediction) -> float |
custom |
fit() averages the loss over train (loss) and validation (val_loss)
after every epoch, plus a baseline row before the first epoch.
agents.EarlyStopping(monitor="val_loss") stops when it stops improving.
Choosing metrics¶
- Start with one deterministic metric that captures correctness for your data (required facts, keys, format). It is cheap, reproducible and cannot be sweet-talked.
- Add one judge with a short, specific rubric and 2–4 dimensions for the open-ended part of quality.
- Blend them (judge 0.5–0.7) into the objective, and use the same blend as the loss.
- Report the components separately (
compile(metrics=[judge, facts, quality])) so a gain in the blend that comes from only one part is visible. - Always evaluate the untouched test split before and after.