Uncertainty

mixle has several uncertainty surfaces, all with the same design bias: uncertainty should change behavior. It should decide whether to answer, escalate, collect more data, or report which evidence source mattered.

This page covers:

  • uncertainty over LLM answers;

  • claim-level reliability for generated text;

  • epistemic versus aleatoric decomposition;

  • the uq dispatcher for fitted models, point predictors, ensembles, and LLM-like callables;

  • conformal answer-or-abstain behavior;

  • cross-modal latent evidence fusion;

  • calibrated task cascades.

Unified uq Dispatcher

mixle.inference.uq provides one front door when the caller owns a heterogeneous predictor and wants Mixle to choose an uncertainty route from the object’s capabilities.

from mixle.inference import uq

fitted_uq = uq(model, training_rows)
interval_uq = uq(point_predictor, (x_cal, y_cal), alpha=0.1)
llm_uq = uq(generate, example_prompts, alpha=0.1)

UQResult exposes method-specific accessors:

  • sample_models and credible_interval for fitted Mixle models through a Laplace parameter posterior;

  • interval and epistemic_std for point predictors or ensembles through split conformal calibration;

  • semantic_entropy and confident for LLM-style generation callables.

Use the specialized APIs below when you already know the exact uncertainty method. Use uq when the application boundary should accept several kinds of predictor behind one call.

LLMUncertainty

LLMUncertainty wraps any stochastic callable:

generate(prompt) -> answer

It samples multiple answers, clusters them by meaning, and returns the majority meaning plus a confidence and semantic entropy.

from mixle.reason import LLMUncertainty

def equivalent(a, b):
    return str(a).strip().lower() == str(b).strip().lower()

uq = LLMUncertainty(generate, equivalent=equivalent, n=20)
assessment = uq.assess("Which city is the Eiffel Tower in?")

print(assessment.answer)
print(assessment.confidence)
print(assessment.semantic_entropy)
print(assessment.clusters)

High confidence means most samples fell into the same meaning cluster. High semantic entropy means the model is disagreeing with itself about the answer, not just rephrasing it.

Equivalence Matters

The default equivalence relation is exact equality. That is fine for labels or normalized short answers. For prose, pass a domain-specific relation:

import re

def normalize_city(text):
    return re.sub(r"[^a-z]", "", text.lower())

uq = LLMUncertainty(
    generate,
    equivalent=lambda a, b: normalize_city(a) == normalize_city(b),
    n=20,
)

In production this relation might use canonicalization, embeddings, entailment, or a task-specific parser.

Epistemic and Aleatoric Split

Use several prompts as ensemble members, for example paraphrases of the same question. decompose separates disagreement across members from spread within each member.

dec = uq.decompose(
    [
        "Who discovered penicillin?",
        "Name the scientist credited with discovering penicillin.",
        "Penicillin was discovered by whom?",
    ],
    n=10,
)

print(dec.epistemic)
print(dec.aleatoric)
print(dec.total)

Epistemic uncertainty is reducible uncertainty: model or prompt sensitivity. Aleatoric uncertainty is within-member ambiguity: the question or output space itself remains variable.

Calibrated Answer-or-Abstain

Sampling confidence is still only a signal until calibrated. calibrate uses labeled examples to choose the lowest confidence threshold whose answered set has empirical error at most alpha.

examples = [
    ("Capital of France?", "Paris"),
    ("2 + 2?", "4"),
]

uq.calibrate(examples, alpha=0.1)

answer = uq.answer("Capital of Japan?")
if answer is None:
    escalate_to_human_or_frontier_model()
else:
    print(answer.answer)

After calibration, answer returns None below the threshold. This is the important behavioral change: the LLM can abstain instead of hallucinating.

Claim-Level Reliability

A response can have a stable headline answer and still contain one fabricated detail. assess_claims takes one sampled response, extracts claims, and checks whether independent samples corroborate each claim.

info = uq.assess_claims(
    "Summarize the contract renewal and include dates and parties.",
    threshold=0.6,
)

print(info.reliability)
for claim in info.fabricated:
    print(claim.claim, claim.support)

Defaults:

  • claim extraction is sentence-like splitting;

  • corroboration uses information-weighted content overlap across samples.

For serious text, pass your own extractor or entailment-based corroborator:

info = uq.assess_claims(
    prompt,
    extract=my_claim_extractor,
    corroborates=my_entailment_check,
)

Uncertainty Helpers

The underlying decomposition functions are available from mixle.inference.uncertainty:

from mixle.inference.uncertainty import (
    Clustering,
    UncertaintyDecomposition,
    cluster_samples,
    decompose_entropy,
    decompose_uncertainty,
    decompose_variance,
    marginalize_meaning,
    posterior_ensemble,
    predictive_distribution,
    semantic_entropy,
)

Use them when you already have samples, probability vectors, or prediction ensembles and do not need the LLM wrapper.

UncertaintyDecomposition is the shared result object for epistemic, aleatoric, and total uncertainty summaries. predictive_distribution and posterior_ensemble build ensemble-style predictive objects from fitted or posterior models, decompose_variance performs the variance analogue of the entropy split, and marginalize_meaning aggregates probabilities over semantic clusters represented by Clustering.

Cross-Modal Reasoning

mixle.reason.reason fuses evidence into a shared latent belief. Each evidence source is a linear-Gaussian observation:

y = H z + noise,  noise ~ N(0, R)

Example:

import numpy as np
from mixle.reason import Evidence, Latent, reason

prior = Latent.vector(2, mean=0.0, var=10.0)
evidence = [
    Evidence(np.array([[1.0, 0.0]]), np.array([2.0]), 0.2, name="sensor-a"),
    Evidence(np.array([[0.0, 1.0]]), np.array([-1.0]), 0.5, name="sensor-b"),
]

ans = reason(prior, evidence)

print(ans.mean)
print(ans.interval(level=0.9))
print(ans.information_gain())
print(ans.attribution(normalize=True))

ReasonedAnswer exposes:

  • posterior mean and covariance;

  • credible intervals;

  • total information gain;

  • per-modality attribution;

  • prediction-level epistemic/aleatoric variance split.

Mechanistic Latents

Latent.mechanistic builds a Gaussian prior over a trajectory constrained by a linear dynamical law. Evidence at one time step updates all time steps through the dynamics.

A = np.array([[1.0, 0.1], [0.0, 1.0]])
prior = Latent.mechanistic(A, steps=20, process_cov=0.01 * np.eye(2))

Use block_selector from mixle.reason.core to observe a specific time block of the stacked trajectory.

Task Calibration

For local task models, uncertainty becomes an answer/escalate decision through CalibratedTaskModel and Cascade:

from mixle.task import CalibratedTaskModel, Cascade

model = CalibratedTaskModel(student, alpha=0.1).calibrate(cal_x, cal_y)
cascade = Cascade(model, teacher)
y = cascade("new request")

See Task Distillation for the full serving workflow.

Choosing the Right Tool

Need

Use

Does the LLM know the answer?

LLMUncertainty.assess and semantic entropy

I have a fitted model or predictor and want Mixle to pick a UQ route

mixle.inference.uq

Should the LLM answer or abstain?

LLMUncertainty.calibrate and answer

Which claim in this answer is suspect?

LLMUncertainty.assess_claims

Is uncertainty due to prompt/model sensitivity?

LLMUncertainty.decompose

How do multiple modalities update a latent?

reason, Evidence, Latent

Should a local task model escalate?

CalibratedTaskModel and Cascade