[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_IDandARIZE_API_KEYfrom your Arize AX space settings - An
OPENAI_API_KEYfrom 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-mnlinatural language inference (NLI) model from Hugging Face, which thenoncontradictionscorer runs locally
Launch Arize AX
If you don’t already have an Arize AX account, sign up at arize.com and grab yourARIZE_SPACE_ID and ARIZE_API_KEY from Settings → Space Settings.
Install
Configure credentials
Define evaluators
The shared setup: a UQLMBlackBoxUQ 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 anyasyncio 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 nameduqlm-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 anasyncio 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 datasetuqlm-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, orpyarrow requires NumPy 2.0 or newer. You are on Python 3.12 or earlier, whereuqlmpinsnumpy<2. Use Python 3.13+, whereuqlmallowsnumpy>=2.- The first run pauses for several minutes before printing anything.
BlackBoxUQdownloads themicrosoft/deberta-large-mnliNLI 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, passnli_model_name=toBlackBoxUQ. asyncio.run() cannot be called from a running event loop. Your Flow 2 evaluator callsasyncio.run(...). Experiment evaluators already run insideasyncio;awaitgenerate_candidate_responses(...)instead.RuntimeError: Event loop is closedfollowed byevaluator failed for example id .... The Flow 2 evaluator reused an HTTP client whose connections were opened inside Flow 1’sasyncio.run(...)loop, which has since closed. Build each flow’sChatOpenAIwith its ownhttp_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 redirectsstdoutwhile it generates, and overlapping redirects from concurrent evaluators can leavestdoutpointing at an in-memory buffer, so every laterprintis lost. Wrapgenerate_candidate_responses(...)inasync with sampling_lock:as shown in Flow 2. llm must be provided to generate responses.BlackBoxUQwas created withoutllm=. Pass a LangChain chat model such asChatOpenAI(...).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-populatedEvaluationResult(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_KEYare right and that you’re connecting to the correct region’s OTLP endpoint (otlp.arize.comfor US,otlp.eu.arize.comfor EU). - Using other UQLM scorers.
BlackBoxUQalso supportssemantic_negentropy,entailment,exact_match,cosine_sim, andbert_score;WhiteBoxUQscores token log probabilities (for examplemin_probability) without sampling. Each scorer’s values land inresult.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 samecombined.pyquickly, regenerateTIMESTAMPfirst or callarize.experiments.delete(...)/arize.datasets.delete(...)on the prior run’s names.