Why Selection Accuracy Degrades
The model picks a tool the way it picks a token: by relevance against everything on offer. With three to five tools, the choice is easy. With thirty tools and overlapping descriptions, the model grabs a near-duplicate, and the task fails in a way that looks like a reasoning error but is really a menu problem.
The costs compound:
- Wrong tool, wrong result: the model calls
orders_searchwhen it neededget_order, and the answer is plausible but wrong. - Wasted recovery: the model must notice the mistake and retry, which is the failure mode covered in agent error recovery.
- Token burn: every tool schema in the prompt costs tokens on every request, before the model decides anything.
| Menu size | Selection behavior | Typical symptom |
|---|---|---|
| 3–5 tools | Reliable | Occasional bad arguments |
| 10–15 tools | Degrades | Picks near-duplicate tools |
| 30+ tools | Unreliable without pruning | Random-looking failures |
The fix is not a better prompt. It is a smaller menu. Everything in this chapter is a technique for shrinking the decision space before the model ever sees it.
This is also a context window problem, not just an accuracy problem. Every tool schema in the prompt consumes tokens that could hold conversation history or retrieved documents. See context window budgeting for the full budget picture — tool schemas are one of the largest fixed costs in it.
Pruning Before the Model
The cheapest selection aid is not showing the model tools it cannot use. If the task is a refund review, the weather tools should not be in the prompt.
Prune in two stages. First, filter by domain tags — the same tags a tool registry assigns:
def prune_tools(all_tools: list[dict], task_domains: list[str]) -> list[dict]:
return [t for t in all_tools if set(t["domains"]) & set(task_domains)]
Input: 40 registered tools, task_domains=["refunds"]. Output: the 6 tools tagged refunds. The model now chooses from 6 candidates instead of 40.
When domains are too coarse, add a keyword prefilter over descriptions:
def keyword_prefilter(tools: list[dict], instruction: str) -> list[dict]:
terms = set(instruction.lower().split())
return [t for t in tools if terms & set(t["description"].lower().split())]
Input: instruction "look up refund policy for order ORD-42", 6 refund-domain tools. Output: the 2 tools whose descriptions contain refund or order.
Pruning has a failure mode: filter too aggressively and the right tool disappears, so the model improvises with a worse one. Keep the prefilter permissive — recall matters more than precision at this stage.
Embedding-Based Shortlisting
Keyword matching misses paraphrases. The instruction "what's the status of my purchase" shares no terms with a tool described as "fetch order by id", yet they are the same intent. Embeddings close that gap.
Embed the instruction and every candidate description, then keep the top-k by cosine similarity:
import numpy as np
def shortlist(instruction: str, tools: list[dict], k: int = 5) -> list[dict]:
instruction_vec = embed([instruction])[0]
tool_vecs = np.stack([embed([t["description"]])[0] for t in tools])
scores = tool_vecs @ instruction_vec
top_k = np.argsort(-scores)[:k]
return [tools[i] for i in top_k]
The dot product equals cosine similarity when vectors are normalized, which most embedding models output by default. See embedding models for how the vectors are produced and similarity metrics for why the angle, not the magnitude, carries the meaning.
Input: instruction "what's the status of my purchase", 40 tools. Output: the 5 closest descriptions — get_order, list_orders, track_shipment, lookup_return, order_history — ranked by semantic distance.
Shortlisting costs one embedding call per tool description, but descriptions can be embedded once and cached; only the instruction needs a fresh embedding per request. The result feeds straight into the pruning pipeline: embed, shortlist, then hand the survivors to the model.
Embeddings are not a silver bullet. They can rank a near-duplicate above the right tool when descriptions are poorly written, and they add latency from the embedding call itself. The robust design is a cascade: domain prune first (free, deterministic), keyword prefilter second (cheap), embedding shortlist third (semantic). Each stage shrinks the set before the next, most expensive stage runs.
Deduplicating Tools and Logging Decisions
Registries accumulate near-duplicates: search_orders, orders_search, and get_orders are three tools with one intent. Every duplicate is a chance for the model to pick the wrong one. Consolidate them under one parameterized tool:
search_tool = {
"name": "search",
"description": "Search a named index for records",
"parameters": {
"type": "object",
"properties": {
"index": {"type": "string", "enum": ["orders", "products", "customers"]},
"query": {"type": "string"},
},
"required": ["index", "query"],
},
}
One tool, three jobs, zero ambiguity about which to call. The index enum makes the choice explicit instead of implicit in the tool name.
Then log every selection decision — chosen tool and rejected alternatives:
def log_selection(instruction: str, chosen: str, rejected: list[str]) -> None:
record = {
"instruction": instruction,
"chosen": chosen,
"rejected": rejected,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
selection_log.append(record)
Rejected alternatives are the signal. If the log shows get_order losing to orders_search on 40% of lookups, the descriptions need rewriting — a systematic mis-selection pattern you can fix before it reaches users.
Failure modes to watch:
- Logging only chosen tools: you cannot see the near-misses that predict systematic mis-selection.
- Over-consolidation: one parameterized tool that does two jobs badly is worse than two clear tools.
- Over-pruning: the right tool is filtered out, and the model improvises with whatever survived.
Quiz
1. Why does tool selection accuracy degrade as the menu grows?
2. What is the purpose of pruning the tool list before invoking the model?
3. Why use embeddings instead of keyword matching for shortlisting?
4. Why log rejected tool alternatives, not just the chosen tool?
Flashcards
Question
Why does selection accuracy drop as tools multiply?
Click to reveal answer
Answer
More candidates mean more near-duplicates; the model picks a plausible-looking wrong tool, and the failure looks like a reasoning error but is really a menu problem.
Question
What is the first line of defense against a large tool menu?
Click to reveal answer
Answer
Prune by task context — domain tags first, then a keyword prefilter — before invoking the model, so the decision space stays small.
Question
When do embeddings beat keyword matching for shortlisting?
Click to reveal answer
Answer
When the instruction paraphrases the description — different words, same intent. Cosine similarity over embeddings catches what term overlap misses.
Question
How do you shrink near-duplicate tools?
Click to reveal answer
Answer
Consolidate them under one parameterized tool — a single search tool with an index enum — instead of three overlapping tools.
Question
Why log rejected tool alternatives?
Click to reveal answer
Answer
Near-misses reveal systematic mis-selection patterns — the same wrong tool picked repeatedly — before they cause user-facing incidents.
Revision Notes
Key Takeaways
- 1. Selection accuracy degrades as tools multiply because near-duplicates make the wrong pick look reasonable
- 2. Prune by domain tags and keyword prefilter before invoking the model — a smaller menu is the fix, not a better prompt
- 3. Embedding shortlisting catches paraphrases where keyword matching sees zero term overlap
- 4. Consolidate near-duplicates under one parameterized tool with an explicit enum
- 5. Logging rejected alternatives exposes systematic mis-selection before it reaches users
Interview Tips
- • Frame it as a system property: selection accuracy is a function of menu size, not model quality
- • Describe the pipeline concretely: prune, prefilter, embed, shortlist, then let the model choose
- • Mention caching: tool descriptions embed once, only the instruction embeds per request
- • Give the logging rationale: rejected alternatives are the early-warning signal for mis-selection
Cheat Sheet
Tool Selection Cheat Sheet
The problem: selection is a relevance decision against everything on offer. Accuracy degrades as the menu grows.
The fix: shrink the decision space before the model sees it.
Techniques in order:
- Prune by domain tags:
discover(task_domains) - Keyword prefilter over descriptions (permissive — recall over precision)
- Embedding shortlist: cosine top-k over cached description vectors
- Consolidate near-duplicates under one parameterized tool
- Log chosen + rejected tools per decision
Embedding shortlist:
scores = tool_vecs @ instruction_vec # normalized vectors: dot product = cosine
top_k = np.argsort(-scores)[:k]
Consolidation pattern: one search tool with an index enum instead of three overlapping tools.
Failure modes: over-pruning (right tool filtered out), over-consolidation (one tool doing two jobs badly), logging only chosen tools (blind to near-misses).
Continue Learning
Before You Start
- No prerequisites listed.