工具调用
约 7449 字大约 25 分钟
2026-09-15
合集说明
本篇合并自参考文档「Tool Calling」章节(工具调用 / 工具搜索工具 / ToolCallingAdvisor / ChatModel 工具调用),按官方文档顺序排列:(各篇之间以 --- 分隔)
- Tool Calling(工具调用)
- Tool Search Tool(工具搜索工具)
- ToolCallingAdvisor(工具调用顾问)
- ChatModel Tool Calling(直接用 ChatModel 调工具)
Tool Calling(工具调用)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/tools.html 📌 原文版本:Java AI 2.0.1
工具调用(Tool Calling)就是让模型能"指挥"你的代码——模型决定调用哪个函数、传什么参数,你的程序负责真的去执行那个函数,再把结果喂回给模型。
白话解读
LLM 自己不能查天气、不能发邮件、不能写数据库。但 LLM 可以说"我建议调用 getWeather("北京")",然后你的代码去查天气 API、把结果告诉 LLM,LLM 再基于真实数据生成回答。这就是工具调用。
注意一个关键安全原则:模型从来不会直接访问工具背后的 API。它只能"建议",真正动手的是你的代码。
工具的两个用途:
- 信息检索:把模型的知识拉到现在(查天气、查订单、搜新闻)。
- 采取行动:在你的系统里动手(发邮件、订机票、写数据库)。
1. 架构概览(2.0 新设计)
Java AI 2.0 把工具调用循环作为 ChatClient advisor 链中的一等公民:
- 定义工具(
@Tool/MethodToolCallback/FunctionToolCallback),传给ChatClient。 ChatClient自动注册ToolCallingAdvisor,驱动循环。- 模型决定调用哪些工具 →
ToolCallingManager执行 → 结果塞回历史 → 循环直到模型产出不带工具调用的响应。 - 其它 advisor(内存、可观测性、重试…)通过"advisor 排序"与循环组合。
关于 1.x
Java AI 1.x 是每个 ChatModel 自己跑工具循环;2.0 统一抽到 advisor 层。直接用 ChatModel 仍然支持低层场景(见 04_ChatModel-Tool-Calling.md),但默认不再自动执行工具。
2. 快速开始
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now()
.atZone(LocaleContextHolder.getTimeZone().toZoneId())
.toString();
}
@Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}String response = ChatClient.create(chatModel)
.prompt("Can you set an alarm 10 minutes from now?")
.tools(new DateTimeTools())
.call()
.content();模型会先调 getCurrentDateTime() 拿到时间,再调 setAlarm("…")。Java AI 自动搞定全程。
3. 定义工具的三种风格
| 风格 | 何时用 | 章节 |
|---|---|---|
@Tool 注解 | 你有现成的方法,想要最少仪式感 | 3.1 |
MethodToolCallback | 需要编程式控制方法工具 | 3.2 |
FunctionToolCallback | 想暴露 Function/lambda/方法引用 | 3.3 |
三种最终都产生 ToolCallback,流过同一个循环。可以混用。
3.1 声明式:@Tool
class WeatherTools {
@Tool(description = "Get the current weather for a given city")
public String getWeather(String city) {
return weatherService.fetch(city);
}
}@Tool 注解字段:
name:工具名,默认方法名,在同批工具中必须唯一。description:强烈建议填——模型靠这个决定何时调。returnDirect:结果直接返回给调用方,不走模型(见"直接返回")。resultConverter:自定义结果序列化器。
参数上加 @ToolParam:
@Tool(description = "Get the weather for a city at a specific time")
public String getWeather(
@ToolParam(description = "City name") String city,
@ToolParam(description = "Time in ISO-8601 format", required = false) String at) {
return weatherService.fetch(city, at);
}AOT/GraalVM 原生镜像
含 @Tool 方法的类必须是 Spring bean(@Component),否则要加 @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)。
3.2 编程式:MethodToolCallback
适用场景:你拿不到源类,或者要在运行时动态构造工具。
Method method = ReflectionUtils.findMethod(WeatherTools.class, "getWeather", String.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinitions.builder(method)
.description("Get the current weather for a given city")
.build())
.toolMethod(method)
.toolObject(new WeatherTools()) // 实例方法必须传;静态方法可选
.build();3.3 编程式:FunctionToolCallback
适用场景:lambda、方法引用、Function/Supplier/Consumer/BiFunction。
FunctionToolCallback callback = FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();4. ToolCallback Bean(应用上下文中的工具)
把工具声明成 @Bean:
@Configuration(proxyBeanMethods = false)
class WeatherToolsConfig {
@Bean
ToolCallback currentWeather(WeatherService weatherService) {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
}注入使用:
@Autowired ToolCallback currentWeather;
ChatClient.create(chatModel)
.prompt("What's the weather in Copenhagen?")
.tools(currentWeather)
.call()
.content();2.0 改动
旧的 SpringBeanToolCallbackResolver + toolNames(...) 模式(按名称解析裸 Function bean)已移除。工具必须显式注册为 ToolCallback bean。
5. 向 ChatClient 传递工具
// 每次调用——仅本请求可用
chatClient.prompt(...).tools(new WeatherTools(), currentWeather).call();
// 默认值——从此客户端的所有请求都可用
ChatClient client = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools(), currentWeather)
.build();- 都接受 POJO 实例、
ToolCallback、ToolCallbackProvider、它们的数组/集合。 .tools(...)追加到.defaultTools(...),不替换——最终工具列表是两者的并集。- 破坏性工具慎用 defaultTools,按调用加更安全。
ChatModel 上的工具优先级
通过 ChatModel 上的 ToolCallingChatOptions.toolCallbacks(...) 注册的工具会被请求级工具列表替换(不是追加)。详见 04_ChatModel-Tool-Calling.md。
6. 工具调用循环
执行流程(DefaultChatClient 自动注册的 ToolCallingAdvisor 驱动):
- 把工具定义发给模型。
- 模型决定调谁,返回响应。
- 若响应含工具调用:
ToolCallingManager找匹配ToolCallback并执行。- 把工具结果追加到对话历史。
- 把更新后的历史再发给模型。
- 步骤 2–3 循环,直到模型产出不含工具调用的响应。
阻塞(.call())和流式(.stream())都支持。ToolCallingAdvisor 是递归 advisor——每轮迭代重新进入下游 advisor 链。同一机制驱动"结构化输出验证重试"等递归 advisor 模式。
单一 ToolAdvisor 约束
DefaultChatClient 强制 advisor 链中恰好有一个 ToolAdvisor。注册第二个会显式失败。
7. 内存与工具循环
MessageChatMemoryAdvisor 相对于 ToolCallingAdvisor(默认 HIGHEST_PRECEDENCE + 300)的位置,决定内存捕获多少上下文。
7.1 循环之外(默认)
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) // 默认顺序 200,在循环外
.build();内存 advisor:
- 循环开始前加载一次历史。
- 循环结束后只持久化最终用户/助手消息。
- 看不到工具调用请求和工具响应消息。
这是所有 ChatMemoryRepository 实现都安全的默认值。
7.2 循环之内
想让模型在后续轮次里看到完整的工具调用记录:
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.order(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // 高于 300,位于循环内
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor)
.build();为避免重复写入,循环内有 MemoryAdvisor 时必须禁用 ToolCallingAdvisor 的内部对话历史。自动注册时 DefaultChatClient 自动处理;手动构造时自己调 .disableInternalConversationHistory()。
仓库兼容性
只有以下内置仓库能持久化完整的工具消息集:
InMemoryChatMemoryRepositoryRedisChatMemoryRepositoryNeo4jChatMemoryRepository
JDBC、MongoDB、Cassandra 会把工具消息静默过滤掉(详见 Chat-Memory.md)。需要 JDBC 完整持久化 + 事件溯源 + 回合压缩 + 多智能体分支隔离的,可以看社区项目 spring-ai-session(计划合并到 2.1)。
8. 扩展到数百个工具
工具定义全塞给模型 → 上下文爆炸 + 选错工具 + token 浪费。ToolSearchToolCallingAdvisor 是直接替换品,实现渐进式工具披露:
- 会话开始时把所有工具索引到
ToolIndex。 - 首轮只发一个内置的
toolSearchTool给模型。 - 模型用自然语言查询 → 命中工具的定义被注入 → 模型再调真实工具。
基准测试在 OpenAI / Anthropic / Gemini 上节省 34–64% token。
启用方式:
spring.ai.chat.client.tool-search-advisor.enabled=true详见 02_Tool-Search-Tool.md。
9. MCP 工具
9.1 消费 MCP 服务器工具
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>spring.ai.mcp.client.stdio.connections.my-server.command=npx
spring.ai.mcp.client.stdio.connections.my-server.args=-y,@modelcontextprotocol/server-everything自动配置连接所有 MCP 服务器,把工具暴露为 SyncMcpToolCallbackProvider bean。故意不自动注册到 ChatClient(避免启动时网络往返):
@Autowired SyncMcpToolCallbackProvider mcpTools;
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(mcpTools) // 或每次调用 .tools(mcpTools)
.build();9.2 将 Spring 工具暴露为 MCP 服务器
把 @Tool 换成 @McpTool:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>@Component
public class WeatherTools {
@McpTool(description = "Get the current weather for a given city")
public String getWeather(@McpToolParam(description = "City name") String city) {
return weatherService.fetch(city);
}
}9.3 混用本地和 MCP
chatClient.prompt().tools(new LocalTools(), mcpTools).call().content();注意事项:
- 名字冲突只在 MCP 侧去重。
DefaultMcpToolNamePrefixGenerator给 MCP 重复项加前缀,但不知道本地@Tool。冲突时自己重命名一个,或用McpToolFilter丢掉。 - 用
McpToolFilter限制暴露范围——MCP 工具来自你无法完全控制的远程源。
10. 工具参数增强(Tool Argument Augmentation)
在不修改工具实现的前提下,给模型加额外参数(推理过程、置信度、元数据…):
public record AgentThinking(
@ToolParam(description = "Your reasoning for calling this tool", required = true)
String innerThought,
@ToolParam(description = "Confidence level (low, medium, high)", required = false)
String confidence
) {}
AugmentedToolCallbackProvider<AgentThinking> provider = AugmentedToolCallbackProvider
.<AgentThinking>builder()
.toolObject(new WeatherTools())
.argumentType(AgentThinking.class)
.argumentConsumer(event -> {
AgentThinking t = event.arguments();
log.info("Tool: {} | Reasoning: {}", event.toolDefinition().name(), t.innerThought());
})
.removeExtraArgumentsAfterProcessing(true)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(provider)
.build();LLM 看到增强 schema,你的消费者拿到 record;原始工具只接收它原本的参数。
11. 用户控制的工具执行
需要每次迭代都"插手"时(审批门、SSE 推送、条件分支…),按调用关闭自动注册:
ChatClient chatClient = ...;
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
String question = "What is the weather in Amsterdam and Paris?";
ChatClientResponse response = chatClient.prompt()
.user(question)
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = new Prompt(result.conversationHistory(), chatOptions);
response = chatClient.prompt()
.messages(result.conversationHistory())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
}全局关闭:
spring.ai.chat.client.tool-calling.enabled=false推荐做法
对于"中间进度推到 UI"这种常见需求,在循环内放一个自定义 advisor 比完全关闭自动注册更简单、更可组合(也保留可观测性、advisor 链)。
12. 扩展循环:自定义 ToolAdvisor
ToolAdvisor 是标记接口——DefaultChatClient 用它识别和约束 advisor 链。
继承 ToolCallingAdvisor 覆盖钩子方法:
| 钩子 | 触发时机 |
|---|---|
doInitializeLoop / doInitializeLoopStream | 第一次迭代前,一次 |
doBeforeCall / doBeforeStream | 每次迭代前 |
doAfterCall / doAfterStream | 每次迭代后 |
doFinalizeLoop / doFinalizeLoopStream | 循环结束后,一次 |
要替换默认 advisor,注册 ToolCallingAdvisor.Builder<?> bean:
@AutoConfiguration(beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
@ConditionalOnProperty(prefix = "my.advisor", name = "enabled", havingValue = "true")
public class MyToolAdvisorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(ToolCallingManager tcm) {
return MyCustomToolCallingAdvisor.builder().toolCallingManager(tcm);
}
}ChatClient.Builder 会透明地用你的 builder 注册 advisor。ToolSearchToolCallingAdvisor 的自动配置正是这条机制。
13. 工具规格参考
13.1 ToolCallback 接口
public interface ToolCallback {
ToolDefinition getToolDefinition(); // 模型看到的契约
ToolMetadata getToolMetadata(); // returnDirect 等
String call(String toolInput); // 用 JSON 输入执行
String call(String toolInput, ToolContext ctx);
}MethodToolCallback / FunctionToolCallback 是内置实现。需要完全控制时直接实现 ToolCallback(MCP 集成就是这么干的)。
13.2 ToolDefinition
public interface ToolDefinition {
String name();
String description();
String inputSchema(); // JSON Schema 字符串
}13.3 JSON Schema 生成
JsonSchemaGenerator 从方法/函数参数列表生成 schema。
参数描述(任选其一,按优先级):
@ToolParam(description = "...")@JsonClassDescription/@JsonPropertyDescription@Schema(description = "...")
必填/可选(按优先级):
@ToolParam(required = false)@JsonProperty(required = false)@Schema(required = false)@Nullable
必填很重要
标记为必填的参数模型必须提供;如果模型没法确定它会编造一个。给可空参数加 required = false 或 @Nullable。
13.4 工具上下文(Tool Context)
ToolContext 让你把"非模型数据"(租户 ID、用户 ID)传给工具,不会发给模型。
class CustomerTools {
@Tool(description = "Retrieve customer information")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
return customerRepository.findById(id, toolContext.getContext().get("tenantId"));
}
}
ChatClient.create(chatModel)
.prompt("Tell me more about the customer with ID 42")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme"))
.call()
.content();运行时 toolContext 和默认 toolContext 都会合并,运行时优先。
13.5 直接返回(Return Direct)
工具的输出已经是最终答案时,跳过模型回灌:
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) { ... }编程式通过 ToolMetadata.builder().returnDirect(true) 设置。
多工具调用
单轮多工具调用时,所有工具都 returnDirect=true 才会真正直返。否则结果发回模型继续循环。
13.6 结果转换(Result Conversion)
ToolCallResultConverter 把工具返回值转成 String:
@FunctionalInterface
public interface ToolCallResultConverter {
String convert(Object result, Type returnType);
}默认是 Jackson 序列化。可在 @Tool(resultConverter = ...) 或 ToolMetadata 里换。
13.7 方法工具限制
以下类型不能做参数或返回值:Optional(改用 @Nullable/required=false)、异步类型(CompletableFuture/Future)、响应式类型(Flux/Mono/Flow)、函数式类型(Function/Supplier/Consumer)。
13.8 异常处理
工具抛异常时被包装为 ToolExecutionException,交给 ToolExecutionExceptionProcessor:
- 默认
DefaultToolExecutionExceptionProcessor:RuntimeException的消息发回模型,受检异常/Error重新抛出。 - 属性
spring.ai.tools.throw-exception-on-error=true:所有错误抛给调用方。
自己实现 ToolCallback 时,从 call() 抛 ToolExecutionException 即可。
13.9 工具调用限制
防失控循环:
ToolCallingManager toolCallingManager = ToolCallingManager.builder()
.maxCallsPerTool(40) // 每工具默认
.maxCallsPerTool("search", 10) // 单工具覆盖
.excludeToolFromLimit("getCurrentWeather") // 排除
.maxTotalToolCalls(150) // 总上限
.onLimitExceeded(ToolCallLimitBehavior.THROW) // 或 RETURN_ERROR_RESPONSE
.build();计数只算当前回合(最后 UserMessage 之后的 ToolResponseMessage)——回放历史不会膨胀计数。
属性:
| 属性 | 说明 | 默认 |
|---|---|---|
spring.ai.tools.limits.max-calls-per-tool-default | 每回合每工具最大调用数,-1 禁用 | 40 |
spring.ai.tools.limits.max-calls-per-tool.<name> | 单工具覆盖 | - |
spring.ai.tools.limits.excluded-tools | 豁免名单 | - |
spring.ai.tools.limits.max-total-tool-calls | 总上限,-1 禁用 | 150 |
spring.ai.tools.limits.on-limit-exceeded | THROW / RETURN_ERROR_RESPONSE | THROW |
13.10 工具解析(Tool Resolution)
public interface ToolCallbackResolver {
@Nullable ToolCallback resolve(String toolName);
}默认 StaticToolCallbackResolver 用应用上下文里的 ToolCallback bean + ToolCallbackProvider 产物(MCP provider 故意排除,避免启动时网络往返)。
启用解析回退:
spring.ai.tools.resolution.fallback.enabled=true解析回退的风险
开启后,解析器能看到的每个工具都能在模型命名时被执行,包括风险工具——无论请求附加了什么。只有在你完全掌控解析器暴露范围时才开启。
13.11 可观测性
工具调用在 spring.ai.tool 名下发 Micrometer observation,包含工具名、元数据、执行时长、追踪上下文。
参数和结果默认不导出到 span(敏感性)。详见 "Tool Calling Observability"。
13.12 日志
logging.level.org.springframework.ai=DEBUG14. ChatModel 工具调用(低层路径)
直接用 ChatModel 不走 ChatClient 也不走 ToolCallingAdvisor——返回的响应里工具调用不会被自动执行。
详见 04_ChatModel-Tool-Calling.md。
15. 相关子页面
02_Tool-Search-Tool.md——ToolSearchToolCallingAdvisor03_ToolCallingAdvisor.md—— 默认 advisor 的 builder/钩子04_ChatModel-Tool-Calling.md—— 直接用ChatModel的低层路径- MCP 章节 —— MCP 客户端/服务器集成
Chat-Memory.md—— 与内存 advisor 的交互
Tool Search Tool(工具搜索工具)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/tools/tool-search-tool.html 📌 原文版本:Java AI 2.0.1
当工具库膨胀到 30+ 个(多 MCP 服务器聚合甚至 50+),把所有工具定义全塞给模型会:
- 上下文爆炸(一个多服务器会话轻松吃掉 55K+ token)
- 工具选择准确率下降(名字相似,模型挑花眼)
ToolSearchToolCallingAdvisor 是默认 ToolCallingAdvisor 的"直接替换品",实现渐进式工具披露模式——工具定义按需增量暴露给模型。
白话解读
类比于"懒加载":工具库是数据库,模型问"我需要个查天气的",advisor 先用嵌入搜索定位到那个工具,把它的定义注入提示,模型再用真实定义去调。
基准测试(OpenAI / Anthropic / Gemini)显示 token 消耗减少 34–64%。
1. 工作原理
ToolSearchToolCallingAdvisor 继承 ToolCallingAdvisor,覆盖初始化和迭代钩子:
- 索引:会话开始时,把所有已注册工具索引到
ToolIndex,不发任何工具定义给模型。 - 初始请求:仅发送内置的
toolSearchTool定义。 - 发现调用:模型需要某能力时,用自然语言查询
toolSearchTool。 - 搜索与展开:
ToolIndex找匹配工具;这些工具的定义被追加到对话。 - 工具调用:模型发常规工具调用。
- 执行:
ToolCallingManager执行并返回结果。 - 响应:模型基于真实结果生成最终答案。
索引按会话划分作用域——并发对话各自隔离。
2. 何时使用
✅ 适合:
ChatClient注册 10+ 工具- 工具定义单次请求消耗 > 10K token
- 多 MCP 服务器聚合
- 大工具集下选错工具变多
❌ 坚持默认 advisor:
- 工具 < 10 个
- 每个会话几乎用全工具
- 工具定义很紧凑(搜索的往返开销 > 节省的 token)
3. 安装
Starter 方式(含 Lucene + 自动配置):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-tool-search-advisor</artifactId>
</dependency>implementation 'org.springframework.ai:spring-ai-starter-tool-search-advisor'手动装配:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool-search-tool</artifactId>
</dependency>4. 快速开始
4.1 自动配置(推荐)
spring.ai.chat.client.tool-search-advisor.enabled=trueChatClient 无需任何代码改动,自动用 ToolSearchToolCallingAdvisor 替换默认 ToolCallingAdvisor。
4.2 手动装配
// 1. 选一个 ToolIndex(语义 / 关键词 / 正则)
@Bean
ToolIndex toolIndex(VectorStore vectorStore) {
return new VectorToolIndex(vectorStore);
}
// 2. 构建 advisor
var toolSearchAdvisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.maxResults(5)
.build();
// 3. 注册到 ChatClient——工具被索引但不预先发给 LLM
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new MyTools())
.defaultAdvisors(toolSearchAdvisor)
.build();
// 4. 调用时必须传会话 ID
String answer = chatClient.prompt("Help me plan what to wear today in Amsterdam")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42-session"))
.call()
.content();5. 会话作用域
默认从 advisor 上下文里以 ChatMemory.CONVERSATION_ID 为键读会话 ID。已有 MessageChatMemoryAdvisor 时,相同键已经存在——白嫖。
自定义键名:
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.sessionIdKeyName("tenantId")
.build();6. 搜索策略
ToolIndex 接口抽象搜索实现。内置三种:
| 策略 | 实现 | 适用 |
|---|---|---|
| 语义 | VectorToolIndex | 自然语言查询、模糊匹配 |
| 关键词 | LuceneToolIndex | 精确词匹配、已知词汇 |
| 正则 | RegexToolIndex(默认) | 工具名严格命名约定 |
6.1 VectorToolIndex(语义)
@Bean
ToolIndex vectorToolIndex(VectorStore vectorStore) {
return new VectorToolIndex(vectorStore);
}需要 VectorStore bean(如 spring-ai-starter-vector-store-pgvector)。工具名/描述被嵌入,查询被嵌入后取 top-K。
6.2 LuceneToolIndex(关键词)
@Bean
ToolIndex luceneToolIndex() {
return new LuceneToolIndex(); // 默认最小分数 0.25
// return new LuceneToolIndex(0.4f); // 自定义阈值
}无需嵌入模型。低于阈值的命中被静默丢弃。
6.3 RegexToolIndex(模式)
@Bean
ToolIndex regexToolIndex() {
return new RegexToolIndex();
}工具名匹配正则(如 get_*_data)。零额外依赖。未显式配置 tool-index-type 时的默认。
7. 配置选项
ToolSearchToolCallingAdvisor.Builder 继承 ToolCallingAdvisor.Builder,新增:
| 选项 | 说明 | 默认 |
|---|---|---|
toolIndex(ToolIndex) | 搜索实现 | 必填 |
maxResults(Integer) | 每次 toolSearchTool 返回的最大工具数;null 由 LLM 决定(内置默认 5) | null |
systemMessageSuffix(String) | 自定义提示后缀 | 内置模板 |
referenceToolNameAccumulation(boolean) | true = 累积所有搜索轮的工具名;false = 仅最近一轮 | true |
sessionIdKeyName(String) | advisor 上下文中的会话 ID 键 | ChatMemory.CONVERSATION_ID |
evictionStrategy(ToolIndexEvictionStrategy) | 索引淘汰策略 | LruEvictionStrategy(1000) |
8. ToolIndex API
public interface ToolIndex {
void indexTool(String sessionId, ToolReference toolReference);
default void indexTools(String sessionId, List<ToolReference> toolReferences);
ToolSearchResponse search(ToolSearchRequest request);
void clearIndex(String sessionId);
}每个操作按 sessionId 划作用域。
9. 索引淘汰
| 策略 | 行为 |
|---|---|
LruEvictionStrategy(maxSessions)(默认) | 超过 maxSessions 时淘汰最久未用 |
NeverEvictStrategy.INSTANCE | 永不自动淘汰;自己 evictSession() |
AlwaysEvictStrategy.INSTANCE | 每请求前清空,强制重建索引(测试用) |
TtlEvictionStrategy(duration) | 空闲超 TTL 淘汰 |
CompositeEvictionStrategy(strategies…) | 任一策略要求即淘汰 |
示例:
// TTL 30 分钟
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.evictionStrategy(new TtlEvictionStrategy(Duration.ofMinutes(30)))
.build();
// TTL + LRU 组合
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.evictionStrategy(new CompositeEvictionStrategy(
new TtlEvictionStrategy(Duration.ofMinutes(30)),
new LruEvictionStrategy(200)))
.build();淘汰在每个请求时惰性评估,无需后台线程。
10. Spring Boot 自动配置
启用:
spring.ai.chat.client.tool-search-advisor.enabled=true自动注册一个 ToolSearchToolCallingAdvisor.Builder bean——透明替换默认 ToolCallingAdvisor。如果应用已显式声明 ToolIndex bean,自动配置跳过。
ToolIndex 自动选择
spring.ai.chat.client.tool-search-advisor.tool-index-type=regex # 默认
spring.ai.chat.client.tool-search-advisor.tool-index-type=lucene
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector应用声明的自定义 ToolIndex bean 始终优先(@ConditionalOnMissingBean)。
完整属性参考
| 属性 | 说明 | 默认 |
|---|---|---|
spring.ai.chat.client.tool-search-advisor.enabled | 启用(替换默认 ToolCallingAdvisor) | false |
spring.ai.chat.client.tool-search-advisor.tool-index-type | regex / lucene / vector | regex |
spring.ai.chat.client.tool-search-advisor.max-results | 每次返回最大数;null 用内置 | null |
spring.ai.chat.client.tool-search-advisor.system-message-suffix | 自定义后缀;null 用内置模板 | null |
spring.ai.chat.client.tool-search-advisor.reference-tool-name-accumulation | 累积 vs 仅最近一轮 | true |
spring.ai.chat.client.tool-search-advisor.session-id-key-name | 会话 ID 键名 | chat_memory_conversation_id |
spring.ai.chat.client.tool-search-advisor.advisor-order | 链顺序 | HIGHEST_PRECEDENCE + 300 |
spring.ai.chat.client.tool-search-advisor.eviction.lru-max-sessions | LRU 上限 | 1000 |
spring.ai.chat.client.tool-search-advisor.eviction.ttl | 空闲 TTL;设了用 LRU+TTL 组合 | null |
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold | Lucene 最低分 | 0.25 |
示例配置
Lucene + TTL:
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=lucene
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold=0.4
spring.ai.chat.client.tool-search-advisor.eviction.ttl=30m向量搜索:
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector多租户自定义键:
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector
spring.ai.chat.client.tool-search-advisor.session-id-key-name=tenantId11. 相关页面
01_Tool-Calling.md—— Scaling to Hundreds of Tools(概念)03_ToolCallingAdvisor.md—— 基类 builder 选项- Dynamic Tool Discovery 指南 —— 完整示例
ToolCallingAdvisor(工具调用顾问)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/tools/tool-calling-advisor.html 📌 原文版本:Java AI 2.0.1
ToolCallingAdvisor 是 Java AI 2.0 中负责工具执行生命周期的递归 advisor,由 DefaultChatClient 自动注册。它驱动循环直到模型产出不含工具调用的响应。
白话解读
这是 Java AI 工具调用的"发动机"。你平时用不到它——ChatClient 默认自动配置好了。但你要自定义、替换、加钩子就得懂它。
本页是 builder API、配置选项、钩子方法和扩展模式的参考文档。
1. 概览
ToolCallingAdvisor 同时实现 CallAdvisor、StreamAdvisor 和 ToolAdvisor 标记接口。ToolAdvisor 这个标记接口是 DefaultChatClient 强制要求链中恰好一个工具 advisor 的依据——见后文"Single-ToolAdvisor Invariant"。
默认顺序 ToolCallingAdvisor.DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 300。默认的 MessageChatMemoryAdvisor 是 +200,所以内存 advisor 默认在工具循环之外。
2. Builder
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(toolCallingAdvisor)
.build();大多数应用不需要手动构造——DefaultChatClient 自动注册。只有需要非默认设置或自定义子类时才手动构造。
3. Builder 选项
| 选项 | 说明 | 默认 |
|---|---|---|
toolCallingManager(ToolCallingManager) | 执行工具调用的 manager | 自动构建 |
toolExecutionEligibilityChecker(ToolExecutionEligibilityChecker) | 决定是否触发下一次迭代 | response != null && response.hasToolCalls() |
advisorOrder(int) | 链顺序(HIGHEST_PRECEDENCE ~ LOWEST_PRECEDENCE) | HIGHEST_PRECEDENCE + 300 |
conversationHistoryEnabled(boolean) | 是否在迭代间内部维护历史;false 时仅转发最新消息 | true |
disableInternalConversationHistory() | 上面设为 false 的快捷方式 | - |
ToolExecutionEligibilityChecker
@FunctionalInterface
public interface ToolExecutionEligibilityChecker {
boolean isToolCallResponse(@Nullable ChatResponse chatResponse);
}可覆盖以适配提供商特定停止原因:
ToolExecutionEligibilityChecker strictChecker = response ->
response != null
&& response.hasToolCalls()
&& "tool_calls".equals(response.getMetadata().getFinishReason());
var advisor = ToolCallingAdvisor.builder()
.toolExecutionEligibilityChecker(strictChecker)
.build();4. 对话历史行为
默认 conversationHistoryEnabled=true:循环内每次 LLM 调用收到由 advisor 维护的完整历史(用户消息、模型响应、工具调用请求、工具响应)。
设为 false 时(disableInternalConversationHistory()):
- 内存 advisor 放在循环内,由它自己管历史。
- 或自己驱动循环,只想转发最新消息。
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.disableInternalConversationHistory()
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.order(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // 循环内
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor, toolCallingAdvisor)
.build();自动行为
用自动注册的 ToolCallingAdvisor 时,DefaultChatClient 检测到循环内 MemoryAdvisor 会自动禁用内部历史,无需手动调 disableInternalConversationHistory()。
5. 钩子方法(Call 路径)
protected ChatClientRequest doInitializeLoop(ChatClientRequest, CallAdvisorChain);
protected ChatClientRequest doBeforeCall(ChatClientRequest, CallAdvisorChain);
protected ChatClientResponse doAfterCall(ChatClientResponse, CallAdvisorChain);
protected ChatClientResponse doFinalizeLoop(ChatClientResponse, CallAdvisorChain);
protected List<Message> doGetNextInstructionsForToolCall(
ChatClientRequest, ChatClientResponse, ToolExecutionResult);| 钩子 | 时机与用途 |
|---|---|
doInitializeLoop | 第一次迭代前一次:设置会话作用域状态(索引、缓存、增强提示) |
doBeforeCall | 每次迭代前:注入/移除工具、改选项、加迭代上下文 |
doAfterCall | 每次迭代后:记录观察、转换响应、决定是否继续 |
doFinalizeLoop | 循环结束后一次:聚合指标、清理、附加最终转换 |
doGetNextInstructionsForToolCall | 决定下一次发给模型的消息;默认取决于 conversationHistoryEnabled |
ToolSearchToolCallingAdvisor 用 doInitializeLoop 索引工具集、用 doBeforeCall 仅注入已发现的工具——继承模式的标准示例。
6. 钩子方法(Stream 路径)
protected ChatClientRequest doInitializeLoopStream(ChatClientRequest, StreamAdvisorChain);
protected ChatClientRequest doBeforeStream(ChatClientRequest, StreamAdvisorChain);
protected ChatClientResponse doAfterStream(ChatClientResponse, StreamAdvisorChain);
protected Flux<ChatClientResponse> doFinalizeLoopStream(Flux<ChatClientResponse>, StreamAdvisorChain);
protected List<Message> doGetNextInstructionsForToolCallStream(
ChatClientRequest, ChatClientResponse, ToolExecutionResult);语义和 Call 路径对应。doAfterStream 操作聚合后的单次响应;doFinalizeLoopStream 可转换整个输出 Flux。
7. 子类示例
public class MyAuditingToolCallingAdvisor extends ToolCallingAdvisor {
private final AuditService audit;
@Override
protected ChatClientResponse doAfterCall(
ChatClientResponse response, CallAdvisorChain chain) {
var toolCalls = response.chatResponse().getResult().getOutput().getToolCalls();
for (var call : toolCalls) {
audit.recordIntent(call.name(), call.arguments());
}
return response;
}
public static Builder<?> builder() { return new Builder<>(); }
public static class Builder<T extends Builder<T>> extends ToolCallingAdvisor.Builder<T> {
private AuditService audit;
public T audit(AuditService audit) { this.audit = audit; return self(); }
@Override
public MyAuditingToolCallingAdvisor build() {
return new MyAuditingToolCallingAdvisor(
getToolCallingManager(),
getToolExecutionEligibilityChecker(),
getAdvisorOrder(),
isConversationHistoryEnabled(),
audit);
}
}
}Builder<T extends Builder<T>> 自引用泛型让子类 builder 能链式继承 setter 不丢失子类类型。
8. Single-ToolAdvisor Invariant
ToolAdvisor 是标记接口,DefaultChatClient 用它强制链中恰好一个工具 advisor:
- 自动注册的
ToolCallingAdvisor算一个。 - 注册第二个
ToolAdvisor实现(自定义子类)→ 跳过默认注册,用你的。 - 同时注册两个
ToolAdvisor→ 链构建快速失败。
替换默认实现用自动配置机制——见 01_Tool-Calling.md 第 12 节。
9. 用户控制的流式处理
用 .stream() 手动驱动时,每次迭代产生一个 chunk Flux。用 ChatClientMessageAggregator 聚合 chunk 同时转发原始流:
ChatClient chatClient = ...;
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
String question = "What is the weather in Amsterdam and Paris?";
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
AtomicReference<ChatClientResponse> ref = new AtomicReference<>();
new ChatClientMessageAggregator().aggregateChatClientResponse(
chatClient.prompt()
.messages(prompt.getInstructions())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.stream()
.chatClientResponse()
.doOnNext(chunk -> forwardToSse(chunk)),
ref::set
).blockLast();
ChatClientResponse response = ref.get();
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = new Prompt(result.conversationHistory(), chatOptions);
AtomicReference<ChatClientResponse> nextRef = new AtomicReference<>();
new ChatClientMessageAggregator().aggregateChatClientResponse(
chatClient.prompt()
.messages(result.conversationHistory())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.stream()
.chatClientResponse()
.doOnNext(chunk -> forwardToSse(chunk)),
nextRef::set
).blockLast();
response = nextRef.get();
}推荐
对于大多数"中间进度推到 UI"的需求,在循环内放一个自定义 advisor 比完全关闭自动注册更简单、更可组合——保留框架循环,只拦截 chunk 流。
10. 观测工具调用循环
public class ToolCallObservingAdvisor implements CallAdvisor, StreamAdvisor {
private final Consumer<ChatClientResponse> observer;
@Override
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 400; } // 循环内
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
request.prompt().getInstructions().forEach(msg -> log.debug("Message: {}", msg));
ChatClientResponse response = chain.nextCall(request);
observer.accept(response);
return response;
}
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
return chain.nextStream(request).doOnNext(observer);
}
}var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new ToolCallObservingAdvisor(chunk -> forwardToSse(chunk)))
.build();
String response = chatClient.prompt()
.user("What is the weather in Amsterdam and Paris?")
.tools(new WeatherTools())
.call()
.content();观察 advisor 在每次迭代上跑,主调用方仍只收到最终答案(ToolCallingAdvisor 从其返回流里过滤掉了工具调用 chunk)。
11. 直接返回(Return Direct)
工具 ToolMetadata.returnDirect=true 时,ToolCallingAdvisor:
- 正常执行工具调用。
- 在
ToolExecutionResult检测到returnDirect标志。 - 跳出循环。
- 把工具执行结果作为
ChatResponse直接返回给调用方——模型永远看不到工具结果。
适用:工具输出就是最终答案(RAG 检索)、或该工具应终止 agent 推理。
12. 退出自动注册
全局禁用:
spring.ai.chat.client.tool-calling.enabled=false单次调用禁用:
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.content();关闭后工具发给模型但不会被自动执行——进入用户控制模式。
13. 另请参见
01_Tool-Calling.md—— 工具调用循环(概念)01_Tool-Calling.md§7 —— 内存与工具循环(内/外顺序)01_Tool-Calling.md§12 —— 扩展循环(自动配置)02_Tool-Search-Tool.md——ToolSearchToolCallingAdvisor- 递归 Advisor —— 底层递归 advisor 模式
ChatModel Tool Calling(直接用 ChatModel 调工具)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/tools/chatmodel-tool-calling.html 📌 原文版本:Java AI 2.0.1
ChatModel 是和聊天提供商之间的底层接口。它接受工具定义、发给模型、返回响应。响应里的工具调用不会被自动执行——执行是调用方的责任。
白话解读
这是"低层路径"——跳过 advisor 链、跳过自动循环,自己掌控一切。绝大多数场景用 ChatClient 就够了,但有些场景(自定义编排器、不想被 advisor 干扰)必须直接用 ChatModel。
1. 何时直接用 ChatModel
- 不要 advisor 链(无 memory、无 observability、无
ToolCallingAdvisor)。 - 把工具调用集成到自定义编排器(领域工作流、非 Spring agent 框架)。
- 在 Java AI 之上构建基础设施(自定义
ChatClient、暴露高层 API 的库)。
其他场景(聊天、RAG、agentic 工作流、工具密集助手)→ 用 ChatClient。
2. 没有内部工具执行
ChatModel.call(prompt) 和 ChatModel.stream(prompt) 返回模型原始响应——可能含工具调用请求,但不会执行。要执行得自己写循环。
来自 1.x?
1.x 每个 ChatModel 自己跑工具循环;2.0 移除了。要迁移看升级说明。
3. 向 ChatModel 传递工具
工具通过 ToolCallingChatOptions.toolCallbacks(...) 传入,接受 List<ToolCallback> 或 ToolCallback[]。用 ToolCallbacks.from(...) 把 @Tool 对象转回调。
3.1 按请求
ChatModel chatModel = ...;
ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
ChatResponse response = chatModel.call(prompt);模型能看到工具定义;如果决定调用,响应里会有调用请求,你需要自己执行。
3.2 默认工具
某些 ChatModel 构建器接受默认选项:
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder().build())
.options(ToolCallingChatOptions.builder()
.toolCallbacks(dateTimeTools)
.build())
.build();默认工具在该 ChatModel 实例的每个请求里都会发给模型。
3.3 覆盖语义
当工具同时在默认和按请求设置时,按请求的工具列表会完全替换默认工具:
ChatModel chatModel = OllamaChatModel.builder()
.options(ToolCallingChatOptions.builder().toolCallbacks(defaultTools).build()) // 5 个
.build();
ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
.toolCallbacks(otherTool) // 1 个
.build();
ChatResponse response = chatModel.call(new Prompt("...", runtimeOptions));
// 模型只看到 otherTool——defaultTools 被替换,不是追加要在单请求里"默认 + 额外",显式合并:
List<ToolCallback> combined = new ArrayList<>(Arrays.asList(defaultTools));
combined.add(otherTool);
ChatOptions runtimeOptions = ToolCallingChatOptions.builder()
.toolCallbacks(combined)
.build();与 ChatClient 的差异
ChatClient 的 .tools(...) 追加到 .defaultTools(...);ChatModel 的按请求选项替换默认选项。要注意这个差异。
4. 手动驱动循环(阻塞)
ChatModel chatModel = ...;
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);
ChatResponse response = chatModel.call(prompt);
while (response.hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);
if (result.returnDirect()) {
return result.conversationHistory(); // returnDirect=true 直接返回
}
prompt = new Prompt(result.conversationHistory(), chatOptions);
response = chatModel.call(prompt);
}
String finalAnswer = response.getResult().getOutput().getText();关键组件:
ToolCallingManager:执行工具调用;Spring Boot 自动配置DefaultToolCallingManager,否则手动ToolCallingManager.builder().build()。response.hasToolCalls():响应是否含工具调用。toolCallingManager.executeToolCalls(prompt, response):找匹配工具、执行、返回更新历史的ToolExecutionResult。result.returnDirect():所有调用工具都有returnDirect=true时为true。result.conversationHistory():原始消息 + 助手工具调用请求 + 工具响应。
5. 手动驱动循环(流式)
ChatModel chatModel = ...;
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
Prompt prompt = new Prompt("What is the weather in Amsterdam and Paris?", chatOptions);
while (true) {
AtomicReference<ChatResponse> aggregated = new AtomicReference<>();
new MessageAggregator().aggregate(
chatModel.stream(prompt).doOnNext(chunk -> forwardToSse(chunk)),
aggregated::set
).blockLast();
ChatResponse response = aggregated.get();
if (!response.hasToolCalls()) break;
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);
if (result.returnDirect()) break;
prompt = new Prompt(result.conversationHistory(), chatOptions);
}完整组合 advisor 的流式变体见 03_ToolCallingAdvisor.md §9。
6. 工具上下文(Tool Context)
和 ChatClient 一样,通过 ToolCallingChatOptions 设置:
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new CustomerTools()))
.toolContext(Map.of("tenantId", "acme"))
.build();默认 + 运行时的 toolContext 都设置时合并(运行时优先)——和 toolCallbacks 的"替换"语义不一样。
7. 直接返回(Return Direct)
returnDirect 由 ToolCallingManager.executeToolCalls(...) 处理:
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response);
if (result.returnDirect()) {
return result.conversationHistory();
}多工具调用时,所有工具都 returnDirect=true 才算 returnDirect。
8. 另请参见
01_Tool-Calling.md—— 推荐路径03_ToolCallingAdvisor.md—— advisor 循环03_ToolCallingAdvisor.md§9 —— ChatClient 的用户控制流式- 从 1.x 升级工具调用到 2.0