Skip to main content
When running LLM applications in production, you quickly accumulate thousands or millions of traces. A high-signal trace is one that points to a new way in which your system fails. The discipline of production observability comes down to finding those traces efficiently and actually reading them. This guide covers:
  • What separates signal from noise in production traces
  • The review practice: a daily smoke check and a weekly error-analysis session
  • Filtering traces by errors, latency outliers, evaluation scores, and session behavior from the CLI and programmatically
  • Turning what you find into a failure taxonomy that drives your next evals

Follow Along with Sample Traces

You can run every query in this guide against any Phoenix project that has traces in it. To follow along with sample traces that cover every technique (hard failures, latency outliers, evaluation scores, and multi-turn sessions), load the prepared sample traces:
  1. Install and launch the Phoenix server.
  2. Install the Phoenix CLI and point px at your server and project.
  3. Download the sample traces in the form of two JSONL files: the spans and their evaluations. This is the same shape Phoenix uses when you export your own spans, so nested fields (like span events) and timestamps arrive already typed.
  4. Load the sample traces into Phoenix. This logs the spans and their evaluation labels into the high-signal-demo project so they appear to have happened in the last 12 hours.
    1. Save this file as data-importer.py:
    1. Run it:
  5. Confirm it landed. Open the Phoenix UI at http://localhost:6006, select the high-signal-demo project and select “all spans” to see ~100 traces including several with ERROR status, a few slow outliers, and session groupings.

Finding the signal despite the noise

The most common mistake teams make is treating observability as a dashboard problem when it’s really an analysis problem. Generic aggregate metrics like an overall “helpfulness” score or a global pass rate create a false sense of measurement and control. It doesn’t matter which way the number moves if nobody can say what it means for users. Evals and observablity expert Hamel Husain calls error analysis “the most important activity in evals”, and observes that successful teams spend the majority of their development effort looking at data rather than building automated checks. So before you start building a metrics dashboard, you need to find out which traces matter. While sorts, scores, and queries help you understand traces and filters find them, they never replace reading them. High-signal traces come from five sources:
  1. Hard failures. Exceptions, tool-call errors, timeouts, malformed outputs. It is the cheapest signal to query and the shallowest. An LLM application can be deeply broken without ever throwing an exception.
  2. Soft failures. Traces where evaluation scores flag a quality problem: a hallucinated answer, an irrelevant retrieval, a failed task.
  3. Outliers. Traces at the extremes of any metric: latency, token count, tool-call count, response length. Both extremes are interesting: the slow tail reveals retry loops and runaway agents; the suspiciously fast tail reveals silent failures like empty retrievals.
  4. Human signals. Negative feedback, support escalations, sessions where the user rephrases the same question three times. A user retry is an eval score the user computed for you.
  5. Novelty. Inputs unlike anything you’ve categorized before. New usage patterns are where your next failure mode is hiding.
Prioritize the most frequent failures. Not every failure is mission-critical. Review time and evals aren’t free. The goal is a ranked list of real, frequent problems, not exhaustive coverage.

The Discipline of timely Trace Reviews

Production trace review is all about discipline. If you can handle a daily checkin and a weekly standup, you can handle the regular reviews traces require. This process consists of a daily smoke check and a weekly error analysis session.

Daily smoke check (minutes)

Glance at the metrics dashboard in your Phoenix server. Select your “high-signal-demo” project from the dropdown at the top of the page and look for anything alarming. Pay attention to spikes and climbing numbers in the following tiles:
  • Trace Latency
  • Cost (not included in the sample traces)
  • LLM spans with errors
  • Tool spans with errors
The dashboard is a smoke detector, not a review process. It tells you that something changed but never why it changed. That requires analyzing individual traces, as shown in the next section.

Weekly error-analysis session (an hour or two)

Once a week, pull a deliberately mixed sample of traces and read them with open-ended notes. This is where improvement is made real. You’ll learn how in the last section of this guide.
Teams that only do the daily check end up dashboard-watching. Teams that only do deep dives get surprised by fires. You need both, as they are complementary. Smoke-check anomalies tell you where to oversample in the weekly review.

How to perform the Daily Check: Finding Hard Failures

Hard failures are queryable with zero setup. Phoenix records span status, exception events, and error messages on every trace. Triage them visually in the Phoenix UI or straight from the terminal with the px CLI. (Prefer code, e.g. for monitoring scripts? The same queries run through the Python and TypeScript clients.)
  1. Open the Phoenix UI at http://localhost:6006 and select your project (high-signal-demo).
  2. You will see a spans table. Click the “Traces” tab at the top of the table, next to “Spans”.
  3. Type a condition into the filter bar: status_code == 'ERROR'. The table narrows to errored spans only.
  4. Add a clause to focus on a span kind: status_code == 'ERROR' and span_kind == 'LLM' for model-call failures, or 'TOOL' for tool errors.
  5. Click any row to open the trace, then select the failing span to read its exception (exception.message / exception.stacktrace) under the “Events” tab.

