Skip to content

Confidence & Thresholds

The answer tells you what; the probabilities tell you whether to act on it. A system that can say "I'm not sure" is one you can build safe automation on.

Probability versus confidence

  • Noul: noul is the probability of yes. There's no separate confidence; a value near 0.5 is the model saying "I can't tell".
  • Choice / Score: probabilities is the full distribution, and confidence (0–1) summarizes how concentrated it is. A single peak gives high confidence; probability spread across options gives low confidence.

Confidence is not the winner's probability. A winner at 0.45 with a runner-up at 0.44 and a winner at 0.45 with the rest scattered thinly are different situations, and confidence is what separates them.

Three bands

A useful starting point divides confidence into three ranges, each with its own behavior:

when {
    // High: act automatically.
    team.confidence >= 0.8 -> routeTo(team.choice.name, "auto")

    // Medium: act, but flag the case for a second look.
    team.confidence >= 0.5 -> routeTo(team.choice.name, "flag for review")

    // Low: don't act on a guess.
    else -> routeTo("triage", "human decides")
}

For a Noul, band() does the same with the probability of yes; see Noul.

Thresholds scale with risk

A threshold isn't one number. Different actions in the same system deserve different bars, depending on what it costs to get them wrong:

enum class BankAction(
    override val description: String,
) : JevOption {
    CHECK_BALANCE("Check the balance of an account"),
    APPROVE_TRANSFER("Approve the pending transfer request"),
    OTHER("Something else"),
}

object VoiceCommand : JevQuery() {
    val action by choice<BankAction>("What action is the user requesting?")
}

// The bar for acting rises with the cost of being wrong.
suspend fun handleVoiceCommand(
    jev: JevApi,
    utterance: String,
): String {
    val action = jev.ask(VoiceCommand, state = utterance)[VoiceCommand.action]
    return when {
        action.confidence < 0.6 -> "route to a support agent"

        // Cheap to get wrong: the floor is enough.
        action.choice == BankAction.CHECK_BALANCE -> "read the balance"

        // Costly to get wrong: act alone only when very sure.
        action.choice == BankAction.APPROVE_TRANSFER && action.confidence > 0.85 -> "approve the transfer"

        action.choice == BankAction.APPROVE_TRANSFER -> "ask the user to confirm first"

        else -> "route to a support agent"
    }
}

Falling back to a coarser answer

With hierarchical labels, an uncertain fine-grained answer can still yield a confident coarse one. In TypeSafe's SEC-filing example, answers above 0.9 confidence were right 90% of the time and those below only 40%. Reporting the uncertain half at the parent level raised the useful answers from 39 of 60 to 48 of 60.

// With a hierarchy of labels, fall back to the parent when the fine-grained answer is uncertain.
enum class Industry(
    val division: String,
) {
    PHARMACEUTICALS("manufacturing"),
    SEMICONDUCTORS("manufacturing"),
    LIFE_INSURANCE("finance"),
    BANKING("finance"),
    SOFTWARE("services"),
}

object IndustryQuery : JevQuery() {
    val industry by choice<Industry>(
        "Which industry does this company operate in? Judge its own operations as this filing describes them.",
    )
}

suspend fun classify(
    jev: JevApi,
    filing: String,
): String {
    val industry = jev.ask(IndustryQuery, state = filing)[IndustryQuery.industry]
    return if (industry.confidence >= 0.9) industry.choice.name else industry.choice.division
}

Combining answers

When a decision uses several answers, its certainty is bounded by the weakest one:

// A decision built from several answers is only as sure as its least certain part.
fun combinedConfidence(vararg parts: ChoiceAnswer<*>): Double = parts.minOf { it.confidence }

Choosing thresholds

  • Start conservative, then tune on labeled examples. Plot confidence against accuracy on your own data.
  • Tie thresholds to a model version. When you pin a model, tuned thresholds stay valid; moving to a new release means re-checking them. See Choosing a model.
  • Expect small run-to-run variation. Identical requests usually return identical values, but borderline answers can shift slightly, so don't set a threshold razor-close to your typical values.
  • Ignore uncertainty that doesn't matter. A low-confidence answer to a speculative question your code isn't using needs no handling. Likewise, when several options would be acceptable, low confidence is harmless.