乐于分享
好东西不私藏

AI智能体工具选择的完整指南

AI智能体工具选择的完整指南

在本文中,你将了解为什么智能体准确性随工具目录增长而退化,以及六种保持工具选择在大规模时准确高效的实用技术。

我们将涵盖的主题包括:

• 为什么给智能体添加更多工具会导致工具幻觉和准确性损失,不仅仅是更慢的响应

• 门控、检索、路由和规划如何各自缩小模型在必须选择工具之前看到的内容

• 如何构建回退逻辑和基准测试框架,以测量这些修复是否真正奏效

这些都不需要更大的模型,只需要更智能地控制模型在行动之前看到什么。

你构建了一个有五个工具的智能体。它在演示中完美运行。三个月后,它有了40个文件操作、CRM访问、Slack、日历和为不同团队拼接的三个不同搜索API。那个在每次演示中表现完美的同一智能体现在调用错误的工具、借用不同工具schema的参数幻觉、或在中途停滞等待一个本不该发出的调用。

模型没有改变。工具列表变了。这不是你最终会遇到的边缘情况。这是每个发布后增长的智能体的默认轨迹。分析MCP生态系统中工具描述的研究发现大量描述至少包含一个质量问题,生产基准显示智能体准确性在工具数量超过约10到15时可测量地退化。2025年5月发表的RAG-MCP论文对此修复给出了硬数据:基于检索的工具选择将工具选择准确性从13.62%提升到43.13%——超过三倍——同时将同一基准任务的提示令牌削减超过一半。

工具选择不是你以后修补的微小实现细节。它是决定智能体是否能与真实工具目录共存的架构决策。本指南涵盖六种解决它的技术,按你实际部署的顺序排列:门控、检索、路由、规划、回退逻辑和告诉你其中任何一项是否有效的基准测试。


为什么工具选择在大规模时崩溃

每个工具定义——其名称、描述和参数schema——在每次请求时都发送给模型,无论该工具是否被使用。50多个工具可能消耗模型5到7%的上下文,在用户实际消息到达之前就挤占了对话历史和任务实际需要的推理空间。

"迷失在中间"效应加剧了这个问题。模型对上下文窗口开头和结尾的信息回忆远比中间埋藏的信息可靠。数十个几乎相同的工具定义顺序堆叠时,真正适合任务的工具恰好在那个死区中——不是因为模型无法推理它,而是因为注意力结构性地被拉向其他地方。

第二个失败模式更糟:工具幻觉。当LLM的注意力分散在太多相似的工具上时,它要么发明不存在的工具名称,要么在调用正确工具时借用不同工具schema的参数。这是一个硬失败。调用不存在的函数没有"稍微错误"的方式。

OpenAI文档了每个智能体128个工具的硬上限,但实际退化远在该限制之前就显现;大多数生产团队在活跃轮换中超过15到20个工具时看到准确性明显下降。修复不是更大的上下文窗口。而是控制模型首先看到什么。


门控:决定是否需要工具

在你优化选择哪个工具之前,先问一个更便宜的问题:这一轮需要工具吗?相当一部分智能体轮次是纯对话性的:“谢谢”、“你什么意思”、后续澄清。在每个轮次上运行完整的检索和工具选择推理意味着即使答案是"不需要工具"也要支付完整的智能体开销。

门控是一个快速、廉价的分类器——有时是一个小模型调用,有时只是模式匹配——在任何昂贵操作之前运行。

# gate.py
# 前置条件:Python标准库之外无需任何依赖 (re)
# 运行:python gate.py

import re

CONVERSATIONAL_PATTERNS = [
r"^\s*(thanks|thank you|thx|ok|okay|cool|got it|sounds good|sure|great)\b",
r"^\s*(hi|hello|hey|good morning|good evening)\b",
r"^\s*what do you mean\b",
r"^\s*can you (clarify|explain that)\b",
]

ACTION_KEYWORDS = [
"send""create""search""find""look up""schedule""book",
"read""write""query""summarize""translate""check",
]

defgate(query: str) -> dict:
"""
    便宜的前置过滤器,决定完整的工具选择管道
    是否需要运行。在检索、路由或规划触发之前
    短路对话性轮次。
    """

    q_lower = query.strip().lower()

# 层级1:正则匹配已知对话模式 -- 近零成本
for pattern in CONVERSATIONAL_PATTERNS:
if re.match(pattern, q_lower):
return {"tool_needed"False"reason""conversational_pattern""tier"1}

# 层级2:如果没有动作动词且消息很短,可能不需要工具
    has_action_keyword = any(kw in q_lower for kw in ACTION_KEYWORDS)
ifnot has_action_keyword andlen(q_lower.split()) < 5:
return {"tool_needed"False"reason""short_with_no_action_keyword""tier"2}

return {"tool_needed"True"reason""action_keyword_or_long_query""tier"2}


if __name__ == "__main__":
    test_queries = [
"thanks!",
"What's the weather like in Lagos today?",
"ok",
"Can you send an email to the sales team about the delay?",
    ]
for q in test_queries:
        result = gate(q)
print(f"'{q}' -> tool_needed={result['tool_needed']} ({result['reason']})")

这成本几乎为零,并在有意义份额的轮次到达管道昂贵部分之前就捕获了它们。构建的门槛很低:如果即使20-30%的轮次是对话性的,门控在延迟和令牌成本上立即就赚回了自身。


基于检索的工具选择

这是有最强已发表证据支持的技术。不是每次调用都发送每个工具定义,而是在向量存储中索引工具描述、嵌入传入查询、仅检索前K个最相关工具、并只将那些发送给模型。

RAG-MCP框架是这个想法的参考实现,使用语义检索在LLM看到完整目录之前识别最相关的MCP工具。报告的数字不是微妙的:工具选择准确性从暴露完整目录的13.62%上升到检索过滤选择的43.13%,超过三倍准确性提升,同时将同一基准任务的提示令牌削减超过50%。

# retriever.py
# 前置条件:pip install sentence-transformers faiss-cpu numpy
# 运行:python retriever.py

import numpy as np
from sentence_transformers import SentenceTransformer
import faiss

TOOL_CATALOG = [
    {"name""search_web""description""Search the web for current information on any topic"},
    {"name""read_file""description""Read the contents of a file given its path"},
    {"name""write_file""description""Write or overwrite content to a file at a given path"},
    {"name""send_email""description""Send an email to a recipient with subject and body"},
    {"name""create_calendar_event""description""Create a new calendar event with a title, date, and time"},
    {"name""query_database""description""Run a SQL query against the company database"},
    {"name""list_github_issues""description""List open issues in a GitHub repository"},
    {"name""create_github_pr""description""Create a pull request on a GitHub repository"},
    {"name""send_slack_message""description""Send a message to a Slack channel or user"},
    {"name""get_weather""description""Get current weather conditions for a city"},
    {"name""translate_text""description""Translate text from one language to another"},
    {"name""summarize_document""description""Summarize a long document into key points"},
    {"name""lookup_stock_price""description""Get the current stock price for a ticker symbol"},
    {"name""book_flight""description""Search and book a flight between two cities"},
    {"name""create_invoice""description""Generate an invoice for a customer with line items"},
]

classToolRetriever:
"""
    在启动时嵌入工具描述并索引到FAISS中。
    在运行时,嵌入传入查询并仅返回前K个
    最相关工具——不是完整目录。
    """

def__init__(self, tools: list[dict], model_name: str = "all-MiniLM-L6-v2"):
self.tools = tools
self.model = SentenceTransformer(model_name)
        descriptions = [f"{t['name']}{t['description']}"for t in tools]
        embeddings = self.model.encode(descriptions, normalize_embeddings=True)
# IndexFlatIP = 内积搜索,当向量归一化时等于余弦相似度
self.index = faiss.IndexFlatIP(embeddings.shape[1])
self.index.add(np.array(embeddings, dtype=np.float32))

defretrieve(self, query: str, top_k: int = 3) -> list[dict]:
        query_emb = self.model.encode([query], normalize_embeddings=True)
        scores, indices = self.index.search(np.array(query_emb, dtype=np.float32), top_k)
return [
            {**self.tools[idx], "score"float(score)}
for score, idx inzip(scores[0], indices[0])
        ]


if __name__ == "__main__":
    retriever = ToolRetriever(TOOL_CATALOG)

    queries = [
"What's the weather like in Lagos today?",
"Can you check if there are any open bugs in our repo?",
"Send a message to the engineering channel about the deploy",
    ]
for q in queries:
        results = retriever.retrieve(q, top_k=3)
print(f"\nQuery: '{q}'")
for r in results:
print(f"  {r['name']} (score={r['score']:.3f})")

每个查询只从15工具目录中发送前3个工具给模型,每次调用减少80%的工具定义,准确性提升叠加因为模型现在在少量真正相关的候选之间选择而非扫描过十几个近似的。


语义路由

路由是检索的更轻量版本,适合不同形状的问题。检索从平面列表中回答"哪个具体工具"。路由回答"哪个工具箱"——当你的工具自然聚类到类别(数据、通信、日程)中且你想只加载相关类别的工具而非每次重新排名整个目录时有用。

# router.py
# 前置条件:pip install scikit-learn numpy
# 运行:python router.py

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

CATEGORIES = {
"data":          ["query the database""read a file""write data to storage""run a SQL query"],
"communication": ["send an email""post a slack message""notify the team""send a message"],
"scheduling":    ["create a calendar event""book a meeting""schedule an appointment"],
}

classSemanticRouter:
"""
    使用与类别质心嵌入的相似性将查询路由到工具类别。
    当没有类别达到置信阈值时回退到'general'——
    从不在低置信匹配上猜测。
    """

def__init__(self, categories: dict[strlist[str]], confidence_threshold: float = 0.15):
self.threshold = confidence_threshold
self.vectorizer = TfidfVectorizer()
        all_examples = [ex for exs in categories.values() for ex in exs]
self.vectorizer.fit(all_examples)

# 通过平均其示例嵌入为每个类别构建一个质心向量
self.centroids = {}
for cat, examples in categories.items():
            vecs = self.vectorizer.transform(examples).toarray()
self.centroids[cat] = vecs.mean(axis=0)

defroute(self, query: str) -> dict:
        query_vec = self.vectorizer.transform([query]).toarray()[0]
        scores = {
            cat: float(cosine_similarity([query_vec], [centroid])[0][0])
for cat, centroid inself.centroids.items()
        }
        best_cat   = max(scores, key=scores.get)
        best_score = scores[best_cat]

if best_score < self.threshold:
return {"category""general""confidence": best_score}

return {"category": best_cat, "confidence": best_score}


if __name__ == "__main__":
    router = SemanticRouter(CATEGORIES)

    test_queries = [
"Can you post a message in the sales Slack channel?",
"I need to run a query against our production database",
"Schedule a meeting with the design team for tomorrow",
"asdkj qpwoe zxcv nonsense",
    ]
for q in test_queries:
        result = router.route(q)
print(f"'{q}' -> {result['category']} (confidence={result['confidence']:.3f})")

在胡乱查询上回退到"general"和正确路由一样重要。一个总是挑选某东西的路由器——即使对它没有真正信号的查询——比一个承认不知道的路由器更危险。


基于规划器的工具选择

检索和路由都回答"与这单轮相关的是什么"。多步任务需要不同的东西:提前规划的工具调用序列,每个步骤只限定到它特别需要的工具。这是避免所谓的"上帝智能体"反模式的架构——一个持有20个工具在上下文中但没有计划结构的智能体——任何地方的失败都会腐蚀整个任务。

模式:首先让模型输出一个结构化计划——一个有序子任务列表,每个标记有它需要的能力——在任何工具执行之前。然后按步骤检索工具,限定到该步骤的标记。

# planner.py
# 前置条件:Python标准库之外无需任何依赖 (json)
# 运行:python planner.py

import json
from dataclasses import dataclass

@dataclass
classPlanStep:
    step_number: int
    description: str
    required_capability: str

defparse_plan(raw_plan_json: str) -> list[PlanStep]:
"""将规划器LLM的JSON输出解析为结构化PlanStep对象。"""
    data = json.loads(raw_plan_json)
return [
        PlanStep(s["step_number"], s["description"], s["required_capability"])
for s in data["steps"]
    ]

# 能力 -> 工具名称映射。在生产中,这从前一节的检索器中获取,
# 限定到仅标记为该能力的工具。
CAPABILITY_TOOLS = {
"search":        ["search_web""query_database"],
"file_io":       ["read_file""write_file"],
"communication": ["send_email""send_slack_message"],
"synthesis":     ["summarize_document"],
}

defget_scoped_tools(step: PlanStep) -> list[str]:
"""仅返回与此步骤相关的工具——不是完整目录。"""
return CAPABILITY_TOOLS.get(step.required_capability, [])


if __name__ == "__main__":
# 这个JSON通常来自LLM调用,请求它将任务分解为步骤,
# 每个标记有所需能力。
    mock_plan = json.dumps({
"steps": [
            {"step_number"1"description""Search for the latest sales report file""required_capability""search"},
            {"step_number"2"description""Read the contents of the report file""required_capability""file_io"},
            {"step_number"3"description""Summarize the key findings""required_capability""synthesis"},
            {"step_number"4"description""Email the summary to the sales lead""required_capability""communication"},
        ]
    })

    plan = parse_plan(mock_plan)
for step in plan:
        scoped = get_scoped_tools(step)
print(f"Step {step.step_number}{step.description}")
print(f"  Capability: {step.required_capability} -> tools available: {scoped}")

此示例中的每个步骤看到一两个工具,永远不是完整集合。这就是规划真正帮助的机制:不是模型有计划时推理更好;而是计划让你合法地缩小每步的工具列表,这是检索拉动同一杠杆在更细粒度上的应用。


回退逻辑

检索和路由有时会失败,不是因为架构错误,而是因为真实查询是模糊的、不够明确的或确实在工具目录覆盖范围之外的。当最佳匹配的置信度低时你做什么,决定了你的智能体是优雅退化还是开始猜测。

三层回退链处理了这一点,不需要诉诸于只会崩溃对话的try/except:高置信时直接解决、低置信时用重新表述的查询重试、以及当重试也失败时升级为明确的澄清请求而非强制工具调用。

# fallback.py
# 前置条件:pip install scikit-learn numpy
# 运行:python fallback.py

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

TOOL_CATALOG = [
    {"name""search_web""description""Search the web for current information on any topic"},
    {"name""get_weather""description""Get the current weather forecast for a city"},
    {"name""send_email""description""Send an email to a recipient with subject and body"},
    {"name""list_github_issues""description""List open issues and bugs in a GitHub repository"},
]

classRetrieverWithFallback:
"""
    将检索包装在三层回退链中:
    1. 高置信度  -> 直接使用顶级结果
    2. 低置信度  -> 用重新表述的查询重试
    3. 仍然低   -> 升级为澄清请求,从不猜测
    """

def__init__(self, tools, confidence_threshold: float = 0.12):
self.tools = tools
self.threshold = confidence_threshold
self.vectorizer = TfidfVectorizer()
        descs = [f"{t['name']}{t['description']}"for t in tools]
self.tool_vectors = self.vectorizer.fit_transform(descs)

def_raw_retrieve(self, query: str):
        query_vec = self.vectorizer.transform([query])
        sims = cosine_similarity(query_vec, self.tool_vectors)[0]
        top_idx = int(np.argmax(sims))
returnself.tools[top_idx], float(sims[top_idx])

defretrieve_with_fallback(self, query: str) -> dict:
        tool, score = self._raw_retrieve(query)
if score >= self.threshold:
return {"status""resolved""tool": tool["name"], "confidence": score, "attempts"1}

# 通过去除填充词重新表述。在生产中,此步骤会是
# LLM调用,请求它用意图/能力术语重述查询。
        reformulated = query.replace("can you""").replace("please""").replace("?""").strip()
        tool2, score2 = self._raw_retrieve(reformulated)
if score2 >= self.threshold:
return {"status""resolved""tool": tool2["name"], "confidence": score2, "attempts"2}

return {
"status""escalated""tool"None,
"confidence"max(score, score2), "attempts"2,
"clarification_request": (
f"I'm not confident which tool fits '{query}'. "
f"Could you clarify what you'd like me to do?"
            ),
        }


if __name__ == "__main__":
    retriever = RetrieverWithFallback(TOOL_CATALOG)

for q in ["What's the weather forecast in Lagos?""xyzzy plugh random nonsense"]:
        result = retriever.retrieve_with_fallback(q)
print(f"Query: '{q}'")
print(f"  Status: {result['status']}")
if result["status"] == "resolved":
print(f"  Tool: {result['tool']} (confidence={result['confidence']:.3f})")
else:
print(f"  {result['clarification_request']}")

升级路径是大多数团队初次构建时跳过的,也是在生产中最重要的。一个自信但错误的工具调用比一个问"我不确定,你能澄清吗?"的系统更糟糕。第二个失败模式在一轮中可恢复。第一个通常不可恢复。


基准测试你的工具选择系统

以上所有在没有测量之前都是假设。方法论很简单:构建一组标记的(查询,正确工具)对,运行你的管道,并测量准确性、令牌成本和延迟,将过滤管道与朴素全目录基线进行比较。MCPToolBench++——从4000多个真实MCP服务器跨40多个类别构建的大规模基准——是在规模上应该有多严谨的参考,但核心思想在任何规模都适用。

# benchmark.py
# 前置条件:pip install scikit-learn numpy
# 运行:python benchmark.py

import time
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

TOOL_CATALOG = [
    {"name""search_web""description""Search the web for current information on any topic"},
    {"name""read_file""description""Read the contents of a file given its path"},
    {"name""write_file""description""Write or overwrite content to a file at a given path"},
    {"name""send_email""description""Send an email to a recipient with subject and body"},
    {"name""create_calendar_event""description""Create a new calendar event with a title and time"},
    {"name""query_database""description""Run a SQL query against the company database"},
    {"name""list_github_issues""description""List open issues and bugs in a GitHub repository"},
    {"name""send_slack_message""description""Send a message to a Slack channel or user"},
    {"name""get_weather""description""Get current weather conditions for a city"},
    {"name""book_flight""description""Search and book a flight between two cities"},
]

# 标记基准集:(查询, 期望工具)。从真实日志查询构建你的集合
# 一旦有生产流量——这只是一个种子集。
BENCHMARK_SET = [
    ("What's the weather in Abuja right now?""get_weather"),
    ("Send an email to the finance team""send_email"),
    ("List the open issues on our main repo""list_github_issues"),
    ("Book me a flight from Lagos to London""book_flight"),
    ("Query the database for last week's signups""query_database"),
    ("Post an update in the team Slack channel""send_slack_message"),
    ("Search the web for the latest interest rates""search_web"),
    ("Read the contents of config.yaml""read_file"),
]

defestimate_tokens(text: str) -> int:
"""粗略令牌估计(1令牌 ~ 4字符)-- 相对比较足够好。"""
returnlen(text) // 4

classBenchmarkHarness:
"""将标记查询集通过检索器运行并报告准确性、令牌成本和延迟。"""

def__init__(self, tools: list[dict], top_k: int = 3):
self.tools = tools
self.top_k = top_k
self.vectorizer = TfidfVectorizer()
        descs = [f"{t['name']}{t['description']}"for t in tools]
self.tool_vectors = self.vectorizer.fit_transform(descs)
self.full_catalog_tokens = sum(estimate_tokens(d) for d in descs)

def_retrieve(self, query: str, top_k: int) -> list[dict]:
        query_vec = self.vectorizer.transform([query])
        sims = cosine_similarity(query_vec, self.tool_vectors)[0]
        top_indices = np.argsort(sims)[::-1][:top_k]
return [self.tools[i] for i in top_indices]

defrun(self, benchmark_set: list[tuple], use_retrieval: bool = True) -> dict:
        correct, total_tokens, latencies = 00, []

for query, expected_tool in benchmark_set:
            t0 = time.perf_counter()

if use_retrieval:
                candidates = self._retrieve(query, top_k=self.top_k)
                tokens_this_query = sum(
                    estimate_tokens(f"{t['name']}{t['description']}"for t in candidates
                )
else:
# 基线:每次发送完整的、未过滤的目录
                candidates = self.tools
                tokens_this_query = self.full_catalog_tokens

if expected_tool in [c["name"for c in candidates]:
                correct += 1
            total_tokens += tokens_this_query
            latencies.append(time.perf_counter() - t0)

        n = len(benchmark_set)
        latencies_sorted = sorted(latencies)
return {
"accuracy":       round(correct / n, 4),
"avg_tokens":     round(total_tokens / n, 1),
"p50_latency_ms"round(latencies_sorted[len(latencies_sorted) // 2] * 10003),
"p95_latency_ms"round(latencies_sorted[int(len(latencies_sorted) * 0.95)] * 10003),
        }


if __name__ == "__main__":
    harness = BenchmarkHarness(TOOL_CATALOG, top_k=3)

    baseline  = harness.run(BENCHMARK_SET, use_retrieval=False)
    retrieval = harness.run(BENCHMARK_SET, use_retrieval=True)

print("Baseline (full catalog every time):")
print(f"  Accuracy:         {baseline['accuracy']*100:.1f}%")
print(f"  Avg tokens/query: {baseline['avg_tokens']}")

print("\nRetrieval-filtered (top-3):")
print(f"  Accuracy:         {retrieval['accuracy']*100:.1f}%")
print(f"  Avg tokens/query: {retrieval['avg_tokens']}")

    reduction = (1 - retrieval["avg_tokens"] / baseline["avg_tokens"]) * 100
print(f"\nToken reduction with retrieval: {reduction:.1f}%")

在这个10工具目录和8查询基准集上,检索过滤保持了准确性稳定同时将平均每查询令牌削减约70%。确切数字会随你的目录和查询集而变化,但比较结构才是关键:你现在有一个可重复的方式来回答"这个变更是否真正有帮助",而不是依赖少数手动抽查。


总结

这六种技术不是竞争选项;它们是层级。门控便宜地在其他任何东西运行之前过滤掉不需要工具的轮次。检索或路由为剩余轮次缩小目录到真正相关的内容。规划序列化多步任务使每步只看到需要的工具。回退逻辑捕获第一次尝试不干净落地的情况。基准测试是你知道以上是否产生了可测量差异的方式,而非只是感觉更好。

RAG-MCP的结果——准确性超过三倍提升和令牌减半——不是异常值。这是一旦你停止要求模型在每次决策前翻阅完整电话簿后可预测发生的情况。这些技术都不需要更大的模型或更长的上下文窗口。它们要求将工具列表本身视为需要设计的东西,而不是只是追加到。

参考资源:

• RAG-MCP:通过检索增强生成缓解LLM工具选择中的提示膨胀

• MCPToolBench++:大规模MCP工具使用基准