loke.dev
Header image for The Truth in the Logits: How I Finally Tamed AI Hallucinations with Token-Level Logprobs

The Truth in the Logits: How I Finally Tamed AI Hallucinations with Token-Level Logprobs

Stop guessing if your AI is lying and start measuring its confidence using raw token-level probability data to build a programmatic uncertainty monitor.

· 4 min read

Your Large Language Model is a pathological liar with a world-class poker face. It will tell you that the square root of 1,234,567 is exactly 1,111.11 with the same unwavering, clinical confidence it uses to tell you that 2+2=4. The problem isn’t that the model is "stupid"—it’s that by the time you see the text on your screen, the model has already hidden all its internal hesitation.

If you want to stop the "hallucination-induced panic" that comes with shipping AI features, you have to stop looking at the final string and start looking at the logits.

The "Vibes-Based" Testing Trap

Most developers debug LLM outputs using what I call "Vibes-Based Testing." You run a prompt, read the result, and if it sounds smart, you ship it. When it fails in production, you try to "prompt engineer" the lies away.

"You are a helpful, honest assistant who never lies," you tell the model.

Newsflash: The model doesn't know what a lie is. It only knows what token is statistically likely to follow the previous one. But underneath that confident output, the model is actually doing a lot of math. For every word it picks, it generates a probability score for thousands of other potential words. When it’s hallucinating, those scores are usually a mess of uncertainty.

What are Logprobs, anyway?

When an LLM generates text, it doesn't just "pick a word." It produces a list of "logprobs" (logarithmic probabilities) for every possible token in its vocabulary.

If the model is 99.9% sure the next word is "Paris," the logprob will be near zero (e.g., -0.001). If it’s guessing between "Paris," "London," and "Berlin" with 33% certainty each, the logprob will be much lower (around -1.1).

By tapping into these numbers, we can build a Programmatic Uncertainty Monitor.

Show Me the Code: Fetching the Data

Most major APIs (OpenAI, Together, Groq) allow you to return these logprobs. Here is how you'd grab them using the OpenAI Python SDK. Note that we have to explicitly ask for them.

import openai
import math

client = openai.OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "What is the specific gravity of Osmium?"}],
    logprobs=True, # This is the magic toggle
    top_logprobs=1 # Return the top 1 most likely token's data
)

# Digging into the response object
token_data = response.choices[0].logprobs.content

for token in token_data:
    # Convert logprob to linear probability (0 to 1)
    probability = math.exp(token.logprob) * 100
    print(f"Token: '{token.token}' | Confidence: {probability:.2f}%")

Building a "Truth Meter"

Simply seeing the numbers isn't enough. You need a strategy to turn those numbers into an actionable signal. I've found that averaging probabilities across the whole response is often misleading. One tiny, high-stakes hallucination (like a "not" or a specific date) can be buried in a sea of high-confidence filler words like "The" and "is."

Instead, look for the minimum confidence bottleneck. If any single factual token falls below a certain threshold (say, 60%), flag the entire response.

Here is a simple wrapper to calculate a "Certainty Score":

def analyze_response_reliability(response):
    logprobs = response.choices[0].logprobs.content
    
    # We ignore common "filler" tokens to avoid noise
    # (In a real app, you might use a more robust stop-word list)
    ignore_tokens = [" ", "\n", ".", "the", "a", "of", "and"]
    
    critical_probs = [
        math.exp(t.logprob) for t in logprobs 
        if t.token.strip().lower() not in ignore_tokens
    ]
    
    if not critical_probs:
        return 1.0
        
    # Minimum probability is usually the best indicator of a hallucination
    min_prob = min(critical_probs)
    avg_prob = sum(critical_probs) / len(critical_probs)
    
    return {
        "is_reliable": min_prob > 0.40, # 40% threshold for "danger zone"
        "min_confidence": round(min_prob, 4),
        "avg_confidence": round(avg_prob, 4)
    }

# Example usage
# result = analyze_response_reliability(response)
# if not result['is_reliable']:
#    trigger_human_in_the_loop()

The "Gotchas" of Tokenization

This isn't a silver bullet. You’ll run into two main headaches:

1. The Tokenization Gap: LLMs don't see words; they see tokens. The word "hallucination" might be three tokens: hallu, cin, and ation. If the model is unsure about the *start* of the word, the logprobs for the subsequent pieces might actually be high because they are grammatically inevitable once the first part is chosen. You need to look at the *first* token of a multi-token word.
2. The Calibration Problem: Some models are "overconfident." They might give a 90% probability to a complete lie because they've been RLHF’d (Reinforcement Learning from Human Feedback) to sound authoritative. This is why you should always pair logprobs with a prompt that encourages the model to say "I don't know."

Why this actually matters for your UI

Imagine you're building a tool that extracts data from legal contracts. If you just display the text, the user has to verify everything.

But if you use logprobs, you can visually highlight the parts of the text where the AI was "sweating."

* High confidence: Black text.
* Medium confidence: Orange text (Check this!).
* Low confidence: Red text with a "Verify Source" button.

This transforms the AI from a "black box that might lie" into a "collaborative tool that knows its own limits."

Stop Guessing, Start Measuring

Stop asking your LLM if it's sure. It's a machine; it will always try to please you. Instead, look at the logits. The raw probability data is the only place where the model’s "poker face" slips. If the logprobs are low, the model is guessing—and if it's guessing, you probably shouldn't be shipping that output to your users without a warning.

It takes about twenty lines of code to implement a basic logprob check. Your users (and your sanity) will thank you.