Security

When the Model Picks No Tool: Scoring the Abstain Case

Antonio Evans9 min read

A clean diagram with three labeled columns: TOOL CALL, NO TOOL, ASK A PERSON.
Key points

Key points

  1. 1Tool-calling evaluations that score only "correct tool + correct arguments" silently punish a model for the right decision to call nothing.
  2. 2Add two terminal labels to the set: no-tool (nothing applies) and ask-a-person (a human must decide), and score each as its own row.
  3. 3Report a per-class confusion matrix so a cautious abstention is never averaged into the same bucket as a wrong tool call.
  4. 4Use a cost matrix when a missed escalation costs more than a missed tool, and pick the operating threshold from that, not from raw accuracy.

Why the general error bucket lies to you

Most tool-calling harnesses inherit their design from function-calling benchmarks, where the graded question is whether the model picked the right function with the right arguments. The Berkeley Function-Calling Leaderboard is the reference for that style, and even its multi-turn categories grade calls rather than restraint; evaluators have begun to argue that call accuracy is necessary but not sufficient for agent work (NVIDIA's agent evaluation guide). If your harness treats every case as "a tool should have been called," then a model that correctly declines is scored identically to one that fired the wrong function. Your dashboard shows one number called "errors" and you cannot tell safety from incompetence.

This is not a hypothetical gap. When2Call was built specifically because existing benchmarks measured tool-call accuracy and not the decision of when to call a tool at all, including cases where tools are provided but none is correct, and cases where no tools are provided (When2Call). The same paper notes the subtle failure: a model with access only to a student records database, asked for grades, should say it cannot answer rather than hallucinate a tool or an answer.

Agentic abstention is harder than question-answering abstention because the decision sequence is grounded in observable environment state. AgentAbstain pairs each should-act task with a should-abstain variant and reports paired accuracy, where a model only scores if it is right on both sides (AgentAbstain). That pairing is the single most useful design idea you can steal for your own harness, and it is cheap to add.

Defining the two terminal cases

Before writing any scoring code, write down what each label means in your product's terms. Ambiguous labels produce ambiguous scores.

LabelThe model shouldTypical triggerCorrect output
tool_callcall a specific tool with valid argumentsrequest maps cleanly onto one toolfunction call, then a grounded answer
no_toolanswer without calling anythingthe question is general knowledge, chit-chat, or out of scope for every registered toolplain text answer, no function call
ask_a_persondefer to a humanmissing required parameters, conflicting instructions, high-stakes or irreversible actiona clarification question or an escalation handoff
ambiguous(excluded)genuinely unlabelable casequarantine and review

Two decision rules keep labeling consistent across annotators.

First, if the request can be satisfied only by inventing a parameter value the user did not supply, the label is ask_a_person, not tool_call. When2Call makes exactly this distinction: when the tool exists but required parameters are missing, the expected behavior is a follow-up question, not a fabricated argument (When2Call).

Second, if no registered tool can answer, the label is no_tool even if the request sounds like a tool request. The travel agent that cancels a flight before confirming a rebooking exists is the canonical counterexample: the correct move was to stop and check, then ask (AgentAbstain). Label that case ask_a_person.

For the abstain side, build pairs. Take a case that works today and perturb it minimally: remove one required argument, swap a tool for a similar-sounding one that cannot help, or introduce a contradictory instruction. The clean version stays a tool_call case; the perturbed version becomes no_tool or ask_a_person. AgentAbstain generated its paired set through controlled perturbation of instruction, tool, or environment state, and three annotators rated 94 to 98 percent of sampled tasks as well designed (AgentAbstain). You do not need that rigor to start, but you do need pairs, because unpaired abstain cases let a model game the set by refusing broadly.

Extending the harness without rewriting it

Assume you already have a runner that produces, for each case, a trace of tool calls and a final message. The change is small if you keep parsing out of it: classify the trace into an outcome, compare the outcome to the target, and score by class.

TERMINAL = {"tool_call", "no_tool", "ask_a_person"}

def classify(trace, final_text):
    calls = trace.tool_calls
    if len(calls) == 0:
        return "ask_a_person" if asks_question(final_text) else "no_tool"
    if len(calls) == 1:
        return "tool_call"
    return "multi_call"  # keep visible; do not fold into "error"

def score(case, trace, final_text):
    predicted = classify(trace, final_text)
    target = case.target          # one of TERMINAL
    return {
        "case_id": case.id,
        "class": target,
        "predicted": predicted,
        "correct": predicted == target,
        # extra signal for the abstain classes
        "acted_before_asking": target == "ask_a_person" and len(trace.tool_calls) > 0,
    }

Three implementation details matter more than the shape of the function.

Keep a multi_call outcome visible rather than folding it into "error." A model that chains four calls on a one-call task is a different defect from one that picks the wrong function, and collapsing them loses the diagnosis.

Record whether the model acted before it asked. AgentAbstain names post-hoc abstention, where the agent executes an irreversible action and only then acknowledges the problem, as a distinct and more dangerous failure than a wrong answer (AgentAbstain). Your acted_before_asking flag is what catches it.

Do not treat a missing tool call as a failed assertion. Several harnesses, including the widely used lm-evaluation-harness, evaluate tasks by comparing generated output against reference answers or log-likelihoods rather than tool traces (lm-evaluation-harness), which is why teams graft tool logic on ad hoc and then have nowhere to put abstention. Give the abstain classes a first-class return type, not a None.

Scoring abstention as its own row

Report tool-call accuracy as one row, then no_tool and ask_a_person as their own rows. Treat each class as a two-by-two problem: for ask_a_person, the positives are cases that should escalate, and the negatives are cases that should not. Precision tells you how often an escalation was warranted; recall tells you how many warranted escalations you caught. Accuracy alone hides which of the two you are failing, and precision, recall and F1 map directly onto tool correctness and hallucination risk in these pipelines (a developer's guide to LLM evaluation).

Worked example. Suppose your set has 400 cases: 200 tool_call, 80 no_tool, 120 ask_a_person. The model is right on 180 of the tool cases. On the 80 no-tool cases it calls a tool 16 times. On the 120 escalation cases it answers directly 24 times and escalates correctly the other 96. A single pooled accuracy figure is 0.90 plus 0.80 plus 0.80, weighted: (180 + 64 + 96) / 400 = 0.85. Now separate them.

ClassnCorrectClass accuracyMisses that matter
tool_call2001800.9020 wrong or malformed calls
no_tool80640.8016 unnecessary calls (each a possible side effect)
ask_a_person120960.8024 missed escalations
pooled4003400.85hides which 60 errors you can tolerate

The pooled number is the least useful figure on the page. Whether you ship depends on which misses land on a production system. An unnecessary call to a read-only search tool costs latency; an unnecessary call to a payment or deletion tool does not. That distinction is why access scope should be bounded independently of model behavior: default-deny allowlists and per-persona tool scopes mean a wrong call has a bounded blast radius regardless of what your eval says (Zscaler on least-privilege for agents, Oso on over-permissioned agents).

Also score the paired form. For each pair, count the case correct only if both the clean and perturbed variants are correct, then report that alongside the per-class figures. AgentAbstain found the best agent across 17 frontier models and 4 harnesses reached only 59.5 percent paired accuracy, and that abstention ability was largely independent of general task-solving ability (AgentAbstain). That independence is the finding that should shape your roadmap: improving your tool-call score will not automatically improve the abstain rows.

Using a cost matrix to set the threshold

Per-class scores tell you what happened. A cost matrix tells you what to do about it. Assign a cost to each off-diagonal outcome, in the units your organization actually feels, then compute expected cost per 1,000 cases.

OutcomeCost (illustrative)Why
missed escalation, low stakes4user retries or gives up; small satisfaction hit
missed escalation, high stakes60irreversible action, complaint, possible loss
unnecessary tool call, read-only1latency and tokens only
unnecessary tool call, state-changing80duplicate record, wrong refund, corrupted state
wrong tool, correct args3recoverable, usually caught by validation
correct abstention0the task was to abstain

With those numbers, a model that misses 24 of 120 escalations and makes 16 unnecessary no-tool calls costs 24 × 60 + 16 × 1 = 1,456 per run, if every missed escalation is high stakes. A second model that catches every escalation but makes 40 unnecessary read-only calls costs 40. The second model has the lower pooled accuracy in most such comparisons and is the correct choice. This is the decision rule the headline metric cannot give you: weight by where the miss lands, not how many misses there are.

Set the escalation threshold from this table rather than tuning against accuracy. If high-stakes misses cost 60 and unnecessary state-changing calls cost 80, the crossover is close, which is an argument for lowering autonomy rather than tuning the prompt. Autonomy levels L1 and L2, where the agent executes defined tasks but escalates ambiguous or high-stakes decisions, are the target for most production agents (MLflow's tool-use best practices). Your cost matrix is the quantitative justification for that choice.

Reading per-class confusion in the final report

Build the report so a reader can answer four questions without opening a notebook: which class regressed, in which direction, on which paired set, and at what cost.

Put class accuracy, paired accuracy and expected cost per 1,000 cases on the same page. Add a short "top failure" line per class with one real trace ID. Teams underinvest in abstention because it shows up as a vague error count rather than a named failure with an example attached; naming the post-hoc abstention case next to the trace is what gets it fixed (AgentAbstain).

Two reporting traps recur.

The first is averaging across classes with different sizes. A 400-case set where ask_a_person is 30 percent of examples will have its pooled score dominated by tool cases. Always show per-class denominators.

The second is scoring single attempts. A model that gets the right call most of the time can still fail a multi-step task when errors compound; consistency metrics that measure success across repeated attempts are a better predictor of production behavior than one-shot scores (Splunk's review of agent benchmarks). Run each abstain case three to five times and report the pass rate, not just the last run.

For the abstain classes specifically, add the false-positive side. A model can score well on ask_a_person recall by escalating constantly; the over-refusal side of the act/abstain boundary is a real trade-off, not a rounding error (AgentAbstain). Your no_tool and clean-pair rows are what keep an over-cautious model honest.

If you are building this in-house and want a second opinion on the label taxonomy and cost table before you wire it into CI, a free 30-minute discovery call with Quanta Consulting is a reasonable place to pressure-test the design (https://quantaintelligence.ai/calendar).

A checklist for the first pass

Use this as the acceptance criteria for extending the harness. Each item is testable.

  • Every eval case carries exactly one target from tool_call, no_tool, ask_a_person.
  • At least half the abstain cases are paired with a clean twin that differs by one controlled perturbation.
  • The classifier records multi_call and acted_before_asking separately from plain errors.
  • The report shows class accuracy, class denominator, paired accuracy and expected cost per 1,000 cases.
  • Each abstain case runs at least three times and reports a pass rate.
  • The cost table is signed off by whoever owns the downstream system the tools touch.
  • Tool scopes are default-deny per persona, so that a wrong call's blast radius is bounded by policy, not by eval score (Oso, Arthur on scoping agent access).

That last item is the bridge between evaluation and engineering. Your eval tells you the model's tendency to abstain; your permission model decides what happens when the tendency is wrong. Both need to be true before an agent with write access goes anywhere near production.

FAQ

FAQ

Is abstention really a separate class, or just a type of answer?

It is a separate class because the expected output differs in kind: no function call, or a question back to the user. Benchmarks such as When2Call were built around precisely that distinction, evaluating when not to call a tool as its own decision rather than as an answer-quality problem (When2Call). Scoring it inside a general error bucket erases the distinction.

How many abstain cases do I need before the numbers mean anything?

Enough that the class denominator can absorb normal run-to-run variance. As a working rule, size each class so that a five-point change is visible, and run each case several times. AgentAbstain's 263 paired tasks across 42 environments is a research-scale set; a few dozen well-labeled pairs is enough to catch a regression (AgentAbstain).

A model that asks a question every time will score well on escalation. How do I stop that?

Score the clean half of each pair as strictly as the perturbed half, and report false escalations alongside missed ones. The over-refusal and under-refusal sides of the act/abstain boundary are a two-sided trade-off, so any metric that rewards only one direction will be gamed by a cautious model (AgentAbstain).

Does a good tool-calling score predict good abstention?

Largely not. Abstention capability appears largely independent of general task-solving capability, so scaling your task-solving evals will not close the gap on the abstain rows (AgentAbstain). Budget for the abstain set as its own workstream, not as a byproduct.

What is the fastest way to get the first version running?

Perturb existing passing cases rather than authoring new ones: drop a required argument, swap in a plausible but useless tool, or add a contradictory instruction. That gives you pairs with known provenance in an afternoon, and it is the same controlled-perturbation approach the AgentAbstain authors used to build their paired set (AgentAbstain).

Sources

Sources

  1. How to Evaluate AI Agents From Tool Calls to Task ...
  2. When2Call: When (not) to Call Tools
  3. AgentAbstain: Do LLM Agents Know When Not to Act?
  4. GitHub - EleutherAI/lm-evaluation-harness: A framework for few-shot evaluation of language models. · GitHub
  5. A Developer's Guide to Reliable LLM Evaluation
  6. How to Establish Least-Privilege for AI Agents and Assistants
  7. How to Prevent Over-Permissioned Agents
  8. AI Agent Tool Use Best Practices for Practitioners - MLflow
  9. Evaluating AI Agents on Tool Calling and Planning
  10. Access Management for AI Agents: Scope What They Touch
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles