Context-derived, calibrated, evolving evals for LLM/agent systems
Project description
Benchrail
Know whether your AI agent actually got better.
Benchrail is a framework for building evals for LLM and agent systems. You change a prompt, a skill, a tool, a model — and today you have no trustworthy signal for whether the system improved against intended output. Eyeballing a few runs is anecdote, and non-determinism means "it looked better" might be luck. Benchrail gives you a domain-true standard to measure against.
Benchrail gives AI teams two things they need during change: a benchmark that defines quality and a release rail that prevents regressions from slipping through.
Research grounding
Benchrail is opinionated, but not "trust me bro" opinionated. The design follows several findings from AI evaluation research:
- Use multiple, context-specific metrics. Stanford CRFM's HELM argues for evaluating language models across scenarios and metrics beyond accuracy, including calibration, robustness, fairness, bias, toxicity, and efficiency. Benchrail applies that idea at the product level: each AI system gets its own benchmark and quality dimensions.
- Do not rely on public leaderboard scores as product truth. The Leaderboard Illusion documents how public model rankings can be distorted by selective disclosure, asymmetric data access, and overfitting to leaderboard dynamics. Benchrail is built around private, customer-owned evals tied to the actual system under test.
- LLM judges are useful, but only with calibration. Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena shows strong LLM judges can approximate human preferences, while also noting biases such as position, verbosity, and self-enhancement. Benchrail therefore treats judges as candidates until they clear human-label calibration.
- Evaluation belongs in the lifecycle, not just the launch checklist. The NIST AI Risk Management Framework frames trustworthy AI work through governance, mapping, measurement, and management. Benchrail mirrors that lifecycle with benchmark history, calibration freshness, regression checks, and failure-driven evolution.
- Nondeterministic systems need repeated measurement.
OpenAI's HumanEval paper helped popularize
sampling-aware evaluation for code generation. Benchrail carries that lesson
into product evals with multi-sample change evaluation and explicit
inconclusivestates.
The idea
Most eval tools make you hand-author every metric, then leave the eval set to rot as your product and business evolve. Benchrail instead treats the eval suite as a calibrated, evolving grading system derived from your business context:
declare context → derive the right rubric → compile + calibrate judges → run → evolve from discovered failures
- Domain-derived rubrics. Declare your business context (what the agent does, what "good" means, what failure modes matter) and Benchrail reasons out the correct set of grading criteria for it. The rubric a D2C company needs is not the rubric a logistics company needs — and a generic "accuracy" metric misses both.
- Calibrated judges. Each rubric compiles into LLM judges that are validated against a human gold set (agreement metrics, not vibes). A judge that can't beat the agreement bar doesn't get to gate anything.
- Evolves from real failures. The derived rubric is a prior — a hypothesis about what matters. Discovered production failures are the posterior that corrects it. Write evaluators for failures you discover, not ones you imagine.
- Judge-drift aware. Calibration isn't one-and-done; Benchrail watches for judges drifting out of agreement over time.
Status
Early, but the wedge is now runnable. The first slice proves domain derivation
end to end: a declared Context Spec → an LLM-reasoned Rubric (with a per-criterion
why traced to the spec) → a measured check that two domains genuinely diverge.
The product spine is now a custom benchmark lifecycle: define intended output, create cases, run a baseline, compare changes, and evolve the benchmark from real failures.
Use uv run benchrail help for the command list, or
uv run benchrail help evaluate-change for command-specific options.
Install
Benchrail is in public-alpha shape. The PyPI distribution name is
benchrail-ai; the CLI command and Python import package are benchrail.
From a clone:
git clone https://github.com/JeffBrines/benchrail.git
cd benchrail
uv sync
uv run benchrail help
Once published to PyPI:
uvx --from benchrail-ai benchrail help
Public Alpha Expectations
- APIs and artifact schemas may change before a stable release.
- Benchmark artifacts can contain sensitive prompts, policies, customer inputs, retrieved context, and model outputs; do not commit secrets or unredacted PII.
- LLM judges should not gate releases until they pass human-label calibration.
- Public leaderboard scores are not a substitute for product-specific evals.
Quick Example
Run the checked-in support-agent example:
uv run benchrail run examples/support-agent/benchmark.yaml \
--outputs examples/support-agent/baseline.outputs.yaml \
--system-version baseline \
--out examples/support-agent/baseline.run.yaml
uv run benchrail run examples/support-agent/benchmark.yaml \
--outputs examples/support-agent/candidate.outputs.yaml \
--system-version candidate \
--out examples/support-agent/candidate.run.yaml
uv run benchrail compare \
examples/support-agent/baseline.run.yaml \
examples/support-agent/candidate.run.yaml
Expected result: improved.
Create a benchmark
uv sync
uv run benchrail init "Support Agent" \
--intended-output "Resolve customer issues accurately using company policy." \
--good "Uses the current policy source" \
--bad "Invents refund promises" \
--user "Customers" \
--risk "Wrong billing guidance" \
--calibration-max-age-days 30 \
--calibration-min-labels 4
This creates benchmarks/support-agent/benchmark.yaml, including the benchmark's
release-gate calibration policy. Commands such as inspect, next, and
evaluate-change use that policy by default; CLI policy flags override it for a
single run.
Add a real case:
uv run benchrail add-case benchmarks/support-agent/benchmark.yaml \
--name "Refund outside window" \
--input "Can I get a refund after 60 days?" \
--expected-behavior "Use the refund policy and do not invent exceptions." \
--tag policy \
--source production-failure
This benchmark artifact is the durable optimization target. Scorers, baseline runs, comparisons, and failure-driven evolution build on it.
Derive candidate scorers from the benchmark:
export OPENAI_API_KEY=sk-...
uv run benchrail derive-scorers benchmarks/support-agent/benchmark.yaml \
--provider openai
Generated scorers are saved into the benchmark file as human-reviewable proposals. They are not release gates until they have been reviewed and, for LLM judges, calibrated against human labels.
Create a run from saved system outputs:
# outputs.prompt-v1.yaml
outputs:
- case_id: refund-outside-window
output: "Refunds are only available within 30 days."
Or collect outputs by running your system once per case:
uv run benchrail collect benchmarks/support-agent/benchmark.yaml \
--command "python agent.py --input {input}" \
--out outputs.prompt-v1.yaml
Command placeholders include {case_id}, {input}, {context}, and
{expected_behavior}. Benchrail shell-quotes placeholder values, so do not wrap
placeholders in additional quotes.
Or import outputs from an AI product/integration log:
{"case_id":"refund-outside-window","output":"Refunds are only available within 30 days.","trace_id":"trace-123","provider":"openai","model":"gpt-small","latency_ms":321.5,"total_tokens":162,"cost":0.0042,"feedback":"thumbs_down","outcome":"ticket_reopened"}
uv run benchrail import-outputs benchmarks/support-agent/benchmark.yaml \
app-outputs.jsonl \
--out outputs.prompt-v1.yaml
import-outputs accepts JSONL, CSV, YAML, or JSON. By default it expects
case_id and output fields, validates case IDs against the benchmark, and
writes the same outputs.yaml format used by benchrail run. It also preserves
optional production metadata such as source_id, trace_id, timestamp,
environment, provider, model, latency_ms, input_tokens,
output_tokens, total_tokens, cost, feedback, feedback_score,
outcome, and a scalar metadata mapping. Use --case-id-field and
--output-field when your integration log uses different field names.
Run LLM judge scorers automatically:
export OPENAI_API_KEY=sk-...
uv run benchrail run benchmarks/support-agent/benchmark.yaml \
--outputs outputs.prompt-v1.yaml \
--system-version prompt-v1 \
--judge-provider openai \
--out runs/support-agent/prompt-v1.yaml
You can also provide manual scorer results in the outputs file. Manual results override judge execution for that scorer:
outputs:
- case_id: refund-outside-window
output: "Refunds are only available within 30 days."
scorer_results:
- scorer_id: policy-correctness
status: pass
rationale: "Matches the refund policy."
Unreviewed LLM judges are advisory by default. Their results are stored on run
artifacts, but hard-gate summaries only count LLM judges whose scorer
trust_status has been promoted to trusted after human review/calibration.
Calibrate judge scorers against human gold labels:
# labels.support-agent.yaml
labels:
- case_id: refund-outside-window
scorer_id: policy-correctness
expected_status: pass
notes: "Human reviewer confirmed the answer follows the refund policy."
uv run benchrail calibrate benchmarks/support-agent/benchmark.yaml \
--run runs/support-agent/gold-run.yaml \
--labels labels.support-agent.yaml \
--out calibrations/support-agent/gold-run.yaml \
--dry-run
Remove --dry-run to update each LLM judge scorer's trust_status to
trusted, warning, or blocked based on sample size, TPR, TNR, and Cohen's
kappa thresholds. Calibration reports include case-level failures showing
false positives, false negatives, and missing judge predictions for human review.
Saved reports are durable artifacts under calibrations/<benchmark-id>/.
Inspect calibration history and latest drift:
uv run benchrail calibration-history benchmarks/support-agent/benchmark.yaml
Inspect release-gate readiness with a freshness policy:
uv run benchrail inspect benchmarks/support-agent/benchmark.yaml \
--calibration-max-age-days 30 \
--calibration-min-labels 4
An LLM hard gate is only "ready" when it is trusted, has calibration provenance, has enough human labels, and its calibration is fresh under the policy.
Compare a candidate run against a baseline:
uv run benchrail compare \
runs/support-agent/prompt-v1.yaml \
runs/support-agent/prompt-v2.yaml
Or let Benchrail run the full agent-native change loop:
uv run benchrail evaluate-change benchmarks/support-agent/benchmark.yaml \
--baseline-command "python agent.py --input {input}" \
--candidate-command "python agent.py --input {input}" \
--judge-provider openai \
--artifact-dir evaluations/support-agent/current-change \
--baseline-model gpt-premium \
--candidate-model gpt-small \
--baseline-cost 1.00 \
--candidate-cost 0.25 \
--samples 3
This writes baseline/candidate outputs, run artifacts, and an
evaluation.yaml decision artifact with pass, fail, mixed, or
inconclusive. By default, evaluate-change requires LLM hard gates to be
release-ready under the benchmark's calibration policy. Use
--allow-unready-gates only for exploratory runs. For nondeterministic agents,
--samples repeats the baseline/candidate comparison and returns
inconclusive when sample decisions disagree.
If you provide model and cost metadata, the evaluation artifact also records
cost movement and a recommendation such as safe_to_downgrade,
do_not_downgrade, needs_more_samples, or
quality_improved_but_cost_increased. Costs are user-supplied estimates for now;
use any consistent unit across baseline and candidate.
Evolve the benchmark from a real failure:
# failures/refund-60-days.yaml
input: "Can I get a refund after 60 days?"
bad_output: "Sure, I can refund that."
expected_behavior: "Use the 30-day refund policy and do not invent exceptions."
source: production-failure
tags: [policy, regression]
rationale: "The agent promised a refund outside policy."
name: "Refund outside window"
uv run benchrail --json evolve benchmarks/support-agent/benchmark.yaml \
--from-failure failures/refund-60-days.yaml \
--dry-run
Remove --dry-run to add the case and append an evolution history event.
Import an existing eval definition:
# external-eval.yaml
name: Support Refund Eval
intended_output: Answer refund questions using policy.
provider: json
external_id: support/refund-policy
cases:
- id: refund-60-days
name: Refund after 60 days
input: Can I get a refund after 60 days?
expected_behavior: Say refunds are only available within 30 days.
source: imported
uv run benchrail import-json external-eval.yaml \
--out benchmarks/support-refund/benchmark.yaml
Discover available and planned import/attach routes:
uv run benchrail --json discover "refund policy support"
Attach a run executed outside Benchrail:
uv run benchrail attach-run benchmarks/support-refund/benchmark.yaml \
--provider inspect \
--external-id run-123 \
--system-version prompt-v1 \
--summary external-run-summary.yaml \
--out runs/support-refund/inspect-run-123.yaml
Run the milestone demo
The demo derives a rubric for the same agent task ("monthly revenue forecast") from two different Context Specs — D2C ecommerce vs freight logistics — and asserts the rubrics diverge.
uv sync
export ANTHROPIC_API_KEY=sk-ant-...
uv run python scripts/derive_demo.py --provider anthropic
Or run the same derivation through OpenAI/GPT:
export OPENAI_API_KEY=sk-...
uv run python scripts/derive_demo.py --provider openai
It writes out/rubric.d2c.yaml and out/rubric.logistics.yaml, prints a
divergence report, and exits non-zero if the rubrics don't diverge (which would
mean the derivation isn't domain-sensitive). Offline tests: uv run pytest.
Provider defaults can also be set with environment variables:
export BENCHRAIL_LLM_PROVIDER=openai
export BENCHRAIL_LLM_MODEL=gpt-5.5
uv run python scripts/derive_demo.py
What's wired
| Piece | Module |
|---|---|
| Context Spec schema + loader | src/benchrail/context_spec.py |
| Benchmark schema + loader | src/benchrail/benchmark.py |
| Benchmark CLI | src/benchrail/cli.py |
| Agent inspection + next action | src/benchrail/agent.py |
| Failure-driven evolution | src/benchrail/evolve.py |
| Agent-native evaluation loop | src/benchrail/evaluation.py |
| External benchmark import | src/benchrail/importer.py |
| Source discovery | src/benchrail/discovery.py |
| Scorer derivation | src/benchrail/scorer.py |
| LLM judge execution | src/benchrail/judge.py |
| Judge calibration | src/benchrail/calibration.py |
| Run + comparison artifacts | src/benchrail/run.py |
| Rubric / Criterion schema | src/benchrail/rubric.py |
| Failure-mode-enumeration prompt | src/benchrail/prompt.py |
| Provider-neutral LLM derivation | src/benchrail/derive.py, src/benchrail/llm.py |
| Divergence measurement | src/benchrail/divergence.py |
Inspect runner integration is the next major slice — see below.
Design docs
DESIGN.md— thesis, positioning, build-on decision, trust model.BRANDING.md— Benchrail naming decision and rename migration plan.CUTOVER.md— external repository, package, domain, and compatibility checklist.CHANGELOG.md— release history and public-alpha contents.RELEASE.md— release validation, publishing, and announcement checklist.ROADMAP.md— execution plan from provider-neutral derivation to calibrated runs.docs/agent-harness.md— how build agents should operate Benchrail.docs/derivation-v0.md— the Context Spec schema, the derivation method, and a worked D2C-vs-logistics example showing how the rubrics diverge.
Benchrail is intended to run on top of Inspect AI rather than reimplementing an eval runner.
Security note: benchmark artifacts can contain production inputs, policies, customer data, and model outputs. Treat them as sensitive repo artifacts, review imported eval definitions before running judges over them, and avoid committing PII or secrets.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file benchrail_ai-0.1.0.tar.gz.
File metadata
- Download URL: benchrail_ai-0.1.0.tar.gz
- Upload date:
- Size: 120.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99efc8ddb4821af5039e803f6a7b1d4f00a642e99cd116f41ca1ca9abfa5c0ae
|
|
| MD5 |
099d5c03f2b7c41e3e7e649921792c18
|
|
| BLAKE2b-256 |
7f0f77e3b4ea613a306ebed701a1dd75aefbb4ef45ae79632441ff0bcb88a3db
|
File details
Details for the file benchrail_ai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: benchrail_ai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 60.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d0978981d0208ab2bdc109b91e8319bfd032bafbb0f82dc17a00fb2973b7bc0
|
|
| MD5 |
b149ce5ed17c6fc92c2dae079a26da6f
|
|
| BLAKE2b-256 |
1e16e79bee7650958d881c9fcea2543990de902b0c2efbc018fa05169a3b7b74
|