一:什么是Spring AI的ChatClient配置流程
博主先演示一遍博主怎么在项目中配置chatClient的
这里面我们调用chatclient就指定了advisor,prompt,等属性,最后调用call方法获取请求,通过chatResponse返回一次AI调用的具体返回内容
这一点博主其实也挺迷惑的,为什么这么设置就可以配置chatCLient了,它内部到底发生了什么,她所需要的各种参数又是啥,刚接触AI那会不太理解为什么我们,现在博主浅浅翻阅了一下源码(只能看懂一点点)+结合AI,算是理解了一点

我们每次构造chatclient,设置prompt(),system()等属性时都会返回一个
chatClientRequestSpec

我们点击
Ctrl 点击advisor时,进入内部会发现chatClientRequestSpec本质是一个接口,而advisor是他的一个接口方法,翻译一下它上面的注释:返回一个 ChatClient.Builder,用它可以创建一个 新的 ChatClient,这个新 Client 的所有默认设置,都是从 当前这个 ChatClientRequest 里复制过去的。 可以知道他是通过Builder构建器来构建chatclient的


通过
Ctrl H我们看到了chatCLientRequestSpec是有一个实现类的,chatclient也是有一个实现类的,说明我们上面通过lambda表达式创建的chatclient以及调用chatclient中的prompt()方法,advisor()方法.....,实际上是创建了chatClient中的默认实现类DefaultChatClient,advisor方法也是走的实现类DefalutChatClientRequestSpec中重写的实现方法

这里还能看到他们都是
DefaultChatCient的内部类,DefaultChatCient内部本身就管理了需要参数的设置,配置,提示词的注入,advisor注入等等


这里通过调试的方式,我们可以看见他们底层创建的就是我们所说的上面提到的实现类,这里的advisor创建的是
DefaultAdvisor,原因如下
我们调用的是这个方法注入advisor,他需要参数AdvisorSpec,封装在一个Consumer函数式接口
java
ChatClientRequestSpec advisors(Consumer<AdvisorSpec> consumer);
具体的
DefaultChatCient的内部中我们调用的advisor的重写方法如下,他手动创建了一个DefaultAdvisorSpec(AdvisorSpec的默认实现类,也是DefaultChatCient的内部类),然后调用了我们传递的Consumer的accept方法,去读取我们设置的值(如我们代码中的a->a.param(xxx)),将这些值放入一个新的DefaultAdvisorSpec(这一块不好理解,但是你自己写一下,然后翻阅一下源码加上问AI,应该能理解,大佬可以跳过)

然后调用this.advisorParams.putAll(advisorSpec.getParams());this.advisors.addAll(advisorSpec.getAdvisors());,

可以看见
advisors,advisorParams本质是DefaultChatCLientRequestSpec中的两个变量,将我们顾问设置的所有参数都传递到了这里。那么什么时候注册advisor呢?答案是当我们调用call方法时

我们先是构建了一个顾问执行器链
BaseAdvisorChain,然后将所有请求,顾问链啥的都仍给DefaultCallResponseSpec,自此chatClient装配阶段结束,这里需要说明一下我们的prompt,tool等其他参数好像是设置后就直接封装在
java
public CallResponseSpec call() {
// 步骤1: 构建 Advisor 链
BaseAdvisorChain advisorChain = buildAdvisorChain();
// 步骤2: 将当前 RequestSpec 转换为 ChatClientRequest
// 他会 将当前 RequestSpec 的所有参数(system、user、options、messages 等)转换为 ChatClientRequest 对象
ChatClientRequest request = DefaultChatClientUtils.toChatClientRequest(this);
// 步骤3: 创建响应规范,准备执行
return new DefaultCallResponseSpec(request, advisorChain,
this.observationRegistry, this.observationConvention);
}
这里我们解释一下为什么prompt会在
RequestSpec中


