Skip to main content

Google Colab

colab.research.google.com
When a new model tops MMLU, GPQA, or the latest leaderboard, it’s tempting to assume it’s the right choice for your application. It usually isn’t — at least not for the reason you think. Public benchmarks measure generic capabilities on generic data with a generic metric: broad academic knowledge, scored as multiple-choice accuracy, averaged over thousands of questions that look nothing like your production traffic. Your task is narrow and specific. You aren’t asking the model trivia — you’re asking it to pull a handful of exact fields out of a customer email, every time, in a schema your downstream code can parse. A model that wins MMLU by two points can lose your task by twenty, because:
  • The data is different — your inputs are your customers’ emails, not exam questions.
  • The metric is different — you care whether the due_date field is exactly right, not whether the prose sounds smart.
  • The failure modes are different — a confident, plausible-sounding wrong answer is worse for you than an “I don’t know,” the opposite of what a knowledge benchmark rewards.
The only benchmark that predicts how a model performs on your task is a benchmark built from your task — and Phoenix experiments are exactly that harness. This cookbook builds one for an email text-extraction service and uses it to compare two models fairly. This cookbook shows examples of:
  • Building a small domain dataset of emails + correct extractions — your benchmark, not a public one
  • Defining an extraction task with a fixed schema and prompt, parameterized only by model
  • Defining two evaluators — string similarity and field-level accuracy — and seeing how they can rank the models differently
  • Running the same harness across gpt-5.4-mini and the flagship gpt-5.5 and comparing them fairly

What makes a benchmark trustworthy

Before trusting any number — public or your own — ask whether the benchmark behind it has these four properties:
  1. Representative data. The examples are drawn from your real inputs, covering the cases you actually see (including the messy and ambiguous ones), not a clean toy sample.
  2. A metric that measures what you care about. The score moves when the output gets better for your purpose and stays flat when it doesn’t. The wrong metric can rank a worse model first — we’ll show exactly this below.
  3. Enough examples to be stable. Two examples can’t distinguish two models; the score has to be more signal than noise.
  4. Reproducible and fair. Every model is judged on the same dataset, with the same metric, under the same prompt — so a difference in the score reflects a difference in the model, not the setup.
A Phoenix experiment is this harness: a fixed dataset, a task you vary, and one or more evaluators. Hold the dataset and evaluators constant, swap only the model, and the comparison is fair by construction.

Notebook Walkthrough

We will go through key code snippets on this page. To follow the full tutorial, check out the full notebook. After configuring tracing with phoenix.otel.register(...) and instrumenting OpenAI, we build a small hand-labeled dataset, define a model-parameterized extraction task, score it two ways, and run the same harness across two models.

Set up tracing

Register a tracer provider so every extraction call shows up as a span in Phoenix — the experiment results link back to the exact calls that produced them. auto_instrument=True activates the installed OpenInference instrumentors (here, OpenAI), so there’s no need to call OpenAIInstrumentor().instrument(...) yourself. Use AsyncClient because the experiment task makes network-bound LLM calls.

Build a domain dataset

This is the part public benchmarks can’t do for you. Hand-label a handful of emails the way your service actually sees them — a meeting request, an invoice, a support escalation — each paired with the exact structured output you want back. In production you’d build this from real traffic (export traces from Phoenix, sample, and label); here we inline a small set so the example is self-contained. The mix of free-text fields (summary) and categorical fields (category, due_date) is deliberate: it’s what lets two reasonable metrics disagree later.
This is a demonstration harness. Eight examples is enough to show the workflow, not to draw stable conclusions (recall property 3 above). A production harness needs a larger, representative sample drawn from your real traffic before you’d trust the ranking.

Define the extraction task

The task is what we hold almost constant: the same schema, the same prompt, the same parsing — only the model changes. Using structured outputs forces every model to return the exact same shape, so the comparison is about extraction quality rather than formatting luck.
make_task(model) returns a task function bound to one model. The experiment calls it once per dataset example; the input it receives is that example’s input dict ({"email": ...}).

Choose metrics that measure what you care about

