Skip to main content
UQLM (Uncertainty Quantification for Language Models), from CVS Health, estimates how trustworthy an LLM response is. Its black-box scorers sample several answers to the same prompt and measure how consistent the original response is with them; its white-box scorers read token log probabilities. Every scorer returns a response-level confidence score in [0, 1], so you can flag risky answers without a reference answer or a judge prompt. This guide shows both ways to wire UQLM into Arize AX: Flow 1 scores existing Arize AX traces with UQLM’s noncontradiction scorer and writes the scores back via client.spans.update_evaluations(...); Flow 2 uploads a small dataset, runs an Arize AX experiment with the same scorer wrapped as an experiment evaluator, and surfaces the scores in Datasets+Experiments. Both flows share the same setup. Run the code blocks below in order inside a single Python session — each block builds on imports and variables from earlier ones.

Prerequisites

  • Python 3.13+
  • An ARIZE_SPACE_ID and ARIZE_API_KEY from your Arize AX space settings
  • An OPENAI_API_KEY from OpenAI Platform (used as both the model under trace and the model UQLM samples candidate answers from)
  • About 3.5 GB of free disk space: the installed packages take about 1.6 GB with CPU-only PyTorch, and the first run downloads the microsoft/deberta-large-mnli natural language inference (NLI) model from Hugging Face, which the noncontradiction scorer runs locally

Launch Arize AX

If you don’t already have an Arize AX account, sign up at arize.com and grab your ARIZE_SPACE_ID and ARIZE_API_KEY from Settings → Space Settings.

Install

Configure credentials

Define evaluators

The shared setup: a UQLM BlackBoxUQ scorer, the canonical 2-row hallucination dataset that both flows score, and an Arize SDK client. BlackBoxUQ with the noncontradiction scorer works in two steps. First, generate_candidate_responses(...) asks the LLM the same question several times at temperature=1.0. Then score(...) uses a local NLI model to check whether the response you are grading contradicts each sampled answer. A response that agrees with the samples scores close to 1.0; one that contradicts them scores close to 0.0. UQLM drives the LLM through LangChain, so pass it a ChatOpenAI model. gpt-4.1-mini is used because it accepts the sampling temperature UQLM sets. The two flows run in different asyncio event loops, and an async HTTP connection can’t outlive the loop that opened it. sampler_llm() therefore gives each ChatOpenAI its own httpx.AsyncClient (by default LangChain shares one cached client), and Flow 2 swaps in a fresh sampler before it runs.

Flow 1 — Evaluate existing traces

Source the spans

Instrument OpenAI with OpenInference, make two calls (each forced to echo a known answer so the trace contains predictable text), then pull the resulting spans back from Arize AX.

Run the evaluators

Flow 1 runs outside any asyncio loop, so wrap the async generate_candidate_responses(...) call in asyncio.run(...). score(...) is synchronous and grades every span in one call. The raw noncontradiction value is continuous and moves slightly between runs because the sampled answers change. The doc applies a 0.5 threshold and normalizes to 1.0 / 0.0 so the printed score column stays stable. To keep the raw confidence instead, assign float(confidence) directly.

Log evaluations to Arize AX

update_evaluations(...) requires a context.span_id column (which export_to_df already provides) plus the reserved eval.<name>.{score,label,explanation} columns.

Expected output

Verify in Arize AX

Open the project named uqlm-tracing-example-<timestamp> (the value printed above) in your Arize AX space. Each ChatCompletion span now carries a uqlm_confidence annotation column showing the normalized 0/1 score, the confident / uncertain label, and the raw noncontradiction value in the explanation.

Flow 2 — Run an experiment

Create a dataset

Define the task

The task function receives the dataset row and returns whatever the experiment should grade. A real workflow would call an LLM here; this passthrough keeps the example deterministic.

Wrap the evaluators

Experiment evaluators run inside an asyncio loop, so use async def and await generate_candidate_responses(...) directly — asyncio.run(...) fails with asyncio.run() cannot be called from a running event loop. Return an EvaluationResult with score, label, and explanation populated: leaving any of those reserved fields as None triggers unsupported cast from null to <type>: reserved column cannot be coerced to canonical type at upload time.

Run the experiment

Expected output

Verify in Arize AX

Open the Datasets + Experiments tab in Arize AX. The dataset uqlm-experiment-example-ds-<timestamp> and the experiment uqlm-experiment-example-<timestamp> (names printed above) appear with one run per dataset row, each carrying the uqlm_confidence score, label, and explanation columns.

Troubleshooting

  • Because arize>=8.0.0 depends on numpy>=2.0.0 ... requirements are unsatisfiable, or pyarrow requires NumPy 2.0 or newer. You are on Python 3.12 or earlier, where uqlm pins numpy<2. Use Python 3.13+, where uqlm allows numpy>=2.
  • The first run pauses for several minutes before printing anything. BlackBoxUQ downloads the microsoft/deberta-large-mnli NLI model (about 1.6 GB) the first time it loads. Later runs read it from the Hugging Face cache (~/.cache/huggingface). To use a smaller NLI model, pass nli_model_name= to BlackBoxUQ.
  • asyncio.run() cannot be called from a running event loop. Your Flow 2 evaluator calls asyncio.run(...). Experiment evaluators already run inside asyncio; await generate_candidate_responses(...) instead.
  • RuntimeError: Event loop is closed followed by evaluator failed for example id .... The Flow 2 evaluator reused an HTTP client whose connections were opened inside Flow 1’s asyncio.run(...) loop, which has since closed. Build each flow’s ChatOpenAI with its own http_async_client=httpx.AsyncClient() and assign a fresh one (uq.llm = sampler_llm()) before running the experiment, as shown above.
  • Nothing prints after All evaluators completed, and the script exits cleanly. Two evaluators sampled at once. UQLM redirects stdout while it generates, and overlapping redirects from concurrent evaluators can leave stdout pointing at an in-memory buffer, so every later print is lost. Wrap generate_candidate_responses(...) in async with sampling_lock: as shown in Flow 2.
  • llm must be provided to generate responses. BlackBoxUQ was created without llm=. Pass a LangChain chat model such as ChatOpenAI(...).
  • column "eval.<name>.label": unsupported cast from null to string: reserved column cannot be coerced to canonical type. Your evaluator returned a bare number or string instead of a fully-populated EvaluationResult(score=..., label=..., explanation=...). Arize AX rejects null values in reserved eval columns — populate all three fields.
  • Spans never appear after 60s. Span flush + ingest typically takes 5–15s. If the loop times out, check that ARIZE_SPACE_ID + ARIZE_API_KEY are right and that you’re connecting to the correct region’s OTLP endpoint (otlp.arize.com for US, otlp.eu.arize.com for EU).
  • Using other UQLM scorers. BlackBoxUQ also supports semantic_negentropy, entailment, exact_match, cosine_sim, and bert_score; WhiteBoxUQ scores token log probabilities (for example min_probability) without sampling. Each scorer’s values land in result.data["<scorer>"]. See the UQLM documentation.
  • Experiment re-runs collide. Both names embed TIMESTAMP = int(time.time()) so a single re-run produces unique names. If you re-execute the same combined.py quickly, regenerate TIMESTAMP first or call arize.experiments.delete(...) / arize.datasets.delete(...) on the prior run’s names.

Resources

UQLM Documentation

UQLM on GitHub

Logging evaluations to Arize AX

Ragas evaluators in Arize AX