Traigent Evaluation Guide¶
How Traigent scores your runs and how to customize it.
Dataset Format¶
Use JSONL with input and optional output/expected_output keys (both are accepted):
{"input": {"question": "What is 2+2?"}, "output": "4"}
{"input": {"question": "Capital of France?"}, "output": "Paris"}
{"input": {"query": "Sentiment of: Great product!"}, "output": "positive"}
Minimal creator:
import json
data = [
{"input": {"question": "What is 2+2?"}, "output": "4"},
{"input": {"question": "Capital of France?"}, "output": "Paris"},
]
with open("data/qa_samples.jsonl", "w") as f:
for row in data:
f.write(json.dumps(row) + "\n")
Attaching a Dataset at Runtime¶
You can also assign the dataset after decoration instead of hard-coding
eval_dataset= in @traigent.optimize(...).
import asyncio
import traigent
from traigent.evaluators.base import Dataset, EvaluationExample
@traigent.optimize(
configuration_space={"model": ["gpt-4o-mini", "gpt-4o"]},
objectives=["accuracy"],
)
def qa_agent(question: str) -> str:
return answer_question(question)
qa_agent.eval_dataset = Dataset(
examples=[
EvaluationExample(
input_data={"question": "What is 2+2?"},
expected_output="4",
),
EvaluationExample(
input_data={"question": "Capital of France?"},
expected_output="Paris",
),
],
name="qa-smoke-test",
)
results = asyncio.run(qa_agent.optimize(max_trials=4))
What this does:
qa_agent.eval_datasetcan be a JSONL path, a list of JSONL paths, an inline list of example dicts, or aDatasetobject.- Traigent evaluates the dataset outside your function. Your function still accepts
one example's
input_datafields as normal arguments. - To swap evaluation sets between runs, assign a new value to
qa_agent.eval_datasetbefore the next.optimize()call.
Evaluation Modes¶
1) Default: exact / case-insensitive match¶
- Traigent's built-in
LocalEvaluatorscoresaccuracyby comparing the agent's output toexpected_outputusing exact string match, falling back to case-insensitive comparison. There is no embedding model and no LLM judge in this path — paraphrased answers will score 0.0 unless you supply your own scorer (see (2) below). - Good fit for: classification, span extraction, structured outputs, enum-like answers.
- Wrong fit for: Q&A, summarization, translation, or any task where multiple wordings are equally valid.
import litellm
import traigent
@traigent.optimize(
eval_dataset="data/classification_samples.jsonl",
objectives=["accuracy", "cost"],
)
def classifier(query: str) -> str:
response = litellm.completion(
model="gpt-4o-mini",
temperature=0.0,
messages=[{"role": "user", "content": f"Label: {query}"}],
)
return response.choices[0].message.content.strip()
Note on
evaluation_type: "semantic": If your dataset metadata tags examples as semantic but you don't pass ascoring_function,LocalExecutionAdapterwill mark those examples assuccess=Falsewith an explicit error and log at ERROR level. The contract is "fail loud", not "silently degrade to exact match".
2) Custom scoring functions¶
Use your own scoring function when exact rules or domain metrics matter.
def exact_match(output: str, expected: str) -> float:
return 1.0 if output.strip().lower() == expected.strip().lower() else 0.0
@traigent.optimize(
scoring_function=exact_match,
eval_dataset="data.jsonl",
objectives=["accuracy"],
)
def strict_agent(query: str) -> str:
...
3) Multiple metric functions¶
Use metric_functions to define multiple custom metrics with named keys:
def accuracy_metric(output: str, expected: str) -> float:
"""Return 1.0 for exact match, 0.0 otherwise."""
return 1.0 if output.strip().lower() == expected.strip().lower() else 0.0
def cost_metric(output: str, expected: str) -> float:
"""Simulate cost based on output length."""
# Approximate token count (4 chars ≈ 1 token)
output_tokens = max(len(output) // 4, 10)
input_tokens = 150 # Estimate for input
# GPT-4o-mini pricing per 1M tokens
input_cost = (input_tokens / 1_000_000) * 0.15
output_cost = (output_tokens / 1_000_000) * 0.60
return input_cost + output_cost
@traigent.optimize(
eval_dataset="data.jsonl",
objectives=["accuracy", "cost"],
metric_functions={
"accuracy": accuracy_metric,
"total_cost": cost_metric, # Use "total_cost" to track costs
},
)
def my_agent(query: str) -> str:
...
Note: Use metric_functions to provide custom metrics in mock mode where no real LLM calls occur.
4) Advanced custom evaluators¶
For full control over evaluation logic, use custom_evaluator which receives the full context:
from traigent.api.types import ExampleResult
def advanced_evaluator(func, config, example) -> ExampleResult:
"""Full control over evaluation with access to function, config, and example."""
output = func(**example.input)
exact = 1.0 if output == example.expected_output else 0.0
length_penalty = max(0.0, 1.0 - abs(len(output) - len(example.expected_output)) / 100)
return ExampleResult(
accuracy=exact,
metrics={"exact_match": exact, "length_score": length_penalty},
)
@traigent.optimize(
custom_evaluator=advanced_evaluator,
eval_dataset="data.jsonl",
)
def strict_agent(query: str) -> str:
...
Failure semantics: custom_evaluator vs metric_functions¶
| Situation | custom_evaluator |
metric_functions |
|---|---|---|
| Your code raises on one row | That row scores 0.0 on quality metrics (cost-family metrics stay unmeasured). The trial stays completed unless every row fails. When some but not all rows failed, the trial metadata records custom_evaluator_failed_rows and custom_evaluator_total_rows, and result.warning_codes contains CUSTOM_EVALUATOR_ROWS_FAILED. |
A metric function that raises on an objective metric fails the trial; on a non-objective metric the error is recorded in metric_errors and the metric is left out for that row. |
| All rows raise | The trial fails (no warning code; the failure itself is the signal). | Same for an objective metric. |
| A row omits an objective key | The trial fails with an EvaluationError naming the metric and the row count. No 0.0 is substituted. |
The trial fails; no 0.0 is substituted for an objective. |
The warning code carries counts only, never row inputs, outputs or exception text.
5) Mock mode¶
Call traigent.testing.enable_mock_mode_for_quickstart() near the top of local tutorial or test code to intercept external LLM/API calls and replace them with canned/deterministic responses. Your evaluator (custom or the built-in LocalEvaluator) still scores those canned responses with its real scoring logic - the SDK no longer synthesizes metrics. Ideal for CI, demos, and budget-safe smoke tests.
from traigent.testing import enable_mock_mode_for_quickstart
enable_mock_mode_for_quickstart()
For shell-only fixtures, TRAIGENT_MOCK_LLM=true remains available outside production for backwards compatibility, but direct user-set activation emits DeprecationWarning.
Reading trial results — the trial-metrics contract¶
Each TrialResult (the items in result.trials) exposes several score-like
values. They do not all mean the same thing:
| Field | Meaning |
|---|---|
trial.score |
The trial's optimization signal — the exact value best_config argmaxes. For a single-objective run it is the objective metric (e.g. accuracy); for a weighted multi-objective run it is the weighted selection basis. Read this when you want "the trial's score". |
trial.metrics["score"] |
Same optimization signal, kept in the metrics map for serialization parity. |
trial.metrics["accuracy"] (and other objective keys) |
The individual metric values. When a custom scoring_function defines the objective, metrics["accuracy"] is the custom scorer's mean. |
trial.metrics["exact_match_default"] |
Present only when a custom scoring_function owns the accuracy objective. It holds the SDK's built-in case-insensitive exact-match scorer as a diagnostic — it is never the optimization signal and is not what best_config uses. |
result = my_agent.optimize_sync(max_trials=10)
for trial in result.trials:
# Correct: trial.score is the signal the optimizer used.
print(trial.config, trial.score)
Before this contract was fixed (issue #1845), trial.score was None and
metrics["score"] held the built-in exact-match scorer even when a custom
scoring_function was supplied — so a consumer doing
trial.score or trial.metrics["score"] would log a number the optimizer never
used, disagreeing with best_config whenever the custom scorer was non-binary.
Troubleshooting¶
- Paraphrased answers score 0.0: The default
LocalEvaluatoraccuracy is exact / case-insensitive match; it is not semantic. Provide your ownscoring_function(ormetric_functions={"accuracy": ...}) that performs embedding similarity or LLM-judge scoring.LocalExecutionAdapteralso raises a visible error when dataset examples are taggedevaluation_type: "semantic"without a configured scorer. - Scores all zeros: Check that dataset
output/expected_outputvalues are non-empty strings. - Dataset errors: Run
traigent validate path/to/dataset.jsonlto see the exact row/field causing issues.
Custom Evaluators - Advanced Patterns¶
Pattern 1: RAG Quality Evaluator¶
def token_overlap(output: str, expected: str) -> float:
output_tokens = {token.lower() for token in str(output).split()}
expected_tokens = {token.lower() for token in str(expected).split()}
if not expected_tokens:
return 0.0
return len(output_tokens & expected_tokens) / len(expected_tokens)
def rag_evaluator(output: str, expected: str, context: dict[str, object]) -> float:
retrieved_docs = context.get("retrieved_docs", [])
context_text = " ".join(str(doc) for doc in retrieved_docs)
answer_similarity = token_overlap(output, expected)
context_support = token_overlap(output, context_text)
return 0.7 * answer_similarity + 0.3 * context_support
Pattern 2: Classification Evaluator¶
from sklearn.metrics import f1_score, precision_score, recall_score
def classification_evaluator(output: str, expected: str) -> Dict[str, float]:
"""Multi-class classification evaluator"""
# Parse outputs
predicted_class = extract_class(output)
true_class = extract_class(expected)
# Store for batch metrics
predictions.append(predicted_class)
true_labels.append(true_class)
# Calculate instance metrics
correct = 1.0 if predicted_class == true_class else 0.0
return {
"accuracy": correct,
"precision": precision_score([true_class], [predicted_class], average='weighted'),
"recall": recall_score([true_class], [predicted_class], average='weighted'),
"f1": f1_score([true_class], [predicted_class], average='weighted')
}
Pattern 3: Code Generation Evaluator¶
import ast
import subprocess
def code_evaluator(output: str, expected: str, context: Dict[str, Any]) -> float:
"""Evaluate generated code quality"""
test_cases = context.get("test_cases", [])
# Check syntax validity
try:
ast.parse(output)
syntax_valid = 1.0
except SyntaxError:
return 0.0 # Invalid syntax = fail
# Run test cases
passed_tests = 0
for test_input, test_output in test_cases:
try:
result = execute_code(output, test_input)
if result == test_output:
passed_tests += 1
except Exception:
continue
test_score = passed_tests / len(test_cases) if test_cases else 0.0
# Compare with expected solution
similarity = code_similarity(output, expected)
return 0.5 * test_score + 0.3 * similarity + 0.2 * syntax_valid
Common Issues and Solutions¶
Issue: 0.0% Accuracy¶
Symptoms:
Trial 1/10: accuracy=0.00, cost=$0.15
Trial 2/10: accuracy=0.00, cost=$0.12
...
Best accuracy: 0.00
Solutions:
-
Verify Dataset Format
import json # Check first line of dataset with open("data/qa_samples.jsonl", "r") as f: first_line = f.readline() data = json.loads(first_line) print(f"Keys: {data.keys()}") print(f"Input: {data.get('input')}") print(f"Expected: {data.get('expected_output')}") -
Check API Keys
# Check whether an OpenAI API key is configured for embeddings if printenv OPENAI_API_KEY | grep -q .; then echo "OPENAI_API_KEY is set" else echo "OPENAI_API_KEY is not set" fi # Or use custom evaluator if no API key -
Use Custom Scoring Function
def debug_scorer(output: str, expected: str) -> float: print(f"Output: '{output}'") print(f"Expected: '{expected}'") score = 1.0 if output.lower() == expected.lower() else 0.0 print(f"Score: {score}") return score @traigent.optimize( scoring_function=debug_scorer, eval_dataset="data.jsonl", ) -
Enable Debug Mode
@traigent.optimize( debug_evaluation=True, # Print detailed eval info eval_dataset="data.jsonl" )
Issue: Inconsistent Accuracy¶
Symptoms: Accuracy varies wildly between runs (20% one run, 80% next run)
Solutions:
-
Fix Random Seeds
import random import numpy as np random.seed(42) np.random.seed(42) @traigent.optimize( random_seed=42, # Deterministic optimization eval_dataset="data.jsonl" ) -
Increase Sample Size
# Use more evaluation examples # Minimum recommended: 50-100 examples -
Use Temperature=0 for Deterministic Output
configuration_space={ "model": ["gpt-4o-mini", "gpt-4o"], "temperature": [0.0] # Deterministic }
Issue: Evaluation Too Slow¶
Solutions:
-
Use Smaller Dataset
from traigent.evaluators.base import Dataset # Sample subset for faster evaluation full_dataset = Dataset.from_jsonl("large_dataset.jsonl") sample_dataset = full_dataset.sample(n=100, random_state=42) -
Enable Parallel Evaluation
@traigent.optimize( parallel_config={ "example_concurrency": 8, # Evaluate 8 examples in parallel "trial_concurrency": 4 # Run 4 trials in parallel } ) -
Use Mock Mode for Testing
from traigent.testing import enable_mock_mode_for_quickstart enable_mock_mode_for_quickstart()
Issue: Out of Memory¶
Solutions:
-
Use a Smaller Dataset Subset
@traigent.optimize( eval_dataset="large_dataset_subset.jsonl" # Use a representative subset ) -
Use Streaming Evaluation
from traigent.evaluators.base import StreamingDataset dataset = StreamingDataset.from_jsonl("large_dataset.jsonl")
Best Practices¶
1. Dataset Quality¶
- Size: Minimum 50-100 examples, ideal 500-1000
- Coverage: Cover all edge cases and common scenarios
- Balance: Ensure balanced distribution across categories
- Quality: Manually review and validate expected outputs
2. Scoring Function Selection¶
- Default (Exact / Case-Insensitive Match): Best for classification, span extraction, and structured outputs. Does not tolerate paraphrasing — supply a custom
scoring_functionfor that. scoring_function: Single custom scorer for domain-specific metricsmetric_functions: Multiple named metrics (accuracy, cost, latency, etc.)custom_evaluator: Full control with access to function, config, and example context
3. Optimization Settings¶
@traigent.optimize(
eval_dataset="validated_dataset.jsonl",
objectives=["accuracy", "cost"],
# Use custom metrics (choose one approach):
# Option 1: Single scoring function
scoring_function=my_scorer,
# Option 2: Multiple named metrics
metric_functions={
"accuracy": accuracy_metric,
"total_cost": cost_metric,
},
# Enable debugging during development
debug_evaluation=True,
# Set reasonable limits
max_trials=50,
timeout_per_trial=60, # seconds
# Enable parallel processing
parallel_config={
"example_concurrency": 4,
"trial_concurrency": 2
}
)
4. Iteration Workflow¶
- Start Small: Test with 10-20 examples in mock mode
- Validate: Ensure evaluator works correctly
- Scale Up: Increase to 100+ examples
- Optimize: Run full optimization with real API calls
- Verify: Manually review best configuration results
Related Documentation¶
Need Help? - GitHub Issues - Discord Community - Documentation