这里大家自行查看,博主解释一下,当我们调用prompt()方法并传入值,他会调用
public ChatClientRequestSpec prompt(Prompt prompt),返回一个接口类型ChatClientRequestSpec,而实际上他的prompt()方法本质是调用我们的实现类DefaultChatClientRequestSpec的重写的prompt()方法,将所有参数都封装在自身内部的变量中(如图上,包括提示词,顾问列表,tool工具)
再来解释一下我们顾问执行链的构建buildAdvisorChain

java
// 在链的末端添加模型调用顾问。
// 它们在顾问链中扮演最后一个角色。
this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());
准备"终端"顾问 (Terminal Advisors)
- 目的 :这两行代码将两个最核心的
Advisor(顾问)添加到处理链的末端。它们是真正与 AI 模型通信的执行者。 ChatModelCallAdvisor:负责处理非流式的 AI 调用,即一次性获取完整的回复。ChatModelStreamAdvisor:负责处理流式的 AI 调用,即逐步获取回复内容。this.chatModel:这是 Spring AI 对不同大模型(如 OpenAI, Ollama 等)的统一抽象接口,具体的模型调用逻辑由它实现。
构建并返回责任链
java
return DefaultAroundAdvisorChain.builder(this.observationRegistry) .pushAll(this.advisors) .templateRenderer(this.templateRenderer) .build();
- 目的:将所有配置好的 Advisor(包括上面添加的两个,以及可能由用户自定义添加的其他 Advisor)组装成一个完整的、可执行的链条。
- DefaultAroundAdvisorChain.builder(...) :创建一个责任链的构建器。
- .pushAll(this.advisors) 将所有 Advisor 实例按顺序压入链中。这个顺序至关重要,它决定了请求和响应被处理的先后次序。
- .templateRenderer(...):设置模板渲染器,用于处理提示词模板。
- .build():最终构建出 BaseAdvisorChain 实例。
最后解释一下什么时候调用AI,我们上方调用
call返回一个CallResponseSpec,此时我们的chatclient完全装配好了,下一步就是调用AI了,那么什么时候触发呢

这是
CallResponseSpec内部,可以看见他是一个接口,内部有很多方法,其中调用他的chatResponse和content方法就会调用AI生成结果,content是获取响应内容,chatResponse是获取一次完整的AI相应结果,内部包含Token消耗,响应结果等更加详细的参数。这里可以发现他是一个接口,那么自然我们要去看他的接口实现类

巧了,他的实现类也是chatclient实现类的内部类,原来我们的chatclient已经封装好了我们所需要的一切内容,chatclient也太省心了(゚∀゚)

可以看见这里面封装了所有的调用call方法传递的参数

这一段是是他重写的我们上面提交的方法。可以看见content方法本质也是先获取chatResponse,然后手动帮我们提取出内部的content

