文章目录
- [1. 概述](#1. 概述)
- [2. 工具加载流程](#2. 工具加载流程)
-
- [2.1 转换为 ToolCallback](#2.1 转换为 ToolCallback)
- [2.2 从各类来源收集所有可用工具](#2.2 从各类来源收集所有可用工具)
-
- [2.2.1 直接传入的工具](#2.2.1 直接传入的工具)
- [2.2.2 ToolCallbackProvider 提供的工具](#2.2.2 ToolCallbackProvider 提供的工具)
- [2.2.3 根据名称解析并加载工具](#2.2.3 根据名称解析并加载工具)
- [2.2.4 解析器中提取工具](#2.2.4 解析器中提取工具)
- [2.2.5 拦截器提供的工具](#2.2.5 拦截器提供的工具)
- [2.2.6 钩子提供的工具](#2.2.6 钩子提供的工具)
- [2.3 工具合并](#2.3 工具合并)
- [2.4 节点分发](#2.4 节点分发)
- [3. 工具执行流程](#3. 工具执行流程)
-
- [3.1 AgentLlmNode 执行阶段](#3.1 AgentLlmNode 执行阶段)
- [3.2 路由阶段](#3.2 路由阶段)
- [3.3 AgentToolNode 执行阶段](#3.3 AgentToolNode 执行阶段)
1. 概述
Tools 是 agents 调用来执行操作的组件。它们通过定义良好的输入和输出让模型与外部世界交互,从而扩展模型的能力。
Spring AI 支持两种方式创建工具:
- 方法作为
Tools:- 声明式,使用
@Tool注解 - 编程式,使用低级
MethodToolCallback实现。
- 声明式,使用
- 函数作为
Tools- 编程式,
FunctionToolCallback - 声明式,将工具定义为
Spring beans,让Spring AI使用ToolCallbackResolver接口(通过SpringBeanToolCallbackResolver实现)在运行时动态解析它们,而不是以编程方式指定工具。
- 编程式,
Spring AI ChatClient 支持多种方式配置全局默认工具:

在运行时也支持多种方式配置工具:

Spring AI Alibaba ReactAgent 支持多种方式配置全局默认工具:

但是不支持配置运行时工具:

设置工具方法:
| 方法 | 说明 |
|---|---|
| tools(List tools) | 直接添加 ToolCallback 列表 |
| tools(ToolCallback... tools) | 可变参数方式添加 ToolCallback |
| methodTools(Object... toolObjects) | 从对象方法自动创建工具(底层使用 ToolCallbacks.from()) |
| toolNames(String... toolNames) | 按工具名称解析工具(依赖工具解析器 ToolCallbackResolver) |
| toolCallbackProviders(ToolCallbackProvider... providers) | 添加工具提供者 |
| resolver(ToolCallbackResolver resolver) | 设置自定义工具解析器 |
设置工具上下文、异常处理器方法:
| 方法 | 说明 |
|---|---|
| toolContext(Map<String, Object> toolContext) | 设置工具上下文信息 |
| toolExecutionExceptionProcessor(ToolExecutionExceptionProcessor processor) | 设置工具执行异常处理器 |
工具执行配置方法:
| 方法 | 说明 |
|---|---|
| parallelToolExecution(boolean parallel) | 启用/禁用并行工具执行 |
| maxParallelTools(int max) | 设置最大并行工具执行数量(默认值为 5) |
| toolExecutionTimeout(Duration timeout) | 设置工具执行超时时间(默认值为 5 分钟) |
| wrapSyncToolsAsAsync(boolean wrap) | 是否自动将同步工具包装为异步执行 |
此外,还支持从 Hook 、ModelInterceptor 中获取工具:
java
public interface Hook extends Prioritized {
/**
* 获取当前钩子提供的工具列表。
* <p>
* 钩子提供的工具会与智能体(Agent)已配置的工具进行合并,
* 并在智能体执行过程中供其调用。
* <p>
* 这些工具会注册到智能体的工具节点中,
* 可在智能体执行流程中被调用。
*
* @return 工具回调列表,默认返回空列表
* @see ToolCallback
*/
default List<ToolCallback> getTools() {
return List.of();
}
}
java
/**
* 模型拦截器:用于对模型调用进行包装增强
* 实现类可修改请求/响应参数,或新增重试、服务降级等增强逻辑
*/
public abstract class ModelInterceptor implements Interceptor {
/**
* 获取当前拦截器提供的工具列表
* 拦截器可提供内置工具,工具会自动注册到智能体(Agent)中
*
* @return 当前拦截器提供的工具回调列表,默认返回空列表
*/
public List<ToolCallback> getTools() {
return Collections.emptyList();
}
}
2. 工具加载流程
方法入口:
java
ReactAgent.builder()
.methodTools(dateTimeTools) // dateTimeTools 是一个普通 Java 对象
.build()
2.1 转换为 ToolCallback
Builder.methodTools() 使用 Spring AI 的 ToolCallbacks.from() 将对象转为 ToolCallback ,并添加到 Builder 的成员属性tools 中:
java
// Builder.java:164-169
public Builder methodTools(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
// Spring AI 的 ToolCallbacks.from() 将 @Tool 注解的方法转为 ToolCallback
this.tools.addAll(Arrays.asList(ToolCallbacks.from(toolObjects)));
return this;
}
2.2 从各类来源收集所有可用工具
接着进入到 DefaultBuilder#build() 构建 ReactAgent 阶段,进行工具的收集合并:
关键代码:
java
// 收集所有工具
List<ToolCallback> allTools = gatherLocalTools();
// 设置给 LLM 执行节点
// Set combined tools to LLM node
if (CollectionUtils.isNotEmpty(allTools)) {
llmNodeBuilder.toolCallbacks(Collections.unmodifiableList(allTools));
}
// 设置给 工具 执行节点
// Setup tool node with all available tools
AgentToolNode toolNode;
AgentToolNode.Builder toolBuilder = AgentToolNode.builder()
.agentName(this.name)
.parallelToolExecution(this.parallelToolExecution)
.maxParallelTools(this.maxParallelTools)
.toolExecutionTimeout(this.toolExecutionTimeout)
.wrapSyncToolsAsAsync(this.wrapSyncToolsAsAsync);
// 设置解析器
if (resolver != null) {
toolBuilder.toolCallbackResolver(resolver);
}
if (CollectionUtils.isNotEmpty(allTools)) {
toolBuilder.toolCallbacks(allTools);
}
gatherLocalTools() 按以下顺序从多个来源归集工具:
- 通过 {
@code tools**} 直接传入的工具 - 来自 {
@code toolCallbackProviders} 工具提供者的工具 - 通过 {
@code toolNames} 工具名称解析得到的工具 - 从 {
@code resolver} 解析器中提取的工具(仅当常规工具未收集到任何内容时生效) - 来自模型拦截器 {
@code modelInterceptors} 的扩展工具 - 来自钩子 {
@code hooks} 的扩展工具
源码如下:
java
protected List<ToolCallback> gatherLocalTools() {
// ===================== 第一步:初始化容器 =====================
// 常规工具集合:存储用户直接配置、提供者、名称解析的核心业务工具
List<ToolCallback> regularTools = new ArrayList<>();
// ===================== 第二步:收集用户直接传入的工具 =====================
// 从直接配置的 tools 集合中加载工具(最高优先级的用户自定义工具)
if (CollectionUtils.isNotEmpty(tools)) {
regularTools.addAll(tools);
}
// ===================== 第三步:从工具回调提供者加载工具 =====================
// 遍历所有 ToolCallbackProvider,加载其提供的工具
if (CollectionUtils.isNotEmpty(toolCallbackProviders)) {
for (var provider : toolCallbackProviders) {
regularTools.addAll(List.of(provider.getToolCallbacks()));
}
}
// ===================== 第四步:根据工具名称解析并加载工具 =====================
if (CollectionUtils.isNotEmpty(toolNames)) {
for (String toolName : toolNames) {
// 去重:如果工具已存在,跳过避免重复加载
if (regularTools.stream().anyMatch(tool -> tool.getToolDefinition().name().equals(toolName))) {
continue;
}
// 校验:工具解析器不能为空,否则无法根据名称解析工具
if (this.resolver == null) {
throw new IllegalStateException(
"ToolCallbackResolver 为空,无法解析工具名称:" + toolName);
}
// 执行名称解析,获取工具实例
ToolCallback toolCallback = this.resolver.resolve(toolName);
// 解析失败:抛出异常并打印警告日志
if (toolCallback == null) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING, toolName);
throw new IllegalStateException("未找到对应工具名称的 ToolCallback:" + toolName);
}
regularTools.add(toolCallback);
}
}
// ===================== 第五步:兜底逻辑 - 从解析器中提取工具 =====================
// 场景:常规工具为空 且 解析器存在时,尝试从解析器自身提取工具
if (regularTools.isEmpty() && this.resolver != null) {
// 方案1:解析器实现了 ToolCallbackProvider 接口,直接获取工具
if (this.resolver instanceof ToolCallbackProvider provider) {
ToolCallback[] resolverTools = provider.getToolCallbacks();
if (resolverTools != null && resolverTools.length > 0) {
regularTools.addAll(List.of(resolverTools));
logger.debug("从 ToolCallbackResolver(ToolCallbackProvider)中提取到 {} 个工具",
resolverTools.length);
}
}
// 方案2:反射兜底 - 解析器未实现接口,尝试反射获取内部 tools 字段
else {
try {
// 获取解析器内部的 tools 私有字段
Field toolsField = this.resolver.getClass().getDeclaredField("tools");
toolsField.setAccessible(true);
Object toolsObj = toolsField.get(this.resolver);
// 校验字段类型为 Map,并提取工具
if (toolsObj instanceof java.util.Map) {
@SuppressWarnings("unchecked")
java.util.Map<String, ToolCallback> toolsMap = (java.util.Map<String, ToolCallback>) toolsObj;
if (!toolsMap.isEmpty()) {
regularTools.addAll(toolsMap.values());
logger.debug("通过反射从 ToolCallbackResolver 中提取到 {} 个工具",
toolsMap.size());
}
}
} catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) {
// 反射失败:仅打印追踪日志,不中断流程(兼容不同解析器实现)
logger.trace("无法通过反射从解析器中提取工具:{}", e.getMessage());
}
}
}
// ===================== 第六步:收集模型拦截器中的扩展工具 =====================
List<ToolCallback> interceptorTools = new ArrayList<>();
if (CollectionUtils.isNotEmpty(modelInterceptors)) {
// 扁平化流:将所有拦截器的工具合并为一个集合
interceptorTools = modelInterceptors.stream()
.flatMap(interceptor -> interceptor.getTools().stream())
.toList();
}
// ===================== 第七步:收集钩子中的扩展工具 =====================
List<ToolCallback> hookTools = new ArrayList<>();
if (CollectionUtils.isNotEmpty(hooks)) {
for (Hook hook : hooks) {
List<ToolCallback> toolsFromHook = hook.getTools();
if (CollectionUtils.isNotEmpty(toolsFromHook)) {
hookTools.addAll(toolsFromHook);
logger.debug("从钩子 '{}' 中收集到 {} 个工具", hook.getName(), toolsFromHook.size());
}
}
}
// ===================== 第八步:按优先级合并所有工具 =====================
// 优先级规则:钩子工具 > 拦截器工具 > 常规业务工具
List<ToolCallback> allTools = new ArrayList<>();
allTools.addAll(hookTools); // 第一优先级:钩子工具
allTools.addAll(interceptorTools);// 第二优先级:拦截器工具
allTools.addAll(regularTools); // 第三优先级:常规业务工具
// 返回最终合并后的完整工具列表
return allTools;
}
2.2.1 直接传入的工具
收集通过以下几种方式直接传入的工具:
tools(List< ToolCallback > tools)tools(ToolCallback... tools)methodTools(Object... toolObjects)
初始化工具集合,并收集用户直接传入的工具:
java
// ===================== 第一步:初始化容器 =====================
// 常规工具集合:存储用户直接配置、提供者、名称解析的核心业务工具
List<ToolCallback> regularTools = new ArrayList<>();
// ===================== 第二步:收集用户直接传入的工具 =====================
// 从直接配置的 tools 集合中加载工具(最高优先级的用户自定义工具)
if (CollectionUtils.isNotEmpty(tools)) {
regularTools.addAll(tools);
}
2.2.2 ToolCallbackProvider 提供的工具
来源 :通过 .toolCallbackProviders(ToolCallbackProvider...) 添加的工具提供者。
从工具回调提供者加载工具,遍历所有 ToolCallbackProvider,加载其提供的工具:
java
if (CollectionUtils.isNotEmpty(toolCallbackProviders)) {
for (var provider : toolCallbackProviders) {
regularTools.addAll(List.of(provider.getToolCallbacks()));
}
}
重点提示 :Spring AI ChatClient 中配置的 ToolCallbackProvider 是懒加载,在实际执行时才调用其 getToolCallbacks() 方法。而 Spring AI Alibaba 中没有懒加载。
2.2.3 根据名称解析并加载工具
来源 :通过 .toolNames(String...) 添加的工具名称,由 resolver 解析成实际的 ToolCallback。
java
if (CollectionUtils.isNotEmpty(toolNames)) {
for (String toolName : toolNames) {
// 去重:如果工具已存在,跳过避免重复加载
if (regularTools.stream().anyMatch(tool -> tool.getToolDefinition().name().equals(toolName))) {
continue;
}
// 校验:工具解析器不能为空,否则无法根据名称解析工具
if (this.resolver == null) {
throw new IllegalStateException(
"ToolCallbackResolver 为空,无法解析工具名称:" + toolName);
}
// 执行名称解析,获取工具实例
ToolCallback toolCallback = this.resolver.resolve(toolName);
// 解析失败:抛出异常并打印警告日志
if (toolCallback == null) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING, toolName);
throw new IllegalStateException("未找到对应工具名称的 ToolCallback:" + toolName);
}
regularTools.add(toolCallback);
}
}
2.2.4 解析器中提取工具
当用户没有提供任何工具时,尝试从 resolver 中提取所有可用工具(兜底方案):
java
if (regularTools.isEmpty() && this.resolver != null) {
// 方式 A:resolver 实现了 ToolCallbackProvider 接口
if (this.resolver instanceof ToolCallbackProvider provider) {
ToolCallback[] resolverTools = provider.getToolCallbacks();
if (resolverTools != null && resolverTools.length > 0) {
regularTools.addAll(List.of(resolverTools));
}
} else {
// 方式 B:反射获取 tools 字段(兜底)
try {
Field toolsField = this.resolver.getClass().getDeclaredField("tools");
toolsField.setAccessible(true);
Object toolsObj = toolsField.get(this.resolver);
if (toolsObj instanceof Map) {
Map<String, ToolCallback> toolsMap = (Map<String, ToolCallback>) toolsObj;
regularTools.addAll(toolsMap.values());
}
} catch (Exception e) {
// 反射失败,忽略
}
}
}
使用的解析器由 Spring AI 提供:
java
public interface ToolCallbackResolver {
/**
* Resolve the {@link ToolCallback} for the given tool name.
*/
@Nullable
ToolCallback resolve(String toolName);
}
提示 : Spring AI 自动配置会提供一个默认的 ToolCallbackResolver ,初始化时会加载 ToolCallbackProvider 中的所有工具,还支持动态从 Spring 容器中根据 Bean 名称查询工具。
2.2.5 拦截器提供的工具
来源 :从 modelInterceptors 拦截器中提取它们提供的工具。
java
List<ToolCallback> interceptorTools = new ArrayList<>();
if (CollectionUtils.isNotEmpty(modelInterceptors)) {
// 扁平化流:将所有拦截器的工具合并为一个集合
interceptorTools = modelInterceptors.stream()
.flatMap(interceptor -> interceptor.getTools().stream())
.toList();
}
2.2.6 钩子提供的工具
来源 :从 hooks 中提取每个 Hook 提供的工具。
java
List<ToolCallback> hookTools = new ArrayList<>();
if (CollectionUtils.isNotEmpty(hooks)) {
for (Hook hook : hooks) {
List<ToolCallback> toolsFromHook = hook.getTools();
if (CollectionUtils.isNotEmpty(toolsFromHook)) {
hookTools.addAll(toolsFromHook);
}
}
}
2.3 工具合并
合并所有工具:
java
List<ToolCallback> allTools = new ArrayList<>();
allTools.addAll(hookTools); // 优先级 1:Hook 工具
allTools.addAll(interceptorTools); // 优先级 2:拦截器工具
allTools.addAll(regularTools); // 优先级 3:用户工具
return allTools;
最终优先级顺序:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1(最高) | hookTools | Hook 提供的工具最先添加 |
| 2 | interceptorTools | 拦截器提供的工具 |
| 3 | regularTools | 用户直接提供的工具 |
注意 :由于是用 addAll 添加到列表末尾,而 List 是支持重复对象的,在前面的位置的工具 LLM 可能优先选择。
2.4 节点分发
工具对象会分发给 LLM 节点、工具执行节点:
java
// DefaultBuilder.java:124-148
// AgentLlmNode - 用于 LLM 调用
if (CollectionUtils.isNotEmpty(allTools)) {
llmNodeBuilder.toolCallbacks(Collections.unmodifiableList(allTools));
}
// AgentToolNode - 用于工具执行
if (CollectionUtils.isNotEmpty(allTools)) {
toolBuilder.toolCallbacks(allTools);
}
最终保存到:
| 属性 | 存储位置 | 用途 |
|---|---|---|
| AgentLlmNode.toolCallbacks | LLM 节点 | 告知 LLM 可用工具 |
| AgentToolNode.toolCallbacks | 工具节点 | 实际执行工具调用 |
可以 Debug 看到:


3. 工具执行流程
3.1 AgentLlmNode 执行阶段
AgentLlmNode#apply() 方法中会从自己的 toolCallbacks 提取工具信息:
java
// 1. 构建请求时提取工具信息
if (toolCallbacks != null && !toolCallbacks.isEmpty()) {
List<String> toolNames = new ArrayList<>();
Map<String, String> toolDescriptions = new HashMap<>();
for (ToolCallback callback : toolCallbacks) {
toolNames.add(callback.getToolDefinition().name());
toolDescriptions.put(callback.getToolDefinition().name(),
callback.getToolDefinition().description());
}
requestBuilder.tools(toolNames);
requestBuilder.toolDescriptions(toolDescriptions);
}
// 构建模型请求
ModelRequest modelRequest = requestBuilder.build();
modelRequest 中包含工具名称、描述信息:

接着进入到构建 ChatClient 请求规格(ChatClientRequestSpec)方法中,涉及工具相关的处理有:
buildChatClientRequestSpec:将工具设置到PromptfilterToolCallbacks:根据指定的工具名称过滤,仅保留名称匹配的工具回调
java
/**
* 构建 ChatClient 请求规格(ChatClientRequestSpec)
* 核心作用:整合消息、工具回调、调用配置,组装为 AI 模型的标准请求对象
* @param modelRequest 模型请求对象,封装输入消息、工具、配置等参数
* @param config 运行时配置,用于传递上下文、动态参数等
* @return 构建完成的 ChatClient 请求规格
*/
private ChatClient.ChatClientRequestSpec buildChatClientRequestSpec(ModelRequest modelRequest, RunnableConfig config) {
// 按需拼接系统提示词(根据模型请求的配置,自动追加系统消息)
List<Message> messages = appendSystemPromptIfNeeded(modelRequest);
// 【重要】如果 ModelRequest 中同时自定义了工具(工具拦截器)和调用选项,工具会覆盖选项中的工具调用配置
// 过滤出可用的工具回调列表(剔除无效/重复工具)
List<ToolCallback> filteredToolCallbacks = filterToolCallbacks(modelRequest);
// 处理模型请求中的动态工具回调:存在则追加到工具列表,并存入配置上下文
if (!CollectionUtils.isEmpty(modelRequest.getDynamicToolCallbacks())) {
filteredToolCallbacks.addAll(modelRequest.getDynamicToolCallbacks());
// FIXME 待优化:通过 RunnableConfig 的配置上下文传递动态工具回调到工具节点(框架内部使用)
config.context().put(RunnableConfig.DYNAMIC_TOOL_CALLBACKS_METADATA_KEY, modelRequest.getDynamicToolCallbacks());
}
// 构建基础请求模板:设置消息列表 + 拦截顾问(Advisors)
var promptSpec = this.chatClient.prompt()
.messages(messages)
.advisors(this.advisors);
// 获取模型请求中的工具调用配置选项
ToolCallingChatOptions requestOptions = modelRequest.getOptions();
// ===================== 分场景处理调用配置 =====================
if (requestOptions != null) {
// 复制配置对象,避免修改原始配置
ToolCallingChatOptions copiedOptions = requestOptions.copy();
// 为配置设置过滤后的工具回调
copiedOptions.setToolCallbacks(filteredToolCallbacks);
// 强制禁用框架内部工具执行,避免与 Agent 自身的工具执行管理逻辑冲突
copiedOptions.setInternalToolExecutionEnabled(false);
// 将处理后的配置应用到请求模板
promptSpec.options(copiedOptions);
} else {
// 场景:无自定义请求选项,检查 ChatModel/ChatClient 的默认配置
if (promptSpec instanceof DefaultChatClient.DefaultChatClientRequestSpec defaultChatClientRequestSpec) {
// 获取默认的聊天配置
ChatOptions options = defaultChatClientRequestSpec.getChatOptions();
// 子场景1:无默认配置 → 新建配置,绑定工具并禁用内部执行
if (options == null) {
options = ToolCallingChatOptions.builder()
.toolCallbacks(filteredToolCallbacks)
.internalToolExecutionEnabled(false)
.build();
defaultChatClientRequestSpec.options(options);
}
// 子场景2:默认配置是工具调用配置 → 复制配置、更新工具、禁用内部执行
else if (options instanceof ToolCallingChatOptions toolCallingChatOptions) {
ToolCallingChatOptions copiedOptions = toolCallingChatOptions.copy();
copiedOptions.setToolCallbacks(filteredToolCallbacks);
copiedOptions.setInternalToolExecutionEnabled(false);
defaultChatClientRequestSpec.options(copiedOptions);
}
}
// 场景:非默认请求规格 + 存在可用工具 → 直接为请求设置工具
else if (!filteredToolCallbacks.isEmpty()) {
promptSpec.tools(filteredToolCallbacks);
}
}
// 返回最终构建完成的请求规格
return promptSpec;
}
java
/**
* 根据模型请求(ModelRequest)中指定的工具名称,过滤出匹配的工具回调列表
* @param modelRequest 模型请求对象,包含需要过滤的工具名称列表
* @return 匹配请求工具的过滤后工具回调列表
*/
private List<ToolCallback> filterToolCallbacks(ModelRequest modelRequest) {
// 初始化最终返回的工具回调集合
List<ToolCallback> toolCallbacks = new ArrayList<>();
// 场景1:模型请求为空,直接使用当前类默认注册的所有工具回调
if (modelRequest == null) {
toolCallbacks.addAll(this.toolCallbacks);
return toolCallbacks;
}
// 场景2:模型请求包含配置,且配置中指定了工具回调,则优先使用请求中的工具回调
if (modelRequest.getOptions() != null && modelRequest.getOptions().getToolCallbacks() != null) {
toolCallbacks.addAll(modelRequest.getOptions().getToolCallbacks());
} else {
// 无操作
// 默认情况下,buildChatOptions() 方法会确保 modelRequest.getOptions().getToolCallbacks() 始终被赋值
// 这样设计允许用户通过在配置中设置空的工具回调列表,来禁用所有工具
}
// 获取模型请求中指定需要调用的工具名称列表
List<String> requestedTools = modelRequest.getTools();
// 场景3:未指定具体工具,直接返回全部已加载的工具回调
if (requestedTools == null || requestedTools.isEmpty()) {
return toolCallbacks;
}
// 场景4:根据指定的工具名称过滤,仅保留名称匹配的工具回调
return new ArrayList<>(toolCallbacks.stream()
.filter(callback -> requestedTools.contains(callback.getToolDefinition().name()))
.toList());
}
进入到 Spring AI 执行,Prompt 对象中的工具信息会发送给大模型:

需要调用工具时,AgentLlmNode 返回相关状态信息:

3.2 路由阶段
需要调用工具时,ReactAgent 中的边(Edge)会路由到工具节点:
java
private EdgeAction makeModelToTools(String modelDestination, String endDestination) {
return state -> {
// 优先级 1:检查 jump_to 指令
if (jumpToValue != null) {
return switch (jumpTo) {
case tool -> AGENT_TOOL_NAME;
case end -> endDestination;
case model -> modelDestination;
};
}
// 优先级 2:检查消息是否有工具调用
Message lastMessage = messages.get(messages.size() - 1);
if (lastMessage instanceof AssistantMessage assistantMessage) {
if (assistantMessage.hasToolCalls()) {
return AGENT_TOOL_NAME; // ← 路由到工具节点
}
}
return endDestination; // 无工具调用,结束
};
}
3.3 AgentToolNode 执行阶段
先选择执行模式:
java
@Override
public Map<String, Object> apply(OverAllState state, RunnableConfig config) throws Exception {
List<Message> messages = (List<Message>) state.value("messages").orElseThrow();
Message lastMessage = messages.get(messages.size() - 1);
if (lastMessage instanceof AssistantMessage assistantMessage) {
List<AssistantMessage.ToolCall> toolCalls = assistantMessage.getToolCalls();
// 选择执行模式
if (parallelToolExecution && toolCalls.size() > 1) {
return executeToolCallsParallel(toolCalls, state, config); // 并行执行
} else {
return executeToolCallsSequential(toolCalls, state, config); // 顺序执行
}
}
}
顺序执行模式中,遍历所有工具并执行调用(executeToolCallWithInterceptors()),顺序执行:
java
/**
* 工具调用的**顺序执行**(框架原始默认行为)
*
* <p>
* 每个工具都会拥有独立的状态更新Map,实现状态隔离。
* 该机制可避免:后续工具执行超时/异常时,清空之前已成功执行工具的状态更新。
* 状态隔离的实现原理:
* <ol>
* <li>为每一次工具执行创建全新的 {@code ConcurrentHashMap}</li>
* <li>工具执行成功后,立即将更新结果合并到总合并集合 {@code mergedUpdates}</li>
* <li>若工具超时,仅清空其独立的状态Map,不会影响已合并的成功数据</li>
* </ol>
* </p>
*
* <p>
* 当前行为与并行执行模式保持一致,并行模式通过 {@link ToolStateCollector} 实现单工具的状态隔离。
* </p>
*
* <h3>状态合并规则</h3>
* <p>
* 顺序执行模式采用 <b>后写覆盖</b> 规则处理状态更新。
* 当多个工具修改同一个状态Key时,最后执行的工具值会覆盖之前的所有值。
* 该规则保留了并行执行功能推出前的框架原始行为。
* </p>
* <p>
* 注意:这与并行执行模式不同,并行模式会严格遵循 {@link com.alibaba.cloud.ai.graph.KeyStrategy} 执行合并操作。
* 如果你需要确定性的合并行为(例如列表追加),请使用并行执行模式。
* </p>
*
* @see #executeToolCallsParallel(List, OverAllState, RunnableConfig)
*/
private Map<String, Object> executeToolCallsSequential(List<AssistantMessage.ToolCall> toolCalls,
OverAllState state, RunnableConfig config) {
// 最终返回的状态更新结果
Map<String, Object> updatedState = new HashMap<>();
// 总合并更新集合:累加所有成功执行的工具产生的状态更新
Map<String, Object> mergedUpdates = new HashMap<>();
// 工具执行响应结果集合
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
// 标记是否直接返回结果
Boolean returnDirect = null;
// 遍历所有工具调用,顺序执行
for (AssistantMessage.ToolCall toolCall : toolCalls) {
// ===================== 核心:状态隔离 =====================
// 每个工具分配独立的更新Map,实现状态隔离
// 若当前工具超时/执行失败,仅清空此独立Map,不会影响已合并的成功数据
Map<String, Object> toolSpecificUpdate = new ConcurrentHashMap<>();
// 执行工具调用(经过拦截器增强)
ToolCallResponse response = executeToolCallWithInterceptors(toolCall, state, config, toolSpecificUpdate,
false);
// 封装工具响应结果
toolResponses.add(response.toToolResponse());
// 判断当前工具调用是否需要直接返回结果
returnDirect = shouldReturnDirect(toolCall, returnDirect, config);
// 立即合并成功的工具更新:后续工具异常不会影响已合并的数据
mergedUpdates.putAll(toolSpecificUpdate);
}
// 构建工具响应消息
ToolResponseMessage.Builder builder = ToolResponseMessage.builder()
.responses(toolResponses);
// 若标记直接返回,添加元数据标识结束原因
if (returnDirect != null && returnDirect) {
builder.metadata(Map.of(FINISH_REASON_METADATA_KEY, FINISH_REASON));
}
ToolResponseMessage toolResponseMessage = builder.build();
// 开启执行日志时,打印工具调用结果
if (enableActingLog) {
logger.info("[ThreadId {}] Agent {} 工具执行结果: {}", config.threadId().orElse(THREAD_ID_DEFAULT),
agentName, toolResponseMessage);
}
// 组装最终状态:消息 + 所有工具的合并更新
updatedState.put("messages", toolResponseMessage);
updatedState.putAll(mergedUpdates);
return updatedState;
}
executeToolCallWithInterceptors 方法是真正执行一个 ToolCall 的总入口方法:
java
/**
* 执行工具调用(支持完整拦截器链),并行执行时支持取消令牌追踪
* 这是 Spring AI Agent 真正执行一个 ToolCall 的总入口方法
*
* @param toolCall 要执行的工具调用(包含工具名、参数、ID)
* @param state 智能体全局状态
* @param config 运行时配置(线程、超时、元数据等)
* @param extraStateFromToolCall 收集工具执行后的状态更新
* @param inParallelExecution 是否并行执行
* @param cancellationTokens 并行执行的取消令牌(可中断工具)
* @param toolIndex 工具在并行列表中的下标
* @return 工具执行结果
*/
private ToolCallResponse executeToolCallWithInterceptors(
AssistantMessage.ToolCall toolCall,
OverAllState state,
RunnableConfig config,
Map<String, Object> extraStateFromToolCall,
boolean inParallelExecution,
Map<Integer, DefaultCancellationToken> cancellationTokens,
int toolIndex) {
// ====================== 1. 构建工具调用请求对象 ======================
// 把 toolCall、config、state 封装成标准的 ToolCallRequest
ToolCallRequest request = ToolCallRequest.builder()
.toolCall(toolCall) // 工具调用信息
.context(config.metadata().orElse(new HashMap<>())) // 上下文元数据
.executionContext(new ToolCallExecutionContext(config, state)) // 执行上下文
.build();
// ====================== 2. 创建【基础执行器】 ======================
// 真正执行工具调用的核心逻辑(后面会被拦截器包裹)
ToolCallHandler baseHandler = req -> {
// 2.1 根据工具名,从 Spring 容器中查找对应的 ToolCallback 实现
ToolCallback toolCallback = resolve(req.getToolName(), config);
// 2.2 找不到工具 → 报错(通常是 LLM 幻觉调用了不存在的工具)
if (toolCallback == null) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING, req.getToolName());
throw new IllegalStateException("No ToolCallback found for tool name: " + req.getToolName());
}
// 2.3 日志:打印正在执行工具
if (enableActingLog) {
logger.info("[ThreadId {}] Agent {} acting, executing tool {}.",
config.threadId().orElse(THREAD_ID_DEFAULT), agentName, req.getToolName());
}
// 2.4 构建工具上下文(基础上下文 + 请求上下文)
Map<String, Object> toolContextMap = new HashMap<>(toolContext);
toolContextMap.putAll(req.getContext());
// 2.5 如果工具需要状态注入 → 把 Agent 状态/配置放进去
// 支持:StateAware / FunctionTool / MethodTool
if (toolCallback instanceof StateAwareToolCallback
|| toolCallback instanceof FunctionToolCallback<?, ?>
|| toolCallback instanceof MethodToolCallback) {
toolContextMap.putAll(Map.of(
AGENT_STATE_CONTEXT_KEY, state, // 全局状态
AGENT_CONFIG_CONTEXT_KEY, config, // 运行配置
AGENT_STATE_FOR_UPDATE_CONTEXT_KEY, extraStateFromToolCall // 可写状态
));
}
// 2.6 【关键】分发执行:同步 / 异步 / 并行
return executeToolByType(
toolCallback,
req,
toolContextMap,
config,
extraStateFromToolCall,
inParallelExecution,
cancellationTokens,
toolIndex
);
};
// ====================== 3. 构建拦截器链 ======================
// 如果有工具拦截器(ToolCallInterceptor),则按顺序包裹基础执行器
// 类似 MVC 拦截器、MyBatis 插件、AOP 切面
ToolCallHandler chainedHandler = InterceptorChain.chainToolInterceptors(toolInterceptors, baseHandler);
// ====================== 4. 执行(经过所有拦截器 → 最终执行工具) ======================
return chainedHandler.call(request);
}
上面的方法会通过以下方式拿到工具对象:
- 从本地注册的工具回调中匹配
- 从配置元数据中解析动态工具回调(由
AgentLlmNode/ModelInterceptor注入) - 通过全局工具回调解析器解析,解析器为空则返回
null
java
/**
* 根据工具名称解析匹配的工具回调
* 查找优先级:本地注册工具 -> 配置元数据动态工具 -> 全局工具解析器
* @param toolName 工具名称
* @param config 运行时配置(存储动态工具元数据)
* @return 匹配的工具回调,未找到则返回null
*/
private ToolCallback resolve(String toolName, RunnableConfig config) {
// 第一优先级:从本地注册的工具回调中匹配
if (toolCallbacks != null) {
var fromNode = toolCallbacks.stream()
// 过滤工具名称完全匹配的工具回调
.filter(callback -> callback.getToolDefinition().name().equals(toolName))
.findFirst();
// 匹配到则直接返回
if (fromNode.isPresent()) {
return fromNode.get();
}
}
// 第二优先级:从配置元数据中解析动态工具回调(由 AgentLlmNode / ModelInterceptor 注入)
ToolCallback fromDynamic = resolveFromConfigMetadata(toolName, config);
if (fromDynamic != null) {
return fromDynamic;
}
// 第三优先级:通过全局工具回调解析器解析,解析器为空则返回null
return toolCallbackResolver == null ? null : toolCallbackResolver.resolve(toolName);
}
executeToolByType 根据工具回调类型【同步/异步】路由执行工具调用,支持并行执行、取消令牌追踪:
java
/**
* 根据工具回调类型【同步/异步】路由执行工具调用,支持并行执行、取消令牌追踪
* 作用:统一分发工具执行逻辑------自动判断用异步执行还是同步执行
*
* @param toolCallback 工具回调实例(真正要执行的工具)
* @param request 工具调用请求(包含工具名、入参等)
* @param toolContextMap 工具上下文(传递给工具的环境信息)
* @param config 运行时配置(线程池、超时等)
* @param extraStateFromToolCall 工具执行中需要收集的状态
* @param inParallelExecution 是否处于【并行工具调用】模式
* @param cancellationTokens 并行执行时的取消令牌(可中途取消工具)
* @param toolIndex 并行执行中当前工具的下标
* @return 工具执行结果响应
*/
private ToolCallResponse executeToolByType(
ToolCallback toolCallback,
ToolCallRequest request,
Map<String, Object> toolContextMap,
RunnableConfig config,
Map<String, Object> extraStateFromToolCall,
boolean inParallelExecution,
Map<Integer, DefaultCancellationToken> cancellationTokens,
int toolIndex) {
// ==========================================
// 分支1:工具本身就是【异步工具 AsyncToolCallback】
// 直接走异步执行逻辑
// ==========================================
if (toolCallback instanceof AsyncToolCallback async) {
return executeAsyncTool(
async,
request,
toolContextMap,
config,
extraStateFromToolCall,
cancellationTokens,
toolIndex);
}
// ==========================================
// 分支2:开启了【同步转异步】配置 + 当前不是并行执行
// 把普通同步工具包装成异步工具,统一用异步执行
//
// 为什么必须是 !inParallelExecution?
// 因为并行模式外层已经有并发调度了,再包装会导致线程耗尽/死锁
// ==========================================
else if (wrapSyncToolsAsAsync && !inParallelExecution) {
// 获取工具执行专用线程池
Executor executor = getToolExecutor(config);
// 把 同步工具 → 包装成 异步工具(统一执行模型)
AsyncToolCallback wrappedAsync = AsyncToolCallbackAdapter.wrapIfNeeded(
toolCallback,
executor,
toolExecutionTimeout);
// 执行包装后的异步工具
return executeAsyncTool(
wrappedAsync,
request,
toolContextMap,
config,
extraStateFromToolCall,
cancellationTokens,
toolIndex);
}
// ==========================================
// 分支3:普通【同步工具】执行
// 不包装、不异步,直接同步调用
// ==========================================
else {
return executeSyncTool(
toolCallback,
request,
toolContextMap,
config);
}
}
最终,调用工具对象处理执行结果、异常,并返回标准化的工具响应:
java
/**
* 同步执行工具调用的核心方法
* 负责调用具体的工具回调对象,处理执行结果、异常,并返回标准化的工具响应
*
* @param callback 工具回调对象(Spring AI 标准工具执行器)
* @param request 工具调用请求对象,包含工具名、参数、调用ID等
* @param toolContextMap 工具执行上下文参数(会话信息、用户信息等)
* @param config 运行时配置(包含线程ID、Agent配置等)
* @return 工具调用的标准化响应对象(成功/失败结果)
*/
private ToolCallResponse executeSyncTool(ToolCallback callback, ToolCallRequest request,
Map<String, Object> toolContextMap, RunnableConfig config) {
// 1. 构建工具执行上下文,封装传入的上下文参数
ToolContext context = new ToolContext(toolContextMap);
try {
// 2. 核心:调用工具的执行方法,传入JSON格式的参数和上下文,获取执行结果
String result = callback.call(request.getArguments(), context);
// 3. 判断是否开启Agent执行日志,打印工具执行成功日志
if (enableActingLog) {
// 打印基础执行日志:线程ID、Agent名称、执行完成的工具名
logger.info("[ThreadId {}] Agent {} acting, tool {} finished",
config.threadId().orElse(THREAD_ID_DEFAULT), agentName, request.getToolName());
// Debug级别:打印工具返回的详细结果
if (logger.isDebugEnabled()) {
logger.debug("Tool {} returned: {}", request.getToolName(), result);
}
}
// 4. 构建并返回【成功】的工具调用响应
return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), result);
}
catch (ToolExecutionException e) {
// 5. 捕获【工具执行专属异常】:使用自定义异常处理器处理业务异常
logger.error("Tool {} execution failed, handling with processor: {}", request.getToolName(),
toolExecutionExceptionProcessor.getClass().getName(), e);
// 调用异常处理器生成友好的错误结果
String result = toolExecutionExceptionProcessor.process(e);
// 返回处理后的异常响应
return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), result);
}
catch (Exception e) {
// 6. 捕获【所有未知/系统异常】:通用异常处理
logger.error("Tool {} execution failed: {}", request.getToolName(), e.getMessage(), e);
// 构建并返回【错误】的工具调用响应(自动封装异常信息)
return ToolCallResponse.error(request.getToolCallId(), request.getToolName(), e);
}
}