动态工具发现与组合
本教程共 45 篇 · 第 43 篇 · 更新于 2026-08-16 · 约 9 分钟阅读
本节目标:理解工具搜索(Tool Search)模式解决的问题与原理,学完你能给 ChatClient 接上动态工具发现,让几十上百个工具按需加载。
43.1 工具太多也是问题
前几章的工具调用都是静态注册:启动时把工具定义全部发给模型。工具少时没问题,多了就出事了。
假设你的智能体接了 Slack、GitHub、Jira 和几个 MCP 服务器。工具轻松超过 50 个,每个定义都要占 token。对话还没开始,几万 token 就烧掉了。
更麻烦的是准确率。模型面对 30 多个名字相近的工具时,选错工具的概率明显上升。工具越多,选择越难。
问题的根源是”全量加载”。有没有办法让模型按需加载工具?这就是动态工具发现。
43.2 核心思路:给模型一个搜索工具
Anthropic 在《Advanced Tool Use》里提出了一种模式,Spring AI 实现了它,叫 Tool Search Tool。
思路很简单:不把所有工具定义发给模型,只发一个”搜索工具”的定义。模型需要能力时,调用搜索工具查一下,系统把命中的工具定义展开到上下文里。
整个过程模型感知不到区别。它只是先搜了一下,然后像平常一样调用工具。但每次请求只带少量工具定义,token 消耗大幅下降。
Spring AI 的实现基于第 6 章讲的 Recursive Advisor,扩展了 ToolCallingAdvisor。它对所有模型厂商通用:OpenAI、Anthropic、Gemini、Ollama 都能用。
Note官方基准测试:28 个工具的场景下,Gemini 节省 60% token,OpenAI 节省 34%,Anthropic 节省 64%。模型不同,收益不同,但都显著。
43.3 快速开始
官方提供 Spring Boot starter,包含 Lucene 索引和自动配置。加依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-tool-search-advisor</artifactId>
</dependency>
然后在 application.properties 里开一个开关:
spring.ai.chat.client.tool-search-advisor.enabled=true
就这两步。工具照常用 @Tool 注册,唯一区别是工具定义不会一开始就发给模型。
索引类型默认是 regex,不需要额外依赖。lucene 需要 lucene-core,vector 需要一个 VectorStore Bean。
# 可选:regex(默认)/ lucene / vector
spring.ai.chat.client.tool-search-advisor.tool-index-type=regex
Tip2.0 里 ToolSearchToolCallingAdvisor 有独立模块 spring-ai-tool-search-advisor,包名是 org.springframework.ai.chat.client.advisor.toolsearch。老版本的 spring-ai-tool-search-tool 模块已废弃。
43.4 工作原理
官方文档把完整流程拆成七步:
- 对话开始时,所有注册的工具被索引进 ToolIndex,但不发给模型。
- 首次请求只带搜索工具的定义。
- 模型需要某个能力,调用搜索工具,传查询词。
- ToolIndex 找出匹配的工具,把定义加入下一次请求。
- 模型同时看到搜索工具和展开的工具定义。
- 命中的工具被执行,结果返回。
- 模型基于结果生成最终回答。
关键在第 4 步:工具定义是”展开”进上下文的,不是一次性全给。模型用多少,就展开多少。
代码层面,你只需要一个 ToolIndex 和 Advisor。下面是一个完整的示例:
@SpringBootApplication
public class Application {
@Bean
CommandLineRunner demo(ChatClient.Builder builder, ToolIndex toolIndex) {
return args -> {
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.build();
ChatClient chatClient = builder
.defaultTools(new MyTools()) // 注册了但不发给模型
.defaultAdvisors(advisor) // 激活工具搜索
.build();
var answer = chatClient.prompt("""
帮我规划今天在阿姆斯特丹穿什么。
请推荐现在还在营业的服装店。
""").call().content();
System.out.println(answer);
};
}
static class MyTools {
@Tool(description = "获取指定地点、指定时间的天气")
public String weather(String location,
@ToolParam(description = "YYYY-MM-DDTHH:mm") String atTime) {
// 实现省略
return "";
}
@Tool(description = "获取指定地点、指定时间营业的服装店名称")
public List<String> clothing(String location,
@ToolParam(description = "YYYY-MM-DDTHH:mm") String openAtTime) {
// 实现省略
return List.of();
}
@Tool(description = "获取指定地点的当前日期和时间")
public String currentTime(String location) {
// 实现省略
return "";
}
// ... 可能还有几百个工具
}
}
注意 defaultTools 的注释:工具注册了,但初始请求不会携带它们的定义。
43.5 三种检索策略
ToolIndex 是接口,官方提供三种实现,对应三种检索策略:
| 策略 | 实现类 | 适合场景 |
|---|---|---|
| 语义检索 | VectorToolIndex | 自然语言查询、模糊匹配 |
| 关键词检索 | LuceneToolIndex | 精确术语匹配、工具名已知 |
| 正则匹配 | RegexToolIndex | 工具名模式匹配(如 get_*_data) |
语义检索需要 VectorStore,把工具描述向量化后按相似度匹配。关键词检索用 Lucene,适合工具名规范的情况。正则最轻量,零依赖。
选型建议:工具少且名字规范,用 regex 就够。工具多、名字乱、查询偏自然语言,上 lucene 或 vector。
43.6 手动配置
不想用 starter 也可以只加核心库,自己声明 Bean:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool-search-advisor</artifactId>
</dependency>
然后手动装配 Advisor 和索引。灵活的地方在于可以换 ToolIndex 实现,比如接入自己的向量库做语义检索。
43.7 官方性能数据
官方用 28 个工具做了基准测试,下面是节选的 token 消耗对比:
| 模型 | 启用工具搜索 | 未启用 | 节省 |
|---|---|---|---|
| Gemini | 2,165 tokens | 5,375 tokens | 60% |
| OpenAI | 4,706 tokens | 7,175 tokens | 34% |
| Anthropic | 6,273 tokens | 17,342 tokens | 64% |
数字是初步基准,但趋势明确:工具定义越大、越多,收益越大。Anthropic 节省最多,因为它的工具定义格式更占 token。
43.8 什么时候用
官方文档给了一张对比表,直接抄来:
| 用 Tool Search | 用传统方式 |
|---|---|
| 工具超过 20 个 | 工具库很小(少于 20 个) |
| 工具定义超过 5K token | 所有工具每个会话都会用到 |
| 多 MCP 服务器场景 | 工具定义非常紧凑 |
| 出现工具选择准确率问题 |
两个判断维度:工具数量和工具定义体积。都小,静态注册更快更简单;任何一个变大,动态发现更划算。
Tip如果所有工具每个会话都用一遍,动态发现的收益很小。它优化的是”工具很多但每次只用少数”的场景。
43.9 与 MCP 组合
第 27-31 章讲过 MCP:多个服务器各自暴露工具。这正是动态发现的典型场景。
一个 MCP 客户端可能挂 5 个服务器,每个服务器 10 个工具,总共 50+。静态注册要把 50 个定义全发给模型,动态发现只发一个搜索工具。
两者定位不同:MCP 解决”工具从哪来”,动态发现解决”工具怎么按需给模型”。组合使用,才能支撑上百个工具的智能体。
43.10 小结
动态工具发现的核心是一个思想:让模型自己搜工具。Token 节省 34%-64%,工具选择准确率也更高。
上手三步:加 starter 依赖、开 enabled 开关、照常注册 @Tool。检索策略按工具规模选,regex 起步,lucene 或 vector 进阶。
下一章是升级指南。2.0 改动很大,老项目迁移要过一遍清单。