Spring AI Tool Calling源码浅析析

Spring AI Tool Calling源码浅析析工具相关的组件工具执行流程关注点0:Spring AI 工具的自动配置关注点1: Spring AI如何把定义的工具告知模型关注点2:Spring AI如何知道模型决定调用工具关注点3:Spring AI如何执行工具的调用参考文档

工具相关的组件

  • ToolDefinition(工具的定义):包含工具名称,描述、输入的Schema

  • ToolMetadata(工具元数据):目前只有控制是否将工具返回的结果直接返回(returnDirect),不在走模型响应

  • ToolCallback(工具回调):工具的的回调处理,即我们的业务逻辑

    • MethodToolCallback:方法调用,通过反射实现

    • FunctionToolCallback:Function.apply方式实现

  • ToolCallbackProvider(工具回调提供者):Spring AI 会自动扫描 Spring 上下文中定义的 ToolCallback 类型的 Bean,并将它们组合成 Provider

    • StaticToolCallbackProvider:直接对外提供已构建好的ToolCallback

    • MethodToolCallbackProvider:扫描带有Tool注解的方法封装为MethodToolCallback,并对外提供

  • ToolCallbackResolver(工具回调):用于需要动态地、按需地查找工具回调

    • StaticToolCallbackResolver:通过ToolCallbackProvider获取的都会添加到StaticToolCallbackResolver

    • SpringBeanToolCallbackResolver:通过name从Spring的上下文动态的获取ToolCallback

    • DelegatingToolCallbackResolver:组合设计模式,组合StaticToolCallbackResolver和SpringBeanToolCallbackResolver对外提供统一的入口

  • ToolCallingManager(工具回调管理器):默认实现是DefaultToolCallingManager,核心功能如下

    • 解析工具定义(resolveToolDefinitions):从ToolCallingChatOptions中解析出工具定义,确保模型能正确识别和使用工具

    • 执行工具调用(executeToolCalls):根据模型响应,执行相应的工具调用,并返回工具的执行结果

    • 构建工具上下文(buildToolContext):为工具调用提供上下文信息,历史的Message记录

    • 管理工具回调:通过 ToolCallbackResolver 解析工具回调,支持动态工具调用

  • ToolCallResultConverter(工具结果转换器):用于把工具返回的结果转换为字符串

  • ToolContext(工具上下文):被构建于工具回调管理器

    • 通过getContext方法获取整个上下文,通过getToolCallHistory方法获取Message的历史记录

工具执行流程

