本篇博客着重讲解 Spring AI 下是如何进行工具调用以及循环调用的过程

环境

示例代码

String response = chatClient.prompt()  
        .system("当前操作环境 os: windows, 确保相关工具调用符合windows的命令")  
        .user(message)  
        .call()  
        .content();
  • 其中 call 即是调用大模型的入口

源码讲解

ChatClient 接口

在这里插入图片描述

  1. call 方法属于 ChatClientChatClientRequestSpec 接口,返回类型为 CallResponseSpec
    在这里插入图片描述

  2. CallResponseSpec 用于获取返回的结果,可以返回的对象如上图

DefaultChatClient 实现类 call 方法

在这里插入图片描述

private BaseAdvisorChain buildAdvisorChain() {
    // 1. 在栈底添加模型调用 Advisor
    this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
    this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());

    // 2. 构建完整的调用链
    return DefaultAroundAdvisorChain.builder(this.observationRegistry)
       .observationConvention(this.advisorObservationConvention)
       .pushAll(this.advisors) // 将用户自定义的 Advisor 和底层的 ModelAdvisor 全部压入栈
       .build();
}
  1. buildAdvisorChain():构建拦截器链
    Spring AI 使用 Advisor 模式来处理横切关注点(如:对话记忆、RAG 增强、日志打印等)。
  • Terminal Advisors(终端顾问): 代码中手动添加了 ChatModelCallAdvisor。它在链的最末端,负责真正调用 LLM(如 OpenAI, Ollama)。

  • 责任链模式: this.advisors 中包含了你在使用 ChatClient 时通过 .advisors(...) 添加的所有组件(比如 MessageChatMemoryAdvisor)。

  • 执行顺序: 当请求开始时,它会按顺序经过:用户自定义 Advisor A -> 用户自定义 Advisor B -> … -> ChatModelCallAdvisor(真正发请求)。响应返回时,则反向穿过。

  1. call() 方法:准备执行规格

当你链式调用的最后写下 .call() 时,发生了以下事情:

@Override
public CallResponseSpec call() {
    // 1. 准备好上面提到的拦截器链
    BaseAdvisorChain advisorChain = buildAdvisorChain();
    
    // 2. 将当前 builder (this) 中所有的参数(Prompt, Options, Tools等)
    // 转换成一个不可变的 ChatClientRequest 对象
    ChatClientRequest request = DefaultChatClientUtils.toChatClientRequest(this);

    // 3. 返回一个 DefaultCallResponseSpec 实例
    return new DefaultCallResponseSpec(request, advisorChain,
          this.observationRegistry, this.chatClientObservationConvention);
}

[!QUESTION] 为什么要返回 CallResponseSpec

call() 并没有立即发送网络请求。它返回的是一个“结果处理器”。 这样设计的目的是为了支持多种返回格式。你可以接着调用:

  • .content():只想要字符串回复。

  • .entity(MyBean.class):想要自动把 JSON 转成 Java 对象(结构化输出)。

  • .chatResponse():想要完整的元数据(包含 Token 消耗等)。

请求发送

在这里插入图片描述

  • .content为例,最后会调用 doGetObservableChatClientResponse ,然后再经过一些可观测的相关设置,就会进入nextCall的调用,此时就会根据 advisorChain 的这个链条来进行调用,类似于 AOP拦截器链(Advisor Chain)设计模式
拦截器详解

![[Pasted image 20260213170609.png]]

在这里插入图片描述

this.advisorChain 是一个包含多个 CallAdvisor 的链条。

  • 在请求真正发给大模型之前,它会按顺序经过你配置的各种 Advisor。

  • 例如:如果你配置了记忆功能,这里会经过 MessageChatMemoryAdvisor 将历史消息塞进请求里;如果配置了日志或安全校验,也会在这里执行。

  • 每执行完一个 Advisor 的 前置逻辑 adviseCall,就会调用 nextCall 进入下一个节点。

在这里插入图片描述
在这里插入图片描述

![[Pasted image 20260213170928.png]]

![[Pasted image 20260213171000.png]]

