工具不是越多越好:AI Agent 工具选择策略的设计与优化

一、工具膨胀的困境
AI Agent 的能力边界由其可用工具决定。给 Agent 10 个工具,它能做 10 类事;给 100 个工具,它反而可能什么都做不好。这不是悖论,而是 LLM 的实际限制。
Function Calling 的机制是:将所有工具的名称、参数定义、描述拼接到 prompt 中,让 LLM 选择调用哪个工具。当工具数量增加时,三个问题随之而来:
- Token 开销:每个工具定义平均消耗 50-100 token,100 个工具就是 5000-10000 token,直接挤占上下文空间
- 选择精度下降:工具越多,LLM 区分相似工具的难度越大,误选率上升
- 延迟增加:更多的工具定义意味着更长的输入序列,推理时间线性增长
某客服 Agent 项目中,工具从 15 个增长到 80 个后,工具选择准确率从 92% 降到 71%。用户问"退款进度",Agent 调用了"创建退款"而不是"查询退款"。工具选择策略不是锦上添花,而是 Agent 系统可用性的决定性因素。
二、工具选择策略的架构设计
2.1 多级工具选择架构
flowchart TD
Q[用户请求] --> R[意图识别]
R --> C[工具分类器<br/>确定工具类别]
C --> C1[订单类工具]
C --> C2[支付类工具]
C --> C3[账户类工具]
C --> C4[内容类工具]
C1 --> S1[子集工具列表<br/>3-8个工具]
C2 --> S2[子集工具列表<br/>3-8个工具]
C3 --> S3[子集工具列表<br/>3-8个工具]
C4 --> S4[子集工具列表<br/>3-8个工具]
S1 --> LLM[LLM Function Calling]
S2 --> LLM
S3 --> LLM
S4 --> LLM
LLM --> CALL[调用选定工具]
CALL --> RESULT[返回结果]
RESULT --> EVAL{结果是否有效?}
EVAL -->|否| RETRY[重试或换工具]
EVAL -->|是| RESP[生成最终回复]
RETRY --> LLM
style C fill:#e8f5e9
style LLM fill:#fff3e0
2.2 三种工具选择策略对比
| 策略 | 核心思路 | 工具数量上限 | 选择延迟 | 适用场景 |
|---|---|---|---|---|
| 全量注入 | 所有工具定义都放入 prompt | ≤ 15 | 低 | 工具少的简单 Agent |
| 两阶段选择 | 先分类再选择 | ≤ 100 | 中 | 工具多的复杂 Agent |
| 向量检索 | 根据请求语义检索相关工具 | 无上限 | 中高 | 工具极多的平台级 Agent |
三、工具选择策略的工程实现
3.1 两阶段选择:分类器 + 精选
from typing import List, Dict, Optional
from dataclasses import dataclass, field
import json
@dataclass
class Tool:
"""工具定义"""
name: str
description: str
category: str # 工具所属类别
parameters: Dict # JSON Schema 参数定义
required: List[str] # 必填参数列表
examples: List[str] = field(default_factory=list) # 使用示例
@dataclass
class ToolCategory:
"""工具类别定义"""
name: str
description: str
keywords: List[str] # 触发该类别的关键词
tools: List[Tool] # 该类别下的工具列表
class TwoStageToolSelector:
"""两阶段工具选择器:先分类,再精选"""
def __init__(self, categories: List[ToolCategory], llm_client):
self.categories = categories
self.llm = llm_client
# 构建类别索引
self.category_map = {c.name: c for c in categories}
def select_tools(
self,
query: str,
max_tools: int = 8,
) -> List[Tool]:
"""两阶段选择:先确定类别,再从类别中精选工具"""
# 第一阶段:确定工具类别
category = self._classify_category(query)
if category is None:
# 分类失败,回退到全量工具(但限制数量)
return self._fallback_select(query, max_tools)
# 第二阶段:从类别中精选工具
selected_tools = self._select_from_category(
query, category, max_tools
)
return selected_tools
def _classify_category(self, query: str) -> Optional[ToolCategory]:
"""第一阶段:基于关键词和 LLM 确定工具类别"""
# 先用关键词快速匹配(零 LLM 调用)
for cat in self.categories:
for keyword in cat.keywords:
if keyword in query.lower():
return cat
# 关键词未命中,用 LLM 做分类
category_names = [c.name for c in self.categories]
category_descs = [
f"- {c.name}: {c.description}" for c in self.categories
]
prompt = (
f"根据用户请求,判断应该使用哪类工具。\n\n"
f"可用类别:\n{''.join(category_descs)}\n\n"
f"用户请求:{query}\n\n"
f"只输出类别名称,不要解释。"
)
result = self.llm.chat(prompt, max_tokens=20).strip()
if result in self.category_map:
return self.category_map[result]
return None
def _select_from_category(
self,
query: str,
category: ToolCategory,
max_tools: int,
) -> List[Tool]:
"""第二阶段:从类别中精选工具"""
# 如果类别下工具数量 ≤ max_tools,全部返回
if len(category.tools) <= max_tools:
return category.tools
# 否则用 LLM 做精选
tool_descriptions = [
f"- {t.name}: {t.description}" for t in category.tools
]
prompt = (
f"从以下工具中选择与用户请求最相关的 {max_tools} 个:\n\n"
f"{''.join(tool_descriptions)}\n\n"
f"用户请求:{query}\n\n"
f"只输出工具名称,每行一个,不要编号。"
)
result = self.llm.chat(prompt, max_tokens=200)
selected_names = [
line.strip() for line in result.strip().split("\n")
if line.strip()
]
# 按名称匹配工具
tool_map = {t.name: t for t in category.tools}
selected = []
for name in selected_names[:max_tools]:
if name in tool_map:
selected.append(tool_map[name])
# 如果 LLM 选择不足,补充类别中的前 N 个
if len(selected) < max_tools:
for tool in category.tools:
if tool not in selected:
selected.append(tool)
if len(selected) >= max_tools:
break
return selected
def _fallback_select(self, query: str, max_tools: int) -> List[Tool]:
"""回退策略:从所有工具中选择"""
all_tools = []
for cat in self.categories:
all_tools.extend(cat.tools)
if len(all_tools) <= max_tools:
return all_tools
# 简单策略:返回每个类别的前 2 个工具
selected = []
for cat in self.categories:
for tool in cat.tools[:2]:
selected.append(tool)
if len(selected) >= max_tools:
return selected
return selected
3.2 向量检索式工具选择
import numpy as np
from typing import List, Tuple
class VectorToolSelector:
"""基于向量检索的工具选择器:适合工具数量极多的场景"""
def __init__(self, tools: List[Tool], embedding_model, top_k: int = 8):
self.tools = tools
self.embedding_model = embedding_model
self.top_k = top_k
# 预计算所有工具的向量表示
self.tool_embeddings = self._build_tool_embeddings()
def _build_tool_embeddings(self) -> np.ndarray:
"""为每个工具构建向量表示"""
# 将工具名称 + 描述 + 示例拼接为文本
texts = []
for tool in self.tools:
text = f"{tool.name}: {tool.description}"
if tool.examples:
text += f" 示例: {'; '.join(tool.examples)}"
texts.append(text)
embeddings = self.embedding_model.embed_documents(texts)
return np.array(embeddings)
def select_tools(self, query: str) -> List[Tool]:
"""根据查询语义检索最相关的工具"""
query_embedding = np.array(
self.embedding_model.embed_query(query)
)
# 计算余弦相似度
similarities = np.dot(self.tool_embeddings, query_embedding) / (
np.linalg.norm(self.tool_embeddings, axis=1) *
np.linalg.norm(query_embedding) + 1e-8
)
# 取 Top-K
top_indices = np.argsort(similarities)[::-1][:self.top_k]
# 过滤低相似度结果
selected = []
for idx in top_indices:
if similarities[idx] > 0.5: # 相似度阈值
selected.append(self.tools[idx])
return selected
3.3 工具描述优化:提升选择精度的低成本手段
工具描述是 LLM 做选择的唯一依据。描述写得不好,选择策略再精妙也没用。
# 反面示例:模糊描述,LLM 无法区分
BAD_TOOL = Tool(
name="query_order",
description="查询订单信息", # 太笼统
category="order",
parameters={...},
required=["order_id"],
)
# 正面示例:精确描述,包含触发条件和边界
GOOD_TOOL = Tool(
name="query_order",
description=(
"根据订单ID查询订单详情,包括订单状态、金额、商品列表。"
"适用于用户询问'我的订单到哪了'、'订单进度'、'物流信息'等场景。"
"不适用于创建订单(用 create_order)或取消订单(用 cancel_order)。"
),
category="order",
parameters={...},
required=["order_id"],
examples=[
"我的订单 12345 到哪了",
"查询订单 67890 的物流",
],
)
描述优化的核心原则:
- 说清楚做什么:功能边界,能做什么、不能做什么
- 给出触发条件:什么情况下应该选这个工具
- 区分相似工具:明确说明与相似工具的区别
- 提供使用示例:2-3 个典型用户输入示例
3.4 工具选择结果评估
class ToolSelectionEvaluator:
"""工具选择效果评估器"""
def __init__(self, selector):
self.selector = selector
def evaluate(
self,
test_cases: List[Dict], # [{"query": ..., "expected_tools": [...]}]
) -> Dict[str, float]:
"""评估工具选择的准确率"""
correct = 0
total = len(test_cases)
precision_sum = 0
recall_sum = 0
for case in test_cases:
selected = self.selector.select_tools(case["query"])
selected_names = {t.name for t in selected}
expected_names = set(case["expected_tools"])
# 命中率:期望工具是否在选中列表中
hits = selected_names & expected_names
if hits:
correct += 1
# 精确率:选中的工具中有多少是正确的
if selected_names:
precision_sum += len(hits) / len(selected_names)
# 召回率:期望的工具中有多少被选中
if expected_names:
recall_sum += len(hits) / len(expected_names)
return {
"hit_rate": correct / total,
"precision": precision_sum / total,
"recall": recall_sum / total,
}
四、工具选择策略的权衡与边界
4.1 策略选择的决策树
- 工具 ≤ 15 个:全量注入,不需要额外策略
- 工具 15-50 个:两阶段选择,按业务域分类
- 工具 50-200 个:两阶段选择 + 向量检索混合
- 工具 > 200 个:向量检索为主,分类器辅助
4.2 工具描述的维护成本
工具描述不是写一次就完事的。每次新增工具,都需要检查是否与现有工具产生歧义。维护策略:
- 定期审计:每月检查工具描述的区分度,用评估器跑测试用例
- 变更评审:新增工具时,检查与现有工具的语义重叠
- A/B 测试:工具描述修改后,用评估数据对比修改前后的选择准确率
4.3 不适合复杂选择策略的场景
- 工具 ≤ 10 个:全量注入的准确率已经足够,额外策略增加复杂度
- 单次调用成本极低:选错工具的代价低,重试即可
- 工具使用频率极度不均:80% 的请求只用 3-5 个工具,直接固定这几个工具的优先级
五、总结
工具选择策略的核心目标是:在 LLM 的选择精度和工具数量之间找到平衡。工具少时全量注入最简单有效;工具多时两阶段选择(分类 + 精选)是工程上最务实的方案;工具极多时向量检索可以突破数量上限。但无论哪种策略,工具描述的质量都是选择精度的基础。描述要精确、有边界、有示例、有区分度。
落地路线:先用全量注入快速上线,监控工具选择的误选率;误选率超过 10% 时引入两阶段选择;工具超过 50 个时叠加向量检索。每一步都用评估数据验证效果,不要在没有量化数据的情况下切换策略。工具描述的优化投入产出比最高,优先于架构层面的调整。

1925

被折叠的 条评论
为什么被折叠?



