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

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

cover

一、工具膨胀的困境

AI Agent 的能力边界由其可用工具决定。给 Agent 10 个工具,它能做 10 类事;给 100 个工具,它反而可能什么都做不好。这不是悖论,而是 LLM 的实际限制。

Function Calling 的机制是:将所有工具的名称、参数定义、描述拼接到 prompt 中,让 LLM 选择调用哪个工具。当工具数量增加时,三个问题随之而来:

  1. Token 开销:每个工具定义平均消耗 50-100 token,100 个工具就是 5000-10000 token,直接挤占上下文空间
  2. 选择精度下降:工具越多,LLM 区分相似工具的难度越大,误选率上升
  3. 延迟增加:更多的工具定义意味着更长的输入序列,推理时间线性增长

某客服 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 的物流",
    ],
)

描述优化的核心原则:

  1. 说清楚做什么:功能边界,能做什么、不能做什么
  2. 给出触发条件:什么情况下应该选这个工具
  3. 区分相似工具:明确说明与相似工具的区别
  4. 提供使用示例: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 个时叠加向量检索。每一步都用评估数据验证效果,不要在没有量化数据的情况下切换策略。工具描述的优化投入产出比最高,优先于架构层面的调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值