框架控制的工具执行生命周期

  1. 当我们想让模型使用某个工具时,会在聊天请求中包含其工具定义(提示)并调用聊天模型API 向 AI 模型发送请求。

  2. 当模型决定调用工具时,会发送响应(聊天回应工具名称和输入参数均依据定义的模式建模。

  3. 聊天模型将工具调用请求发送给ToolCallingManager应用程序接口。

  4. ToolCallingManager负责识别调用的工具,并以提供的输入参数执行。

  5. 工具调用的结果返回给ToolCallingManager.

  6. ToolCallingManager返回工具执行结果聊天模型.

  7. 聊天模型将工具执行结果返回给 AI 模型(工具响应信息).

  8. AI模型利用工具调用结果作为额外上下文生成最终响应,并将其发送给呼叫者(聊天回应)通过ChatClient.

下面我们通过OpenAiChatModel来结合源码分析一下

关注点0:Spring AI 工具的自动配置

  • ToolCallingAutoConfiguration

    • SpringBeanToolCallbackResolver:通过name从Spring的上下文动态的获取ToolCallback

    • ToolCallbackProvider:通过工具回调提供者获取ToolCallback

 @Bean
   @ConditionalOnMissingBean
   ToolCallbackResolver toolCallbackResolver(
       GenericApplicationContext applicationContext, // @formatter:off
       List<ToolCallback> toolCallbacks,
       // Deprecated in favor of the tcbProviders. Kept for backward compatibility.
       ObjectProvider<List<ToolCallbackProvider>> tcbProviderList,
       ObjectProvider<ToolCallbackProvider> tcbProviders) { // @formatter:on
 ​
     List<ToolCallback> allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
 ​
     // Merge ToolCallbackProviders from both ObjectProviders.
     List<ToolCallbackProvider> totalToolCallbackProviders = new ArrayList<>(
         tcbProviderList.stream().flatMap(List::stream).toList());
     totalToolCallbackProviders.addAll(tcbProviders.stream().toList());
 ​
     // De-duplicate ToolCallbackProviders
     totalToolCallbackProviders = totalToolCallbackProviders.stream().distinct().toList();
 ​
     totalToolCallbackProviders.stream()
       .filter(pr -> !isMcpToolCallbackProvider(ResolvableType.forInstance(pr)))
       .map(pr -> List.of(pr.getToolCallbacks()))
       .forEach(allFunctionAndToolCallbacks::addAll);
 ​
     var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
 ​
     var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
       .applicationContext(applicationContext)
       .build();
 ​
     return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
   }

关注点1: Spring AI如何把定义的工具告知模型

  • 分为三步:

  1. 定义阶段:你定义了一个 Java Function<Request, Response>。

  • 当你将一个函数注册为 Bean 时(通常使用 @Bean 并配合 @Description 注解),SpringBeanToolCallbackResolver 会动态的发现并包装为 FunctionCallback 。

  • Tool注解的方法会经过MethodToolCallbackProvider处理为MethodToolCallback

 @Operation(summary = "流式聊天接口")
 @GetMapping(path = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
 public Flux<String> streamChat(HttpServletResponse response,
                                @RequestParam("userInput") String userInput,
                                @RequestParam("sessionId") Long sessionId) {
   Flux<String> answerFlux = chatClient.prompt().user(userInput)
     .advisors(a -> a.param(CONVERSATION_ID, sessionId))
     .toolNames("currentWeather")
     .tools(new BaiduTranslateTools(baiduTranslateProperties))
     .stream()
     .content();
 ​
   return answerFlux;
 }
 ​
 @Bean
 @Description("根据城市查询天气") // 描述会被发给 AI
 public Function<WeatherRequest, WeatherResponse> currentWeather() { ... }
  1. 解析阶段:Spring AI 使用反射分析 Request 类,生成 JSON Schema。

 {
   "type": "object",
   "properties": {
     "city": { "type": "string", "description": "城市名称" },
     "unit": { "type": "string", "enum": ["C", "F"] }
   },
   "required": ["city"]
 }
  1. 传输阶段:Spring AI 构造 HTTP 请求,将 Schema 放入 tools 数组发送给模型。

Spring AI 将上述 Schema 包装进 LLM(如 OpenAI)的标准 API 请求格式中。这个请求体(Payload)大致如下:

 {
   "model": "gpt-4",
   "messages": [
     { "role": "user", "content": "北京天气怎么样?" }
   ],
   // 关键在这里!Spring AI 自动填充了这个 tools 字段
   "tools": [
     {
       "type": "function",
       "function": {
         "name": "currentWeather", // 对应 Bean 的名字
         "description": "根据城市查询天气", // 对应 @Description
         "parameters": { 
            // 这里就是上面生成的 JSON Schema
            "type": "object",
            "properties": { ... } 
         }
       }
     }
   ]
 }
  • 相关的源码:org.springframework.ai.openai.OpenAiChatModel#call

   @Override
   public ChatResponse call(Prompt prompt) {
     Prompt requestPrompt = buildRequestPrompt(prompt);
     return this.internalCall(requestPrompt, null);
   }
   
   public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {
     //构成请求:见关注点1
     ChatCompletionRequest request = createRequest(prompt, false);
 ​
     // 模型决策需要进行工具调用:见关注点2
     if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response != null
         && response.hasToolCalls()) {
       //执行工具调用:见关注点3
       var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
       if (toolExecutionResult.returnDirect()) {
         //直接返回工具的调用
         return ChatResponse.builder()
           .from(response)
           .generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
           .build();
       }
       else {
         //发送工具调用的结果再给到模型
         return this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
             response);
       }
     }
 ​
     return response;
   }
 ​
 ​
 ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
     request = ModelOptionsUtils.merge(requestOptions, request, ChatCompletionRequest.class);
 ​
     // 将工具定义添加到请求的 tools 参数中。
     List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
     if (!CollectionUtils.isEmpty(toolDefinitions)) {
       request = ModelOptionsUtils.merge(
           OpenAiChatOptions.builder().tools(this.getFunctionTools(toolDefinitions)).build(), request,
           ChatCompletionRequest.class);
     }
     return request;
   }
 ​

关注点2:Spring AI如何知道模型决定调用工具

  • ToolCallback 的 call() 方法是在 LLM(大模型)分析了你的 Prompt,决定需要调用工具,并返回了工具调用请求(Tool Call Request)之后 执行的。

  • 模型决策:LLM 接收到 Prompt,发现需要调用工具(例如 currentWeather),于是返回一个特殊的响应,由于此时 finishReason 是 tool_calls(或 function_call),而不是普通的文本。

关注点3:Spring AI如何执行工具的调用

  • 框架拦截:Spring AI 的 ChatModel(如 OpenAiChatModel)检测到这个响应包含工具调用指令

  • 相关的源码:

 @Override
   public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) {
     Optional<Generation> toolCallGeneration = chatResponse.getResults()
       .stream()
       .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
       .findFirst();
     AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
 ​
     ToolContext toolContext = buildToolContext(prompt, assistantMessage);
 //执行工具调用
     InternalToolExecutionResult internalToolExecutionResult = executeToolCall(prompt, assistantMessage,
         toolContext);
 ​
     List<Message> conversationHistory = buildConversationHistoryAfterToolExecution(prompt.getInstructions(),
         assistantMessage, internalToolExecutionResult.toolResponseMessage());
 ​
     return ToolExecutionResult.builder()
       .conversationHistory(conversationHistory)
       .returnDirect(internalToolExecutionResult.returnDirect())
       .build();
   }
 ​
 ​
 private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage,
       ToolContext toolContext) {
       String toolResult;
       try {
         //如果是FunctionToolCallback执行apply  MethodToolCallback执行对应的方法
         toolResult = toolCallback.call(toolInputArguments, toolContext);
       }
       catch (ToolExecutionException ex) {
         toolResult = toolExecutionExceptionProcessor.process(ex);
       }
 ​
       toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));
     }
     return new InternalToolExecutionResult(new ToolResponseMessage(toolResponses, Map.of()), returnDirect);
   }

参考文档

Spring AI/工具调用

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