This is where benchmarks quietly lie. Score the same outputs two ways:
  • jaro_winkler — string similarity on the free-text summary field. Cheap and forgiving — the kind of “looks about right” metric people reach for first — and the right tool for a summary, which can be correct while worded differently.
  • field_accuracy — the fraction of the operational fields (sender, category, action_required, due_date) that match exactly (case-insensitive). This is what downstream code depends on: a due_date that’s “close” is still a wrong date. summary is deliberately left out — exact-matching a summary would punish good extractions for harmless rewording.
The two metrics measure genuinely different things, so they can rank the models differently: a model might write better summaries (higher jaro_winkler) while miscategorizing more emails (lower field_accuracy), or vice versa. When they disagree, the metric that should decide is the one tied to your downstream needs — here, field_accuracy.
The two metrics can rank the same outputs in opposite order. Made deterministic — two candidate extractions for one invoice email: one with the right operational fields but a reworded summary, one that looks almost identical but has the due_date off by a day:

Run the same harness across models

Same dataset, same evaluators, same prompt — change only the model argument. That’s what makes this a fair comparison instead of an anecdote.

Compare fairly

Phoenix prints a per-experiment summary and lets you compare both runs example-by-example in the UI. Rolling the scores up per metric makes the disagreement explicit:
With only eight examples the two models may or may not separate cleanly on a given run — that’s exactly why you look rather than assume. The lesson holds regardless: if the two metrics rank the models differently, the public-leaderboard instinct (“just take the higher-scoring model”) could have led you to the wrong choice — which model is “better” depends on the metric, and the metric that should win is the one that reflects your downstream needs (here, field_accuracy). If they agree, you now have evidence grounded in your data and your metric. Either way, you trust the result because you built the harness.

Reading the results in Phoenix

Open the dataset’s Experiments tab to see the runs side by side. Each experiment is a row; each evaluator becomes its own score column (so field_accuracy sits next to jaro_winkler), alongside operational columns — average latency, cost, and error rate — that matter for a real model choice but never show up on a public leaderboard.
Comparing the gpt-5.5 and gpt-5.4-mini experiments in Phoenix — field_accuracy, jaro_winkler, latency, cost, and error rate side by side.
The two metrics can rank the models differently — a model that writes better summaries (higher jaro_winkler) might still miscategorize more emails (lower field_accuracy), or vice versa. When they disagree, sort by the metric tied to your downstream needs (field_accuracy) rather than the forgiving one, and click any row to drop into the example-level view: the input email, the model’s extraction, and each evaluator’s score for that single example. That’s where you see why one model wins — a due_date the model dropped, a sender it over-captured — instead of trusting an aggregate.
The example-level compare view in Phoenix: the input email, the reference extraction, and the model's output with per-example field_accuracy and jaro_winkler scores.

Where to go next

The harness is reusable — everything below holds the dataset and evaluators constant and changes one thing at a time, so each comparison stays fair:
  • Iterate the prompt. Vary PROMPT instead of model to find the wording that extracts most reliably.
  • Add models and providers. Drop another model name — or another provider’s client — into make_task and rerun. The leaderboard ranking rarely survives contact with your task.
  • Grow the dataset. Move from eight inline examples to a representative sample exported from your real traffic, so the scores become stable enough to trust (property 3).
  • Make it a standing benchmark. Rerun the same harness on every model upgrade or prompt change to catch regressions before they reach production.

Takeaway

A public benchmark tells you how a model does on someone else’s task, with someone else’s metric. That number rarely transfers to production. The fix isn’t a better leaderboard — it’s a small, trustworthy harness of your own:
  • Representative data — a dataset built from your real inputs.
  • A metric that measures what you care about — and the discipline to notice when a convenient metric (string similarity) disagrees with the real one (field accuracy).
  • Enough examples to make the score stable.
  • A fair, repeatable setup — same dataset, same evaluators, same prompt; vary only the model. (Run-to-run sampling adds a little noise, but with the dataset, prompt, and evaluators held fixed, a difference in score still traces to the model — that’s the point, not bit-for-bit reproducibility.)
A Phoenix experiment gives you all four. Once you have it, comparing models — or prompts, or providers — is just swapping one argument and reading a number you can actually defend.