源码阅读:Spring AI 框架是如何进行工具调用以及循环调用的过程

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

环境

示例代码

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

源码讲解

ChatClient 接口

  1. call 方法属于 ChatClientChatClientRequestSpec 接口,返回类型为 CallResponseSpec

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

DefaultChatClient 实现类 call 方法

Java 复制代码
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() 时,发生了以下事情:

Java 复制代码
@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)设计模式
拦截器详解

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

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

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

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


// 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() 返回给你。

发送请求逻辑详解
  • 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 解析: 负责 流程控制上下文维护
java 复制代码
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 方法、处理异常以及记录监控数据。
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填入对应标志位),那么就会把上下文拼接进去,再次发送请求,一次循环
相关推荐
桂花饼2 小时前
Sora-2 API 低成本接入指南:Python 实现 0.08 元/次的视频生成方案
人工智能·python·qwen3-next·nano banana pro·gemini-3-pro·sora2pro
AI周红伟2 小时前
周红伟老师 :企业级RAG+Agent+Skills+OpenClaw智能体内训方案大纲,企业六大智能体技术
人工智能
Liue612312312 小时前
YOLO11有效改进系列及项目实战目录_食品包装有效期检测_包含图像处理_目标检测等创新机制_以及_实际应用案例
图像处理·人工智能·目标检测
imbackneverdie2 小时前
从机制图、流程图到数据图,覆盖《Cell》《Nature》级期刊插图
图像处理·人工智能·ai·aigc·流程图·科研绘图
Lun3866buzha2 小时前
【城市建筑外墙材料识别】基于YOLO11-AIFI模型的建筑外墙材料智能识别与分类系统_1
人工智能·分类·数据挖掘
Katecat996632 小时前
【深度学习实战】:基于YOLOv8-RePVit的鱼眼图像分割任务详解(附完整代码实现)_1
人工智能·深度学习·yolo
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于Java的网上书店管理系统为例,包含答辩的问题和答案
java·开发语言
东东5162 小时前
ssm机场网上订票系统 +VUE
java·前端·javascript·vue.js·毕设