本篇博客着重讲解 Spring AI 下是如何进行工具调用以及循环调用的过程
环境
- springAI 2.0.0-m2
- jdk 21
- openAiChatModel 作为 ai model
- skill 工具为示例,相关内容参考上篇博客 Spring AI 下的 skill demo实现
示例代码
java
String response = chatClient.prompt()
.system("当前操作环境 os: windows, 确保相关工具调用符合windows的命令")
.user(message)
.call()
.content();
- 其中
call即是调用大模型的入口
源码讲解
ChatClient 接口

-
call方法属于ChatClient的ChatClientRequestSpec接口,返回类型为CallResponseSpec

-
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();
}
buildAdvisorChain():构建拦截器链
Spring AI 使用 Advisor 模式来处理横切关注点(如:对话记忆、RAG 增强、日志打印等)。
-
Terminal Advisors(终端顾问): 代码中手动添加了
ChatModelCallAdvisor。它在链的最末端,负责真正调用 LLM(如 OpenAI, Ollama)。 -
责任链模式:
this.advisors中包含了你在使用ChatClient时通过.advisors(...)添加的所有组件(比如MessageChatMemoryAdvisor)。 -
执行顺序: 当请求开始时,它会按顺序经过:
用户自定义 Advisor A->用户自定义 Advisor B-> ... ->ChatModelCallAdvisor(真正发请求)。响应返回时,则反向穿过。
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 工具,也可以在某次特定对话中临时传入一个动态工具。 - 循环调用 :如果工具返回的结果是需要继续调用(
executeToolCall的 4.1 部分通过callback填入对应标志位),那么就会把上下文拼接进去,再次发送请求,一次循环