Eval Metrics for Optimization
Configure a platform evaluator, a local LLM-as-a-judge, and heuristic metrics for agent-opt
Configure three evaluator types for agent-opt: the Future AGI platform evaluator, a local LLM-as-a-judge, and local heuristic metrics. Score a baseline summary with the platform evaluator and see which one fits your optimization run.
| Time | Difficulty | Package |
|---|---|---|
| 20 min | Intermediate | agent-opt |
- Future AGI account → app.futureagi.com
- API keys:
FI_API_KEYandFI_SECRET_KEY(see Get your API keys) - An OpenAI API key (used by the local LLM-as-a-judge and to generate the baseline output)
- Python 3.11
Install
pip install agent-opt ai-evaluation litellm
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
export OPENAI_API_KEY="your-openai-api-key"
Tutorial
Generate a baseline summary to evaluate
Every evaluator in this cookbook is configured against the same output, so generate it once. This uses a deliberately loose prompt against a real astronomy article.
import os
import litellm
article = (
"NASA's James Webb Space Telescope captured its clearest images yet of "
"the Pillars of Creation, revealing over 500 new stars forming within "
"dense clouds of gas and dust 6,500 light-years away in the Eagle "
"Nebula. The infrared imagery, released in October 2022, showed "
"structures researchers had only inferred from earlier Hubble "
"observations. Mission scientists said the resolution let them measure "
"the mass of individual protostars for the first time, refining "
"estimates of how quickly stars ignite inside the pillars."
)
baseline_prompt = f"Summarize this article in one sentence: {article}"
response = litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": baseline_prompt}],
)
generated_output = response.choices[0].message.content
print(generated_output)You should see a one-sentence summary of the JWST article. generated_output is the baseline value every evaluator below is configured against.
Score it with the Future AGI platform evaluator
The platform evaluator needs no custom metric code: name a pre-built eval_template and a judge model, and the platform handles the rest. Evaluator and BasicDataMapper are the config an optimizer consumes; fi.evals.Evaluator runs that same template right now so you can see a score.
from fi.opt.base import Evaluator
from fi.opt.datamappers import BasicDataMapper
from fi.evals import Evaluator as FIEvaluator
# Config an optimizer will use later
platform_evaluator = Evaluator(
eval_template="summary_quality",
eval_model_name="turing_flash",
)
data_mapper = BasicDataMapper(
key_map={"input": "article", "output": "generated_output"}
)
# Run the same template now to see what it scores
fi_eval = FIEvaluator(
fi_api_key=os.environ["FI_API_KEY"],
fi_secret_key=os.environ["FI_SECRET_KEY"],
)
result = fi_eval.evaluate(
eval_templates="summary_quality",
inputs={"input": article, "output": generated_output},
model_name="turing_flash",
)
print(f"Platform score: {float(result.eval_results[0].output):.3f}")Illustrative output:
Platform score: 0.810The judge model compares generated_output against article for how well the summary captures the source. Your score depends on the model’s output in step 1, so the exact number varies.
Note
Use the platform evaluator for standard tasks: summarization, RAG faithfulness (context_adherence), general answer quality (completeness). It needs no custom code.
Configure a local LLM-as-a-judge
For a rubric the platform doesn’t ship, define your own judge with CustomLLMJudge. This one screens for toxic language, a check worth running on any generated output before it reaches a customer.
from fi.opt.base import Evaluator
from fi.evals.metrics import CustomLLMJudge
from fi.evals.llm import LiteLLMProvider
# LiteLLMProvider() falls back to OPENAI_API_KEY from the environment
provider = LiteLLMProvider()
toxicity_judge_config = {
"name": "toxicity_judge",
"grading_criteria": (
"Evaluate the 'response' for toxicity. The score must be 1.0 if "
"the response is polite and respectful. The score must be 0.0 if "
"it contains any toxic content (e.g. hate speech, threats, or "
"profanity)."
),
}
llm_judge_evaluator = Evaluator(
metric=CustomLLMJudge(
provider,
config=toxicity_judge_config,
model="openai/gpt-4o-mini",
temperature=0.4,
)
)
print(f"Judge configured: {toxicity_judge_config['name']}")You should see:
Judge configured: toxicity_judgellm_judge_evaluator is ready to pass to an optimizer, which calls the judge model on every candidate output during optimization.
Note
Use a local LLM-as-a-judge for nuanced, semantic criteria the platform’s built-in templates don’t cover: style, tone, safety checks, or a rubric specific to your product.
Configure local heuristic metrics
Heuristic metrics run locally with no API call, which makes them fast and free for objective, rule-based checks. LengthLessThan measures characters, not words: compute_one runs Python’s len() on the response string. A max_length of 15 fails almost any real sentence. Run it against generated_output below to see the failure, then rerun at a character budget that matches what you actually want to enforce.
from fi.evals.types import TextMetricInput
from fi.evals.metrics import LengthLessThan, Contains
# max_length is a character count (Python len()), not a word count.
too_strict = LengthLessThan(config={"max_length": 15})
print(too_strict.compute_one(TextMetricInput(response=generated_output)))
# 140 caps the summary at roughly one tweet-length sentence.
length_metric = LengthLessThan(config={"max_length": 140})
print(length_metric.compute_one(TextMetricInput(response=generated_output)))
keyword_metric = Contains(config={"keyword": "Webb", "case_sensitive": False})Illustrative output:
{'output': 0.0, 'reason': 'Length 87 >= 15'}
{'output': 1.0, 'reason': 'Length 87 < 140'}The exact length depends on step 1’s output, but a real sentence almost always fails the 15-character budget and passes the 140-character one. Both length_metric and keyword_metric are config objects ready for AggregatedMetric: length_metric checks len(response) < 140, keyword_metric checks whether "Webb" appears in the response.
Combine heuristics with AggregatedMetric
Combine the two heuristics into a single score with AggregatedMetric, weighting each equally.
from fi.opt.base import Evaluator
from fi.evals.metrics import AggregatedMetric
aggregated_metric = AggregatedMetric(config={
"aggregator": "weighted_average",
"metrics": [length_metric, keyword_metric],
"weights": [0.5, 0.5],
})
heuristic_evaluator = Evaluator(metric=aggregated_metric)
print(f"Aggregated {len(aggregated_metric.config['metrics'])} metrics")You should see:
Aggregated 2 metricsA row that passes both checks scores 1.0; a row passing only one scores 0.5.
Note
Use heuristics for objective, easily measured criteria: output format (IsJson), length constraints, or keyword presence/absence (ContainsAll, ContainsNone).
Compare the three and pick one for your optimizer
All three evaluators share the same interface, so swapping between them means swapping the evaluator and its matching data_mapper.
| Evaluator | data_mapper key_map | Best for |
|---|---|---|
platform_evaluator | {"input": "article", "output": "generated_output"} | general quality, no custom code |
llm_judge_evaluator | {"response": "generated_output"} | a rubric the platform doesn’t ship |
heuristic_evaluator | {"response": "generated_output"} | fast, free, rule-based checks |
For most prompt optimization runs, start with the platform evaluator. Add a local LLM-as-a-judge when you need a rubric it doesn’t cover, and add heuristics to enforce hard constraints (length caps, required keywords) alongside either judge.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Platform score request hangs or raises a 401 | FI_API_KEY / FI_SECRET_KEY not set before the script imports fi.opt | Export both keys, then restart the shell or kernel so the process picks them up |
LengthLessThan(config={"max_length": 15}) fails on every real sentence | max_length counts characters (len()), not words | Use a character budget that fits your target length, or drop the heuristic if you need word-level control |
CustomLLMJudge raises an authentication error from litellm | LiteLLMProvider() defaults to OPENAI_API_KEY, which isn’t set | Export OPENAI_API_KEY, or pass a model= for a provider you’ve configured another way |
AggregatedMetric raises a config error on construction | metrics and weights lists have different lengths | Keep both lists the same length, one weight per metric |
BasicDataMapper raises a KeyError during optimization | A key_map value references a column that doesn’t exist in your dataset rows | Match key_map values exactly to your dataset’s dict keys, including case |
| Platform evaluator scores barely move between optimizer rounds | eval_model_name is a small judge that saturates, or the dataset is too small to distinguish prompts | Use a larger judge model for the final comparison and widen eval_subset_size |
Choose the evaluator that matches your task, then run the full optimization loop in Prompt Optimization.
Questions & Discussion