// Apply the advisor chain that terminates with the ChatModelCallAdvisor.

  • 这条拦截器链的最后一个节点(Terminal Node) 是系统默认添加的 ChatModelCallAdvisor

  • 当调用链传递到这里时,ChatModelCallAdvisor 的主要职责是将前面所有 Advisor 处理完毕的 ChatClientRequest(包含系统提示词、用户消息、历史消息、函数调用配置等)转换为标准的 Prompt 对象

  • 然后,它会调用底层接口:ChatModel.call(Prompt)

  • 因为在项目中引入并配置的是 OpenAI 的 Starter,所以此时实现 ChatModel 接口的 Bean 就是 OpenAiChatModel。 在 OpenAiChatModel 内部,流程如下:

    • 模型适配:将 Spring AI 通用的 Prompt 对象,转换为 OpenAI 原生 API 所需要的请求体(例如 OpenAiApi.ChatCompletionRequest)。

    • 发起请求:通过底层的 OpenAiApi 类,向 api.openai.com 发起真实的 HTTP POST 请求。

    • 解析响应:收到 OpenAI 的 JSON 响应后,再反向解析包装回 Spring AI 的通用 ChatResponse 对象。

    • 原路返回ChatResponse 沿着之前的 Advisor 链原路返回(触发后置逻辑,如保存新消息到 Memory),最后回到 DefaultChatClient 被提取为 .content() 返回给你。

发送请求逻辑详解

![[Pasted image 20260213172005.png]]

  • createRequest : 创建请求体
  • this.openAiApi.chatCompletionEntity: 发送请求
  • 对于返回的对象进行判空验证
  • List<Generation> generations = choices.stream().map(...):将原始响应转换为 Spring AI 的 Generation 对象
提取工具参数

在这里插入图片描述

  • toolCall.function().arguments()这里获得了AI 生成的 JSON 参数字符串,但是此处还是 String。

结果判断与执行

在这里插入图片描述

  • isToolExecutionRequired: 判断 AI 是否发起了有效的工具调用请求
  • executeToolCalls: 解析工具参数、调用工具,返回结果

[!IMPORTANT] 此时的分支即表示出了如果没用工具调用那么直接返回response,如果有工具调用那么判断工具调用的结果是否是 returnDirect(),如果还需要继续调用那么就会继续调用 internalCall 从而达到循环的目的

工具执行

在这里插入图片描述
在这里插入图片描述

  • executeToolCalls 解析: 负责 流程控制上下文维护
