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:
noulis the probability of yes. There's no separate confidence; a value near 0.5 is the model saying "I can't tell". - Choice / Score:
probabilitiesis the full distribution, andconfidence(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.