Skip to content

Tool preference: what moves a model's choice between equal tools

cli.py tools preference measures how a model chooses between two tools that do the same job, and how much one change to one of them moves that choice: a vendor-branded name, an assertive or a social-proof sentence, a longer description, a "do NOT use for …" clause, or simply its place in the list.

Use it before you tune a skill or MCP tool description to win selection. On the models measured so far, assertive wording moved the choice share by +0.3 to +0.45 and a longer description by +0.1 to +0.25. A selection rate measured on one wording therefore says little about the next one.

Input: groups

A tool_preference_groups.v1 file lists groups. Each group is one user request, two tools that serve it equally well (the twins A and B), and distractors that do not. A group with "expect": "none" is a control: its request needs no tool, and calling one counts as over-calling.

{
  "schema_version": "tool_preference_groups.v1",
  "groups": [{
    "id": "G01",
    "query": "Find where the retry backoff is configured in the docs for our HTTP client.",
    "twins": {
      "A": {"name": "search_docs", "description": "Search the project documentation and return matching passages.",
            "vendor_name": "acme_docs_search",
            "detail": "Covers the user guide, API reference and changelog.",
            "example": "search_docs(query='retry backoff')",
            "exclusion": "Do NOT use this tool for editing documentation files."},
      "B": {"name": "find_documentation", "description": "Look up passages in the project's reference and user guides."}
    },
    "distractors": [{"name": "read_file", "description": "Read the contents of a file at a given path."}]
  }]
}

The twins' description is the neutral baseline. The optional fields feed the factors: vendor_name for vendor_name, detail and example for long_desc, and exclusion for exclusion. A factor a twin has no field for is skipped for that twin. cues overrides the sentence assertive and social_proof append, and system overrides the system prompt (an empty string sends none). domain is a free-form label copied to every item of the group in the result, for slicing. The full format is tool_preference_groups.v1.json.

The groups are evaluation content: keep them with the tools they are about, not in this repository.

Design

The twins sit at the two ends of the tool list, with the distractors in between in the order given. Every factor changes one twin (the treated one) and is crossed with which twin is treated and whether it is listed first or last. The baseline offers both orders. A pair group therefore yields 2 + 4 per factor items, and a control group 2 + 2 per factor.

Each item is one call with tool_choice="auto", scored on its first turn only, at the provider's default sampling. Current reasoning models accept only temperature=1 (see Pinning sampling), so the spread is measured with --reps rather than pinned away.

python cli.py tools preference --groups groups.json --dry-run      # count items and calls
python cli.py tools preference --groups groups.json \
  --model claude-haiku-5-5 --provider anthropic --reps 3 --out-json pref.json

--max-cost-usd caps the spend: once the priced calls have spent it, no new call starts. The calls not run are kept as skipped, left out of the summary (n_skipped), and the command exits 2. Calls already in flight finish, so the spend can pass the cap by up to --concurrency − 1 calls. A reply cut at --max-tokens before any tool call is billed, so it counts. A call that comes back with no price leaves the cap blind: no new call starts after it, and the command exits 2. Items run group by group; at any concurrency the last group reached may be partial. Partial groups are summarized from the cells that ran, so their A/B counterbalancing is incomplete. A call priced at exactly 0 does not move the cap.

Reading the result

tool_preference.v1 keeps every call (results: the tools called in the first turn, and an outcome A, B, both, distractor, unmatched or none), the settings that were sent (max_tokens, and the system prompt and cues with the defaults filled in) and a summary:

field meaning
factor_effects.<factor> treated twin's choice share minus the same tool's share at the same position in the baseline; mean over groups, 95% bootstrap CI that resamples groups
first_twin_share_baseline share of the first-listed twin when both are neutral; 0.5 is no position bias
pair_outcomes.both_twins share of first turns that called both twins in parallel (counted as 0.5 for each twin in the shares above)
pair_outcomes.distractor, .none first turns that took a wrong tool, or no tool
pair_outcomes.unmatched first turns that called only names the item did not offer (made up, or empty); counted as over-calling on controls
control_over_call.<factor> share of control calls that used any tool
same_choice_rate, modal_share across --reps: items whose outcome never changed, and the mean share of the most common outcome

A call that failed is kept with its error and left out of the summary (n_errors); so is a reply cut at --max-tokens before any tool call. A reply cut after its tool call is scored, since only the tool's name counts, and flagged truncated (n_truncated).

Example: claude-haiku-5-5 on Bedrock, 40 pair groups and 8 controls, 3 reps (2928 calls, $0.53):

first-listed twin share (baseline): +0.48 [+0.40, +0.56] (groups=40)
  assertive     +0.44 [+0.39, +0.48] (groups=40)
  exclusion     +0.05 [+0.01, +0.08] (groups=40)
  long_desc     +0.20 [+0.13, +0.28] (groups=40)
  social_proof  -0.02 [-0.06, +0.02] (groups=40)
  vendor_name   -0.01 [-0.07, +0.05] (groups=40)
pair items: both twins 0.278, distractor 0.0114, no call 0.0345
controls, any tool called: assertive 0.0, baseline 0.0, exclusion 0.0, long_desc 0.0, social_proof 0.0, vendor_name 0.0
same choice on every rep: 0.8576 (modal share 0.9498)

Two cautions:

  • The probe scores one call per item, the way a single-call selection proxy does. On a real harness the direction held, but the size was smaller: for a skill whose "use when" sentence was removed, a one-call proxy showed −0.42 and Claude Code −0.20. Confirm a large effect on the harness you ship to.
  • Score choices from the calls, not with an LLM judge. A judge that sees the conversation but not the tool definitions cannot see a description effect at all.