How to read error traces

Find the first failure, not the loudest one. In multi-step agent traces, upstream errors cascade. A malformed tool output at step 2 produces a confused plan at step 4 and a wrong answer at step 9. Note the first point where the trace went wrong; fixing it usually dissolves the downstream symptoms. Collect and group before you fix. A hundred error traces might be three actual problems. Resist debugging trace-by-trace; collect, categorize, count in the weekly session.

Finding Performance Outliers

Latency and cost problems rarely announce themselves in averages. A healthy p50 can hide a p99 where agents loop, contexts balloon, and users give up. Sort by extremes and read both tails:
  • Slow tail: retry storms, agents stuck in tool-call loops, oversized retrieved contexts, sequential calls that should be parallel.
  • Fast tail: short-circuits and silent failures, empty retrieval results, guardrails firing when they shouldn’t, models returning refusals or stubs.
From the terminal, or programmatically for latency-tail analysis:
Go to your project’s dashboard. The “Trace Latency” panel plots the percentile lines (P50 through P99) over time, so you can read both tails at a glance: watch the P99 line for the slow tail, and the P50 dipping toward zero for the suspiciously fast tail. Toggle individual percentiles in the legend to isolate the tail you’re reading. The dashboard tells you when a tail moved; use the CLI or Python tabs to pull the actual outlier traces to read.
For regressions specifically, the unit of comparison is the change: compare the latency or token distribution before and after a deploy, prompt revision, or model swap, not against an absolute threshold. And whatever you use to determine “outliers” (p95, one standard deviation from the median), treat it as a starting point, not a finding.

Finding Quality Issues with Evaluations

Evaluation results attached to traces let you query for soft failures the way you query for exceptions. Treat eval results as exploration clues, not verdicts:
  • Review failures as well as passes. Failing labels show you problems; passing labels show you whether your evaluator is calibrated. If you only ever read failures, you’ll never notice your judge passing garbage.
  • Prefer binary and categorical judgments to numeric scales. “Did the assistant hand off to a human when it should have: yes/no” is actionable. “Helpfulness: 4.2” is not. Arize research testing LLM judges on continuous ranges found numeric score evals unreliable, with small prompt changes producing wildly different results, and a 2025 re-test on newer models confirmed that binary and categorical judgments remain the most stable (original study, follow-up). Phoenix’s built-in evaluators emit categorical labels for exactly this reason.
  • A 100% pass rate is a warning sign. It usually means your evals aren’t stressing the system. A 70% pass rate often indicates an eval that’s actually testing something.
A practical consequence of label-based evals: filter on labels, not score thresholds. Score direction varies by evaluator (for Phoenix’s hallucination evaluator, a higher score means factual), so label filters are both more readable and harder to get backwards. From the terminal, or programmatically for filtering by evaluation results:
Each recurring quality issue you confirm by reading the flagged traces is a candidate for a new, specific evaluator. This is how your eval suite grows out of real world failures instead of generic metrics and arbitrary benchmarks.

Session Signals: Multi-Turn Failures

Single-trace filters miss a whole class of problems that only exist across turns: the user who rephrases three times, the conversation that derails halfway, the agent that loses context it had two turns ago. Phoenix sessions group traces into conversations so you can read the interaction the way the user experienced it. Session-level signals worth querying for:
  • Sessions containing any error trace
  • Unusually long sessions (turn count as an outlier metric often portrays a user fighting the system)
  • Sessions with negative feedback on any turn
When reading a session, the first-failure principle matters even more: find the earliest turn where things went wrong, because every turn after it is contaminated context. From the terminal, or programmatically for session analysis:

The Weekly Session: Finding Failure Clusters

Individual bad traces can be anecdotal, edge cases. Clusters help you see what to prioritize. This clustering method comes from qualitative research:
  1. Open coding. In qualitative research, “coding” means annotating data with labels, not programing. Pull a sample of traces and write free-form notes on anything wrong with each one. Don’t pre-define categories before looking at the traces, or you’ll miss the failure modes unique to your application.
  2. Axial coding. Group the notes around shared themes (the “axes” the method is named for) producing a failure taxonomy: distinct, named categories. You can do this yourself like grouping socks after doing the laundry, but an LLM is often faster and just as good.
  3. Count. Tally failures per category. This step creates a prioritized roadmap. In one of Hamel Husain’s client engagements, three categories accounted for over 60% of all problems.
  4. Iterate to saturation. Keep sampling until new traces stop revealing new categories. Rule of thumb: review at least 100 traces to start; once ~20 consecutive traces turn up nothing new, you’re saturated.