核心还是
doGetObservableChatClientResponse这个方法,源码扒到这里,后面其实博主就不太能看懂了,能力有限,大家可以自己去自己尝试扒一下,这里解释引用一下AI的解释
这段代码是 Spring AI 中真正 "扣动扳机" 的地方。
当你调用 .content() 或 .chatResponse() 时,最终都会汇聚到这个 doGetObservableChatClientResponse 方法。
它的核心作用有两个:
- 包裹监控(Observation) :利用 Micrometer 框架记录这次 AI 调用的指标(如耗时、Token 消耗等)。
- 触发执行 :通过
this.advisorChain.nextCall(...)真正启动责任链,最终调用 AI 模型。
下面我为你逐段解析这段源码的逻辑: . 处理输出格式上下文
1. 处理输出格式上下文
java
if (outputFormat != null) {
chatClientRequest.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(), outputFormat);
}
- 作用:如果你使用了结构化输出(比如 .entity(User.class)),这里会将目标格式存入请求的上下文(Context)中。
- 目的:让后续的拦截器(Advisor)知道你需要什么格式的数据,以便进行相应的转换。
. 2.构建监控上下文 (Observation Context)
java
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
.request(chatClientRequest)
.advisors(this.advisorChain.getCallAdvisors())
.stream(false) // 标记这不是流式调用
.format(outputFormat)
.build();
- 作用:创建一个容器,用来存放这次调用相关的所有元数据(请求对象、拦截器链、是否流式等)。
- 目的:这是为了配合 Spring 的 Observability(可观测性)系统,方便后续生成 Trace ID 或统计指标。
3.创建监控实例 (Observation)
java
var observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(
this.observationConvention,
DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION,
() -> observationContext,
this.observationRegistry
);
- 作用:从注册中心获取一个 Observation 实例。
- 通俗理解:这就像是在开始工作前,先拿出一个"秒表"和"记录本",准备记录这次任务的开始时间、结束时间和状态。
4.核心执行:启动责任链 这是最关键的部分:
java
var chatClientResponse = observation.observe(() -> {
// Apply the advisor chain that terminates with the ChatModelCallAdvisor.
return this.advisorChain.nextCall(chatClientRequest);
});
observation.observe(...):
-
observation.observe(...):- 它会先启动计时。
- 然后执行大括号
{}里的代码。 - 如果代码执行成功或抛出异常,它会记录结果并停止计时。
-
this.advisorChain.nextCall(chatClientRequest):- 这就是真正的触发点!
- 它调用了责任链的入口。请求会像接力棒一样,从第一个 Advisor 传到最后一个。
- 最终,链条末端的
ChatModelCallAdvisor会收到请求,并调用底层的ChatModel(如 OpenAI 的 API),拿到结果后原路返回
5.返回结果
java
return chatClientResponse != null ? chatClientResponse : ChatClientResponse.builder().build();
- 作用:做一个非空检查,确保即使出现意外情况,也返回一个空的对象而不是 null,防止空指针异常。
(2)其他一些博主疑惑的解释
为什么它能通过.prompt(),.advisor()都形式封配置多个chatclient参数
核心还是DefaultChatCLientRequestSpec,他是DefaultChatClient的静态内部类,我们不论设置prompt(),advisor(),还是别的啥,我们最终都是往这个类中设置,比如我们设置prompt时,他先拿到我们的DefaultChatCLientRequestSpec引用,将prompt设置到它内部的变量属性中,设置完后返回这个DefaultChatCLientRequestSpec引用,后续的设置都是继续拿到这个引用,然后继续设置值,这就是"链式调用"


设置prompt时他是先获取到当前
DefaultChatClient的静态内部类DefaultChatCLientRequestSpec,然后往里面设置值,最后返回这个静态内部类都引用

这是静态内部类
DefaultChatCLientRequestSpec内部自身的设置advisor的方法,最后的return this也是返回他这个类的引用。this永远代表 当前方法所属的那个对象 这个方法写在哪个类里,this就是哪个类的实例!
(3)一些自己的思考点
通过浅读源码,博主发现我们的设置的prompt()等值,其实没必要强制返回引用
DefaultChatCLientRequestSpec,因为我们本质设置值时都将数据设置到这个静态内部类内部了,也没有怎么对外暴露,当然这只是博主的一些当前读取后的思考,也仅仅是浅度,对于他设置返回值返回引用,可能后面的代码中他的用处才显现出来。目前我觉得是官方特意设置的,简化我们的配置步骤
java
chatClient.prompt(prompt).advisors().call();
通过返回引用,可以支持链式调用,直接操作上一个对象返回的引用对象(本质传递的还是同一个对象引用)
java
// 第一步:调用 prompt,返回 spec ChatClientRequestSpec
spec = chatClient.prompt(prompt);
// 第二步:用 spec 调用 advisors,返回 spec
ChatClientRequestSpec spec2 = spec.advisors(...);
// 第三步:用 spec2 调用 call
CallResponseSpec callResult = spec2.call();
可以看见这种不返回引用的写法,确实有点麻烦,需要配置一个值,调用一次
ChatClientRequestSpec内部的对应的prompt(),advisor()方法设置值
(4)梳理的流程图
这是一部分chatclient的内部体系

这是用户通过
.advisor(Consumer<AdvisorSpec> xx)形式设置顾问时的一些调用流程图

这是用户调用
.call方法时的调用流程图

这是用户调用
.chatResponse方法后的调用流程图

