Machine Learning Mastery

The Complete Guide to Tool Selection in AI Agents

8.5内容质量

TL;DR · AI 摘要

AI代理工具选择影响准确性,六种技术(检索、路由等)可提升效率,RAG-MCP研究显示准确率提升至43.13%。

核心要点

  • 工具数量超过10-15个时,AI代理准确性显著下降
  • RAG-MCP研究显示检索方法使准确率从13.62%提升至43.13%
  • 六种核心解决方案:检索、路由、规划、门控、回退逻辑、基准测试

结构提纲

按章节快速跳转。

  1. AI代理工具选择问题随工具数量增长而加剧,需系统性解决方案

  2. 工具定义占用模型上下文导致准确性下降,'中间丢失'效应加剧问题

  3. 六种技术(检索、路由等)通过结构化过滤提升选择准确性

  4. 基准测试验证方案有效性,RAG-MCP研究提供量化数据支撑

  5. 演示从5工具到40工具的系统退化案例及修复效果

  6. 工具选择是AI代理架构核心决策,需优先解决

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AI代理工具选择优化
    • 问题原因
      • 上下文占用
      • 中间丢失效应
    • 解决方案
      • 检索
      • 路由
      • 规划
      • 门控
      • 回退逻辑
      • 基准测试
    • 验证数据
      • RAG-MCP研究
      • 生产基准

金句 / Highlights

值得收藏与分享的关键句。

#AI代理#工具选择#RAG-MCP#模型优化#机器学习
打开原文

AI代理工具选择完整指南 - MachineLearningMastery.com

AI代理工具选择完整指南

作者:

Shittu Olumide

2026年7月6日

分类:

人工智能

0

分享

本文将介绍为什么随着工具目录的扩大,代理的准确性会下降,以及六个实用技巧,用于在大规模场景下保持工具选择的准确性和效率。

我们将涵盖的主题包括:

  • 为什么向代理添加更多工具会导致工具幻觉和准确性下降,而不仅仅是响应速度变慢。
  • 门控、检索、路由和规划如何在模型选择工具之前缩小模型所看到的内容范围。
  • 如何构建回退逻辑和基准测试框架,以便衡量这些修复措施是否真正有效。

所有这些都不需要更大的模型,只需要更智能地看待模型在行动前所看到的内容。

AI代理工具选择完整指南 点击放大

引言

你构建了一个包含五个工具的代理。在演示中,它完美无缺。三个月后,它拥有40个文件操作、CRM访问权限、Slack集成、日历功能,以及为不同团队添加的三个不同搜索API。原本在演示中表现完美的同一个代理,现在却会调用错误的工具,借用其他工具架构中的参数进行幻觉,或在任务中途等待一个本不该发生的调用。

模型本身没有变化,工具列表变了。这不是你最终会遇到的边缘情况,而是每个部署后不断增长的代理的默认轨迹。对生态系统中MCP工具描述的分析发现,大量工具中至少存在一个质量问题,而生产基准测试显示,当工具数量超过约10到15个时,代理的准确性会显著下降。2025年5月发表的《RAG-MCP论文》给出了修复的具体数据:在相同的基准任务中,基于检索的工具选择将工具选择准确性从13.62%提升至43.13%,同时将提示词数量减少了超过一半。

工具选择不是后期修补的次要实现细节,而是决定代理能否与真实工具目录成功交互的架构决策。本指南将按实际部署顺序介绍六个解决方法:门控、检索、路由、规划、回退逻辑,以及判断这些方法是否有效的基准测试。

为什么工具选择在规模扩大时会失效

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

“中间迷失效应”进一步加剧了这一问题。模型在上下文窗口的开头和结尾部分回忆信息的可靠性远高于中间部分的信息。当数十个几乎相同的工具定义按顺序堆叠时,真正适合任务的工具往往正好位于这个死亡区域,被忽视的原因不是模型无法推理,而是注意力结构上被其他地方吸引。

第二种失败模式更为严重:工具幻觉。当LLM的注意力分散到太多相似的工具上时,它要么编造不存在的工具名称,要么在调用正确工具时借用其他工具的参数格式。这是一种严重故障。调用不存在的函数没有“略微错误”的方式。

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", ] def gate(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) if not has_action_keyword and len(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']})")

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

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"

,

"^\s*(hi|hello|hey|good morning|good evening)\b"

"^\s*what do you mean\b"

"^\s*can you (clarify|explain that)\b"

]

ACTION_KEYWORDS

"send"

"create"

"search"

"find"

"look up"

"schedule"

"book"

"read"

"write"

"query"

"总结"

"翻译"

"检查"

def

gate

(

query

:

str

)

->

dict

""

"

廉价的预过滤器,决定是否需要运行完整的工具选择流程。在检索、路由或规划触发之前,直接截断对话轮次。

q_lower

.

strip

lower

第1层:与已知对话模式进行正则表达式匹配 -- 接近零成本

for

pattern

if

match

return

{

"tool_needed"

False

"reason"

"conversational_pattern"

"tier"

}

第2层:如果消息中没有动作动词且内容简短,很可能不需要工具

has_action_keyword

any

kw

not

and

len

split

<

"short_with_no_action_keyword"

True

"action_keyword_or_long_query"

__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?"

q

result

print

f

"'{q}' -> tool_needed={result['tool_needed']} ({result['reason']})"

运行方式(无需依赖项):

python gate.py

python

py

这几乎不消耗资源,能在对话到达流程昂贵部分前拦截大量轮次。构建价值的门槛很低:如果20-30%的对话属于纯对话类型,这种过滤机制能在延迟和token成本上立即实现收支平衡。

基于检索的工具选择

这是目前有最强实证支持的技术方案。与每次调用都发送所有工具定义不同,该方法将工具描述索引到向量存储中,对查询进行嵌入,仅检索最相关的前K个工具,再将这些工具发送给模型。

RAG-MCP框架是该思路的参考实现,使用语义检索在LLM看到完整工具目录前,就识别出最相关的MCP工具。实验数据具有说服力:当完整目录可见时工具选择准确率为13.62%,采用检索过滤后提升至43.13%(准确率提升超3倍),同时在相同基准任务中减少超过50%的提示token。

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": "搜索网络以获取任何主题的最新信息"}, {"name": "read_file", "description": "根据文件路径读取文件内容"}, {"name": "write_file", "description": "将内容写入指定路径的文件或覆盖现有内容"}, {"name": "send_email", "description": "向收件人发送带有主题和正文的电子邮件"}, {"name": "create_calendar_event", "description": "创建带有标题、日期和时间的新日历事件"}, {"name": "query_database", "description": "对公司数据库执行SQL查询"}, {"name": "list_github_issues", "description": "列出GitHub仓库中的开放问题"}, {"name": "create_github_pr", "description": "在GitHub仓库中创建拉取请求"}, {"name": "send_slack_message", "description": "向Slack频道或用户发送消息"}, {"name": "get_weather", "description": "获取城市的当前天气状况"}, {"name": "translate_text", "description": "将文本从一种语言翻译成另一种语言"}, {"name": "summarize_document", "description": "将长文档总结为关键点"}, {"name": "lookup_stock_price", "description": "获取股票代码的当前股价"}, {"name": "book_flight", "description": "在两个城市之间搜索并预订航班"}, {"name": "create_invoice", "description": "为客户生成带有明细项的发票"}, ]

class ToolRetriever: """在启动时一次性嵌入工具描述并使用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))

def retrieve(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 in zip(scores[0], indices[0]) ]

if __name__ == "__main__": retriever = ToolRetriever(TOOL_CATALOG) queries = [ "今天拉各斯的天气如何?", "能检查一下我们仓库中是否有开放的bug吗?", "向工程频道发送关于部署的消息", ] 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})")

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

retriever.py

依赖项:pip install sentence-transformers faiss-cpu numpy

运行方式:python retriever.py

numpy

as

np

from

sentence_transformers

SentenceTransformer

faiss

TOOL_CATALOG

"name"

"search_web"

"description"

"搜索网络以获取任何主题的最新信息"

"read_file"

"根据文件路径读取文件内容"

"write_file"

"将内容写入指定路径的文件"

"send_email"

"向收件人发送带有主题和正文的电子邮件"

"create_calendar_event"

"创建带有标题、日期和时间的新日历事件"

"query_database"

"对公司数据库执行SQL查询"

"list_github_issues"

"列出GitHub仓库中的开放问题"

"create_github_pr"

"在GitHub仓库中创建拉取请求"

"send_slack_message"

"向Slack频道或用户发送消息"

"get_weather"

"获取某城市的当前天气状况"

"translate_text"

"将文本从一种语言翻译为另一种语言"

"summarize_document"

"将长文档总结为关键点"

"lookup_stock_price"

"获取股票代码的当前股价"

"book_flight"

"在两个城市之间搜索并预订航班"

"create_invoice"

"为客户生成包含明细项的发票"

class

ToolRetriever

在启动时一次性嵌入工具描述并使用FAISS进行索引。

运行时,对传入的查询进行嵌入并仅返回前-K

最相关的工具——而非完整工具目录。

__init__

self

tools

list

model_name

"all-MiniLM-L6-v2"

model

descriptions

"{t['name']}: {t['description']}"

t

embeddings

encode

normalize_embeddings

IndexFlatIP = 内积搜索,等同于余弦相似度

当向量归一化时——这是此用例的标准设置。

index

IndexFlatIP

shape

add

array

dtype

float32

retrieve

top_k

int

query_emb

scores

indices

search

*

idx

"score"

float

score

zip

retriever

queries

"你能检查一下我们仓库中是否有开放的bug吗?"

"向工程频道发送有关部署的消息"

results

"\nQuery: '{q}'"

" {r['name']} (score={r['score']:.3f})"

如何运行:

pip install sentence-transformers faiss-cpu numpy python retriever.py

pip

install

sentence

-

transformers

cpu

每次查询只从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": ["查询数据库", "读取文件", "将数据写入存储", "运行SQL查询"], "communication": ["发送电子邮件", "发布Slack消息", "通知团队", "发送消息"], "scheduling": ["创建日历事件", "预订会议", "安排预约"], } class 语义路由器: """ 使用与类别中心点嵌入的相似性将查询路由到工具类别。当没有类别达到置信度阈值时回退到'general'——从不猜测低置信度匹配。 """ def __init__(self, categories: dict[str, list[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) def route(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 in self.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 = 语义路由器(CATEGORIES) test_queries = [ "你能将消息发布到销售Slack频道吗?", "我需要对生产数据库运行查询", "为明天与设计团队安排会议", "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 class PlanStep: step_number: int description: str required_capability: str def parse_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"], } def get_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": "搜索最新的销售报告文件", "required_capability": "search"}, {"step_number": 2, "description": "读取报告文件内容", "required_capability": "file_io"}, {"step_number": 3, "description": "总结关键发现", "required_capability": "synthesis"}, {"step_number": 4, "description": "将摘要通过电子邮件发送给销售负责人", "required_capability": "communication"}, ] }) plan = parse_plan(mock_plan) for step in plan: scoped = get_scoped_tools(step) print(f"步骤 {step.step_number}: {step.description}") print(f" 能力: {step.required_capability} -> 可用工具: {scoped}")

planner.py

依赖项:仅需Python标准库(json)

运行方式:python planner.py

json

dataclasses

dataclass

@

PlanStep

step_number

description

required_capability

parse_plan

raw_plan_json

"将规划器LLM的JSON输出解析为结构化PlanStep对象。"

data

loads

s

"step_number"

"required_capability"

"steps"

能力到工具名称的映射。生产环境中会将此传递给检索器

来自上一节的内容,仅限于标记为该能力的工具。

CAPABILITY_TOOLS

"file_io"

"synthesis"

get_scoped_tools

step

"仅返回与当前步骤相关的工具——而非完整工具目录。"

此 JSON 通常来自对 LLM 的调用,要求其将任务分解为多个步骤,每个步骤都标记所需的技能

mock_plan

dumps

"搜索最新的销售报告文件"

"读取报告文件的内容"

"总结关键发现"

"将摘要通过电子邮件发送给销售负责人"

plan

scoped

"步骤 {step.step_number}: {step.description}"

"所需技能: {step.required_capability} -> 可用工具: {scoped}"

python planner.py

planner

在本例中的每一步骤都只会看到一到两个工具,而非全部工具集。这就是规划之所以能起作用的实际机制:并非模型在拥有计划时推理能力更强,而是计划能让你合法地按步骤缩小工具列表,这与检索所使用的杠杆原理相同,只是粒度更精细。

回退逻辑

检索和路由有时都会失败,这并非因为架构存在缺陷,而是因为实际查询可能存在歧义、描述不充分或确实超出了工具目录的覆盖范围。当最高匹配项的置信度较低时所采取的措施,决定了你的代理是优雅降级还是开始猜测。

三级回退链可以在不使用会导致对话崩溃的 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": "搜索网络以获取任何主题的最新信息"}, {"name": "get_weather", "description": "获取城市的当前天气预报"}, {"name": "send_email", "description": "向收件人发送带有主题和正文的电子邮件"}, {"name": "list_github_issues", "description": "列出GitHub仓库的开放问题和漏洞"}, ] class RetrieverWithFallback: """ 通过三级回退链包装检索过程: 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)) return self.tools[top_idx], float(sims[top_idx]) def retrieve_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"我不确定哪个工具适合'{query}'。 " f"您能澄清您希望我做什么吗?" ), } 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']}")

"你能澄清一下你希望我做什么吗?"

"拉各斯的天气预报是什么?"

"xyzzy plugh random nonsense"

"查询:'{q}'"

"状态:{result['status']}"

"工具:{result['tool']}(置信度={result['confidence']:.3f})"

否则

"{result['clarification_request']}"

pip install scikit-learn numpy python fallback.py

fallback

升级路径是大多数团队在首次构建此类系统时最容易忽略的部分,但在生产环境中却是最关键的部分。一个自信但错误的工具调用,比一个系统询问"我不确定,你能澄清一下吗?"的错误更糟糕。第二种故障模式可以通过一次交互恢复,而第一种通常无法恢复。

评估你的工具选择系统

在你进行测量之前,以上所有内容都只是假设。方法很简单:构建一组带有标签的(查询,正确工具)对,运行你的流水线进行测试,并测量准确率、令牌成本和延迟,将过滤后的流水线与原始完整目录基线进行对比。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": "搜索网络以获取任何主题的最新信息"}, {"name": "read_file", "description": "根据文件路径读取文件内容"}, {"name": "write_file", "description": "将内容写入指定路径的文件"}, {"name": "send_email", "description": "向收件人发送带有主题和正文的电子邮件"}, {"name": "create_calendar_event", "description": "创建带有标题和时间的新日历事件"}, {"name": "query_database", "description": "对公司数据库运行SQL查询"}, {"name": "list_github_issues", "description": "列出GitHub仓库中的开放问题和错误"}, {"name": "send_slack_message", "description": "向Slack频道或用户发送消息"}, {"name": "get_weather", "description": "获取城市当前天气状况"}, {"name": "book_flight", "description": "搜索并预订两个城市之间的航班"}, ] # 标注的基准测试集:(查询, 预期工具)。当您有生产流量后,可以从实际 # 记录的查询中构建自己的测试集——这是一个种子集。 BENCHMARK_SET = [ ("阿布贾现在的天气如何?", "get_weather"), ("给财务团队发送电子邮件", "send_email"), ("列出我们主仓库的开放问题", "list_github_issues"), ("预订从拉各斯到伦敦的航班", "book_flight"), ("查询上周的注册信息", "query_database"), ("在团队Slack频道发布更新", "send_slack_message"), ("搜索最新利率信息", "search_web"), ("读取config.yaml文件内容", "read_file"), ] def estimate_tokens(text: str) -> int: """粗略的令牌估计(1个令牌约4个字符)——对于相对比较已足够。""" return len(text) // 4 class BenchmarkHarness: """通过检索器运行标注查询集并报告准确率、令牌成本和延迟。""" 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] def run(self, benchmark_set: list[tuple], use_retrieval: bool = True) -> dict: correct, total_tokens, latencies = 0, 0, [] 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] * 1000, 3), "p95_latency_ms": round(latencies_sorted[int(len(latencies_sorted) * 0.95)] * 1000, 3), } 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("基线(每次使用完整目录):") print(f" 准确率: {baseline['accuracy']*100:.1f}%") print(f" 平均令牌数/查询: {baseline['avg_tokens']}") print("\n检索过滤(前3名):") print(f" 准确率: {retrieval['accuracy']*100:.1f}%") print(f" 平均令牌数/查询: {retrieval['avg_tokens']}") reduction = (1 - retrieval["avg_tokens"] / baseline["avg_tokens"]) * 100 print(f"\n使用检索的令牌减少量: {reduction:.1f}%")

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

benchmark.py

运行: python benchmark.py

时间

"创建一个带有标题和时间的新日历事件"

带标签的基准测试集: (查询, 预期工具). 一旦有生产流量后,可以从真实的日志查询中构建自己的测试集 -- 这是一个种子集。

BENCHMARK_SET

"阿布贾现在的天气如何?"

"给财务团队发送一封邮件"

"列出主仓库中的开放问题"

"为我预订从拉各斯到伦敦的航班"

"查询上周的注册用户"

"在团队Slack频道发布更新"

"搜索最新的利率信息"

"读取config.yaml的内容"

estimate_tokens

"粗略的token估计值(1 token ≈ 4个字符)-- 对于相对比较已经足够。"

// 4

BenchmarkHarness

"通过检索器运行带标签的查询集,并报告准确率、token成本和延迟。"

full_catalog_tokens

sum

d

_retrieve

top_indices

argsort

::

i

run

tuple

use_retrieval

bool

correct

total_tokens

latencies

expected_tool

t0

perf_counter

candidates

tokens_this_query

基线:每次发送完整的未过滤目录

c

+=

append

n

latencies_sorted

sorted

"准确率"

round

/

"平均token数/查询"

"p50延迟ms"

// 2] * 1000, 3),

"p95延迟ms"

0.95

1000

harness

baseline

retrieval

"基线(每次发送完整目录):"

" 准确率: {baseline['accuracy']*100:.1f}%"

" 平均token数/查询: {baseline['avg_tokens']}"

"\n检索过滤(前3名):"

" 准确率: {retrieval['accuracy']*100:.1f}%"

" 平均token数/查询: {retrieval['avg_tokens']}"

reduction

"\n检索过滤后的token减少量: {reduction:.1f}%"

pip install scikit-learn numpy python benchmark.py

benchmark

在这个包含10个工具的目录和8个查询的基准测试中,检索过滤在保持准确率的同时,将每个查询的平均token数减少了约70%。具体数字会随着你的目录和查询集变化,但比较的结构才是关键:你现在有了一个可重复的方法来回答“这次更改是否真的有帮助”,而不是依赖少量手动检查。

总结

这六种技术不是相互竞争的选项,而是分层的。门控过滤在任何其他操作之前,以低成本排除完全不需要工具的请求。检索或路由将目录缩小到剩余请求实际相关的内容。规划多步骤任务的序列,使每一步只看到它需要的工具。后备逻辑处理首次尝试未能成功的情况。基准测试是判断上述任何方法是否产生可衡量差异的方式,而不仅仅是感觉更好。

RAG-MCP的结果,准确率提高超过三倍,token数减少一半,这不是特例。这是在停止要求模型在每次决策前阅读完整电话簿后可预测发生的结果。这些技术不需要更大的模型或更长的上下文窗口。它们需要将工具列表本身视为需要设计的内容,而不仅仅是追加的内容。

资源 :

  • RAG-MCP: 通过检索增强生成缓解LLM工具选择中的提示膨胀问题
  • MCPToolBench++: 一个大规模的MCP工具使用基准

更多相关内容

  • 掌握LLM工具调用:完整框架…
  • 掌握AI代理中工具调用的路线图
  • 在Python代理中实现权限门控的工具调用
  • 机器学习工程师的 Docker 完整指南
  • 使用 Pydantic 进行验证的完整指南…
  • 机器学习数据增强的完整指南

/.entry /think