Random sampling gets inefficient once the obvious failures are fixed. Most random traces are fine. Bias your sample toward signal: Stratified sampling. Group traces by the dimensions you care about, for example: user segment, model, feature, query category, which tools were called, session turn count. Sample from each group, so a high-volume happy path doesn’t drown out a broken niche. For agent applications, “tools-called” is often the most failure-correlated dimension you have, and it’s already an attribute on your spans. In Phoenix, the attributes you attach at instrumentation time (user IDs, session IDs, custom metadata) are exactly what makes this possible: stratification is an instrumentation decision before it’s a query. For a quick assessment, pull a fixed number of spans per group straight from the CLI:
To build a reusable review set that you can annotate in the UI, hand to a teammate, or run experiments against, proportionally sample in Python and load it into a Phoenix dataset rather than a local file. Save the following script as sample.py and run it from the terminal with python sample.py.
df is a spans dataframe pulled with SpanQuery, and df["model"] is the column you stratify on Embedding clustering (advanced, optional). Embed the message content (usually the user inputs) and cluster it to reveal the natural groupings in your traffic, then sample from every group. Note that this is a different kind of clustering from the failure taxonomy above:
  • Axial coding groups failures, and rarely needs a formal algorithm.
  • Embedding clustering groups inputs, so you can sample across the real shape of your traffic and cross-reference input clusters against error rates and eval labels.
How you weight the samples depends on your goal. Weight proportionally to cluster size when you want representative failure-rate estimates; oversample the small clusters when you’re hunting for failure modes you haven’t seen yet. The first measures, the second discovers. The pipeline below has four stages, each doing one job:
  1. Embed the user inputs. An embedding model converts each input into a long vector of numbers, positioned so that semantically similar inputs land near each other. This turns “which inputs resemble each other?” into a geometry question.
  2. Reduce with UMAP. Raw embeddings have over a thousand dimensions, and at that scale distances stop being informative. Everything is roughly equally far from everything else. UMAP compresses the vectors to 5–25 dimensions while preserving which points are neighbors, which is exactly what the next step needs.
  3. Cluster with HDBSCAN, which finds the dense regions of that space and calls each one a cluster. Unlike k-means, it doesn’t make you guess the number of clusters before you’ve seen the data, and it doesn’t force every point into a group: inputs that fit nowhere get labeled noise (-1). Don’t discard them. An input that resembles nothing else in your traffic is your novelty signal, found for free.
  4. Label each cluster with an LLM using representative examples, because “cluster 3” tells you nothing until you know it means “billing-dispute questions.”
The code below adds two more steps that put the clusters to work: cross-referencing them against failure signals, then building the review sample.
BERTopic packages this exact pipeline if you’d rather not assemble it yourself.
This optional pipeline isn’t covered by the base install.
  1. First add the extra libraries (HDBSCAN ships in scikit-learn ≥ 1.3):
  1. Then set an embedding provider key:
  1. Save the following as pipeline.py:
  1. Run the script with
Phoenix doesn’t include a built-in embedding projector. The workflow above shows the full DIY approach: embed, reduce, cluster, cross-reference against error rates. This portability is a feature. You control the embedding model and clustering algorithm.

Putting It Together: The Triage Loop

The full weekly loop, end to end:
  1. Pull a mixed sample. All hard failures since last review, both latency tails, the lowest (and a few highest) eval scores, sessions with negative feedback or high turn counts, plus a stratified random slice so you don’t develop tunnel vision.
  2. Annotate, open-coding style. Free-form notes, first failure first. Phoenix annotations keep notes attached to the trace where the next reviewer can find them.
  3. Update the taxonomy and re-count. New failure modes get new categories; recurring ones get their tallies bumped.
  4. Prioritize by frequency × impact and fix from the top.
  5. Convert each confirmed failure mode into an evaluator and where possible, a code assertion, an LLM-as-judge with validated binary outputs where not.
Every review cycle makes the next cycle’s automated filtering smarter. Where agents fit. The filtering and sampling steps above are mechanical enough to delegate to a coding agent once your sampling logic is settled. More interestingly, agent-as-judge review over full trajectories adds a recall layer that scalar filters miss: an agent can assess whether a complex tool call or generated shell command was actually correct, something no human reviewer can verify at scale. It can contextualize the smoke signals your filters raise. Agents triage and judge what humans can’t check at volume; humans keep what agents can’t be trusted with, validating the judge’s calibration, naming new failure categories, and deciding what matters. Agent review is one more filter whose escalations a human reads. The scripts below are starting-point templates. Adapt the sampling logic, thresholds, and time windows to match your application’s failure modes and review capacity.
Re-run the loop whenever you ship a significant change. Thirty minutes reading 20–50 traces after a prompt revision catches regressions that no pre-existing eval was written to see.

Next Steps

  • Convert your top failure categories into automated evaluators and run them as online evals
  • Set up annotation queues so domain experts as well as engineers can review edge cases
  • Track failure-category counts over time: a category trending up is a regression even if no individual eval fires
  • Revisit the full loop after every significant prompt, model, or retrieval change

Further Reading

The methodology in this guide draws on practitioner research in error analysis and evaluation: