LLM Uncertainty

This tutorial wraps an arbitrary LLM-like callable with mixle.reason.LLMUncertainty. The goal is not to make generation reliable by assertion. The goal is to turn repeated samples into behavior: answer, abstain, inspect disagreement, or escalate.

The wrapper needs only one callable:

generate(prompt) -> answer

For a real system, generate might call a hosted model, a local model, or an internal agent. For a test, it can be a deterministic fixture.

1. Define Equivalence

Semantic uncertainty depends on what counts as the same answer. Exact string matching is fine for normalized labels, but most LLM outputs need a domain relation.

import re
from mixle.reason import LLMUncertainty

def normalize(text):
    return re.sub(r"[^a-z0-9]+", "", str(text).lower())

def equivalent(a, b):
    return normalize(a) == normalize(b)

uq = LLMUncertainty(generate, equivalent=equivalent, n=20)

The equivalence function is part of the model. Record it with the application because changing it changes the uncertainty numbers.

2. Assess One Prompt

assessment = uq.assess("Who discovered penicillin?")

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

confidence is the mass of the majority meaning cluster. semantic_entropy is high when samples disagree about meaning, not merely wording.

3. Decompose Prompt Sensitivity

Use paraphrases as ensemble members when you want to separate within-prompt ambiguity from prompt sensitivity.

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 points to reducible sensitivity: prompt wording, model choice, retrieval context, or missing evidence. Aleatoric uncertainty points to ambiguity that remains inside each prompt.

4. Calibrate Abstention

Raw sample agreement is a signal, not a guarantee. Calibrate it on labeled examples before using it to decide whether to answer.

examples = [
    ("Capital of France?", "Paris"),
    ("2 + 2?", "4"),
    ("Who discovered penicillin?", "Alexander Fleming"),
]

uq.calibrate(examples, correct=equivalent, 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 learned confidence threshold. This is the operational payoff: the model can decline rather than fabricate.

5. Inspect Claim Reliability

A response can have a stable headline answer and still contain unsupported details. Claim assessment extracts claims from one response and checks whether independent samples corroborate them.

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)

For serious use, pass a task-specific claim extractor and an entailment-style corroborates function. The defaults are intentionally lightweight.

Validation Checklist

Before serving an LLM uncertainty wrapper:

  • define and version the equivalence relation;

  • calibrate on examples from the same prompt distribution;

  • monitor abstention rate and false-answer rate together;

  • inspect high-entropy prompts instead of averaging them away;

  • route abstentions to a human, retrieval step, frontier model, or safer fallback.

Read Uncertainty for the full API and Reasoning Systems for claim reliability, graph-producing LLMs, and cross-modal evidence.