Dynamic Tool Discovery(动态工具发现)
约 1741 字大约 6 分钟
2026-09-15
动态工具发现(Dynamic Tool Discovery)
📌 原文地址:https://docs.spring.io/java-ai/reference/guides/dynamic-tool-search.html 📌 原文版本:Java AI 2.0.1
当你的 MCP Server 暴露了几十个甚至几百个工具时,每次对话把全部工具描述塞进 prompt(提示词)会让 token(模型按字数计费的单位)消耗暴增——既费钱又拖慢响应。动态工具发现(Dynamic Tool Discovery) 就是为了解决这个问题:让 AI 在需要时再"按需检索"工具,而不是一次性把所有工具都喂给它。
白话解读
想象你去一个超大仓库:传统做法是把整本目录都塞进 AI 的脑子里;动态工具发现则相当于给 AI 配了个"仓库管理员",你说要找啥,管理员再告诉你去哪个货架。
一、为什么需要动态发现
直接把所有工具塞给 AI 会有三大问题:
| 问题 | 影响 | 量化(举例) |
|---|---|---|
| Token 消耗大 | 每次请求都带全量工具描述 | 100 个工具 × 每条 200 token = 2 万 token |
| 响应慢 | prompt 越长,模型首 token 延迟越高 | 输入越长,首字输出越慢 |
| 工具选择准确率下降 | 模型被相似工具"迷惑" | 100 个工具里挑 1 个,准确率明显低于 10 个里挑 1 个 |
性能瓶颈
Anthropic、OpenAI 等模型供应商对单次请求的"工具列表长度"都有软上限。工具太多会导致请求失败或被截断。
二、核心思路:用一个"工具搜索工具"
动态发现的核心套路是:
- 保留少量"入口工具":把每个工具的"索引信息"(名字、简短描述、分类标签)暴露成一个轻量工具,名字叫
tool_search_tool(类似 GitHub Copilot 的工具名) - AI 先调用搜索工具:AI 想干活时,先问
tool_search_tool:"有没有处理 PDF 的工具?" - 搜索工具返回完整工具列表:返回 3-5 个最相关的工具的完整 schema
- AI 再调用真正的工具:拿到完整工具描述后,正常调用
关键洞察
工具的"索引信息"通常比"完整 schema"短 5-10 倍,所以搜索这一步非常省 token。
三、Java AI 的实现:Tool Search Tool
Java AI 提供了一个开箱即用的实现,名为 ToolSearchToolCallingAdvisor。它的工作原理:
用户提问 → ChatClient → ToolSearchToolCallingAdvisor
│
▼
AI 是否调用 tool_search_tool?
├─ 否:正常工具调用
└─ 是:从索引里检索相关工具,返回完整 schema
│
▼
AI 继续调用真实工具3.1 依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>3.2 三种索引策略
Java AI 内置了三种索引实现,可按工具数量和查询方式选用:
| 索引类型 | 实现类 | 适用场景 | 查询方式 |
|---|---|---|---|
| 向量索引 | VectorToolIndex | 工具非常多(100+),描述较复杂 | 语义相似度 |
| Lucene 全文索引 | LuceneToolIndex | 工具名/描述里有大量关键词 | 关键字全文搜索 |
| 正则索引 | RegexToolIndex | 工具名很规范,能用正则匹配 | 正则表达式 |
选择建议
- 工具 < 20 个:直接全量加载,不需要动态发现
- 工具 20-100 个:用 Lucene 全文索引(最快)
- 工具 100+:用向量索引(最智能,但启动稍慢)
- 工具名特别规范(如
db.query.*、weather.*):用正则索引
3.3 完整示例:基于向量索引
第一步:把 MCP 工具注册成索引
@Configuration
public class ToolIndexConfig {
/**
* 从 MCP Server 获取所有工具,构建向量索引。
*/
@Bean
public VectorToolIndex toolIndex(VectorStore vectorStore,
EmbeddingModel embeddingModel,
List<McpSyncClient> mcpClients) {
// 把每个 MCP 工具的"名称 + 描述"转成 Document
List<Document> toolDocs = mcpClients.stream()
.flatMap(client -> client.listTools().tools().stream())
.map(tool -> new Document(
tool.name(),
tool.description(),
Map.of("tool_name", tool.name(),
"tool_schema", objectMapper.writeValueAsString(tool))
))
.toList();
// 写入向量库(一次性)
vectorStore.add(toolDocs);
// 用向量库 + EmbeddingModel 构建索引
return new VectorToolIndex(vectorStore, embeddingModel);
}
/**
* 注册 ToolSearchToolCallingAdvisor。
*/
@Bean
public ToolSearchToolCallingAdvisor toolSearchAdvisor(ToolIndex toolIndex) {
return new ToolSearchToolCallingAdvisor(toolIndex);
}
}第二步:把 Advisor 接入 ChatClient
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder,
ToolSearchToolCallingAdvisor searchAdvisor) {
this.chatClient = builder
.defaultAdvisors(searchAdvisor) // 关键:把搜索 Advisor 接入
.build();
}
@GetMapping("/chat")
public String chat(@RequestParam String question) {
return chatClient.prompt(question)
.call()
.content();
}
}启动后,AI 会自动按需调用 tool_search_tool,而不是每次都看到所有工具。
四、性能基准
Java AI 团队在多种主流模型上做了测试,结果令人惊喜:
| 模型 | 工具总数 | 节省 token(占比) | 备注 |
|---|---|---|---|
| Gemini 2.5 | 200 | 60% | 工具描述最长,节省最明显 |
| OpenAI GPT-4o | 200 | 34% | 工具描述较短,但仍有可观节省 |
| Anthropic Claude 4 Sonnet | 200 | 64% | 节省最显著 |
关键数据
测试场景下,平均 token 节省 34%-64%,相当于成本直接砍掉三分之一到三分之二。
五、最佳实践
5.1 工具描述的写法
工具描述直接决定搜索的准确率,建议:
- 用动词开头:「查询天气」「创建订单」「搜索文档」
- 包含同义词:「天气 / 气温 / 温度」「文档 / 文件 / 文章」
- 避免太长的句子:3-5 个短语最佳
// 好的写法
@McpTool(name = "searchDocuments",
description = "搜索文档 / 文件 / 文章。支持关键词、作者、日期范围。")
// 不好的写法(搜索难命中)
@McpTool(name = "searchDocuments",
description = "这是一个用于在文档系统中根据用户输入查找相关文档的功能性接口")5.2 索引更新策略
如果你的工具列表会动态变化(比如运行时注册新工具),需要:
@Scheduled(fixedDelay = 60_000) // 每分钟刷新一次
public void refreshIndex() {
toolIndex.refresh(); // 重新拉取 MCP 工具列表
}性能权衡
频繁刷新索引会消耗额外资源。建议根据工具变化频率动态调整:稳定的工具列表每 10 分钟刷新一次,频繁变化的工具用事件驱动即时刷新。
5.3 与传统工具调用混合
动态发现不必"二选一"。你可以:
- 把 高频工具 直接暴露给 AI(O(1) 调用,省去搜索步骤)
- 把 低频工具 放进索引(按需发现)
@Bean
public ChatClient hybridChat(ChatClient.Builder builder,
ToolSearchToolCallingAdvisor searchAdvisor,
// 高频工具直接注入
WeatherTools weatherTools,
OrderTools orderTools) {
return builder
.defaultAdvisors(searchAdvisor)
.defaultTools(weatherTools, orderTools) // 这些不需要搜索
.build();
}六、对比:什么时候"不要"用动态发现
反向建议
工具数量小于 10 个时,动态发现反而增加复杂度。建议工具 < 10 个时直接全量加载。
适合直接全量加载的场景:
- 工具数量 < 10
- 工具描述都 < 50 token
- 工具调用非常频繁,每次对话都会用到
七、小结
一句话总结
动态工具发现 = 用"工具搜索工具"代替"全量工具列表",省 token、省时间、提高工具选择准确率。Java AI 的 ToolSearchToolCallingAdvisor 让你几行代码就能用上。
下一步,如果你想进一步提升 AI 输出质量,可以阅读《LLM as a Judge》了解如何用"AI 评审 AI"的递归优化模式。