public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) {
    // 1. 安全检查:确保有入参
    Assert.notNull(prompt, "prompt cannot be null");
    Assert.notNull(chatResponse, "chatResponse cannot be null");

    // 2. 检查 AI 的回复里是否真的包含“调用工具”的指令
    // (chatResponse 可能只是一段普通文本,那样就不需要执行工具了)
    Optional<Generation> toolCallGeneration = chatResponse.getResults().stream()
        .filter((g) -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
        .findFirst();

    if (toolCallGeneration.isEmpty()) {
        throw new IllegalStateException("No tool call requested...");
    } else {
        // 3. 提取 AI 的指令 (AssistantMessage)
        AssistantMessage assistantMessage = ((Generation)toolCallGeneration.get()).getOutput();
        
        // 4. 构建上下文 (ToolContext),可能包含用户 ID、会话 ID 等元数据
        ToolContext toolContext = buildToolContext(prompt, assistantMessage);
        
        // 5. 【关键】调用内部方法,真正去执行工具逻辑
        InternalToolExecutionResult internalResult = this.executeToolCall(prompt, assistantMessage, toolContext);
        
        // 6. 更新对话历史
        // 把 "AI 说要调用的指令" 和 "工具执行完的结果" 都追加到历史记录里
        // 这样下次发给 AI 时,AI 才知道"哦,我刚才调用了 X,结果是 Y"
        List<Message> conversationHistory = this.buildConversationHistoryAfterToolExecution(
            prompt.getInstructions(), assistantMessage, internalResult.toolResponseMessage());
        
        // 7. 打包返回结果
        return ToolExecutionResult.builder()
            .conversationHistory(conversationHistory)
            .returnDirect(internalResult.returnDirect())
            .build();
    }
}

在这里插入图片描述

在这里插入图片描述

  • executeToolCall解析:负责解析 JSON 参数、反射调用 Java 方法、处理异常以及记录监控数据。
private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage, ToolContext toolContext) {
    // 1. 准备工具列表
    // 从 prompt 的 options 中获取这次对话允许使用的工具回调 (ToolCallback)
    List<ToolCallback> toolCallbacks = ...; 

    List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList();

    // 2. 遍历 AI 请求的所有工具
    // (注意:AI 可能一次请求调用多个工具,比如同时查询北京和上海的天气)
    for(AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
        
        String toolName = toolCall.name();
        String toolInputArguments = toolCall.arguments(); // 这是 AI 生成的 JSON 字符串,例如 "{\"city\": \"Beijing\"}"

        // 3. 参数容错处理
        // 如果 AI 没给参数,默认给一个空 JSON 对象 "{}",防止解析报错
        if (!StringUtils.hasText(toolInputArguments)) {
            finalToolInputArguments = "{}";
        } else {
            finalToolInputArguments = toolInputArguments;
        }

        // 4. 【核心】寻找匹配的 Java 工具 (ToolCallback)
        // 先在 options 里找,找不到再去全局的 toolCallbackResolver 里找 (比如 Bean 容器)
        ToolCallback toolCallback = (ToolCallback)toolCallbacks.stream()
            .filter((tool) -> toolName.equals(tool.getToolDefinition().name()))
            .findFirst()
            .orElseGet(() -> this.toolCallbackResolver.resolve(toolName));

        if (toolCallback == null) {
            // 找不到工具就报错,提示可能是名字被截断或者前缀问题
            throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
        }
		// 4.1填入是否需要 直接返回 的标志位
		if (returnDirect == null) {  
		    returnDirect = toolCallback.getToolMetadata().returnDirect();  
		}  
		else {  
		    returnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect();  
		}
        // 5. 开启监控 (Observation)
        // 这里集成了 Micrometer,可以监控工具调用的耗时、成功率等
        ToolCallingObservationContext observationContext = ...;
        
        String toolCallResult = (String)ToolCallingObservationDocumentation.TOOL_CALL
            .observation(...)
            .observe(() -> {
                try {
                    // 6. 【真正执行】
                    // toolCallback.call() 内部会做两件事:
                    //   a. 把 JSON 字符串反序列化成 Java 对象 (比如 WeatherRequest)
                    //   b. 调用你的 Java 方法 (getWeather)
                    //   c. 把你的返回值序列化成 String
                    return toolCallback.call(finalToolInputArguments, toolContext);
                } catch (ToolExecutionException ex) {
                    // 7. 异常处理
                    // 如果你的 Java 代码报错了,这里会将异常信息转化为 AI 能看懂的错误文本
                    return this.toolExecutionExceptionProcessor.process(ex);
                }
            });

        // 8. 将单个工具的结果存入列表
        toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolCallResult));
    }

    // 9. 返回结果
    // 包含所有工具的执行结果,以及 returnDirect 标志
    return new InternalToolExecutionResult(
        ToolResponseMessage.builder().responses(toolResponses).build(), 
        ...
    );
}

关键点总结

  • JSON 解析发生的时机: 在这个方法里,finalToolInputArguments 仍然是 String (JSON)。真正的解析发生在 toolCallback.call(finalToolInputArguments) 内部。ToolCallback 知道参数的 Class 类型,它会用 Jackson 把字符串转成 Java 对象。
  • 容错与监控
    • 容错:处理了参数为空的情况,也处理了 ToolExecutionException(把异常栈变成文本给 AI 看,让 AI 知道出错了)。
    • 监控:通过 Observation 接口,所有的工具执行都被埋点监控了。
  • 查找机制: 代码先在 Prompt 的 Options 里找,找不到再去 Resolver (Spring Context) 里找。这允许你既可以使用全局注册的 Bean 工具,也可以在某次特定对话中临时传入一个动态工具。
  • 循环调用:如果工具返回的结果是需要继续调用(executeToolCall4.1 部分通过callback填入对应标志位),那么就会把上下文拼接进去,再次发送请求,一次循环
Logo

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

更多推荐