Spring AI Alibaba 1.x 系列【10】ReactAgent 工具加载和执行流程

文章目录

  • [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. 概述

Toolsagents 调用来执行操作的组件。它们通过定义良好的输入和输出让模型与外部世界交互,从而扩展模型的能力。

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) 是否自动将同步工具包装为异步执行

此外,还支持从 HookModelInterceptor 中获取工具:

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 AIToolCallbacks.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:将工具设置到 Prompt
  • filterToolCallbacks:根据指定的工具名称过滤,仅保留名称匹配的工具回调
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);
	}
}
相关推荐
上海蓝色星球2 小时前
流程标准化・作业一体化|蓝色星球造价机器人,以全流程线上化破解造价咨询管理困局
大数据·人工智能
lee_curry2 小时前
JUC第一章 java中基础概念和CompletableFuture
java·多线程·并发·juc
ai大模型中转api测评2 小时前
2026年前端新工具:Gemini 3.1 SVG工作流从Prompt到部署
前端·人工智能·prompt·api
marteker2 小时前
哈雷戴维森在推出增长战略前重塑品牌形象
大数据·人工智能
X.Ming 同学2 小时前
AI时代工程师的Superpowers进化论
人工智能
极光代码工作室2 小时前
基于机器学习的信用卡欺诈检测系统设计
人工智能·python·深度学习·机器学习
quetalangtaosha2 小时前
Anomaly Detection系列(CVPR2025 EG-MPC论文解读)
人工智能·深度学习·计算机视觉
前端不太难2 小时前
鸿蒙游戏 Store 设计(AI + 多端)
人工智能·游戏·harmonyos
未来智慧谷2 小时前
Claude Mythos技术解析:97.6%漏洞利用率意味着什么?AI安全红线在哪里?
人工智能·anthropic·claude mythos