聊天客户端API
约 13530 字大约 45 分钟
2026-09-15
合集说明
本篇合并自参考文档「Chat Client API」章节(含 Advisors 子章节),按官方文档顺序排列:(各篇之间以 --- 分隔)
- Chat Client API(聊天客户端 API)
- Advisors(顾问)
- Recursive Advisors(递归顾问)
Chat Client API(聊天客户端 API)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/chatclient.html 📌 原文版本:Java AI Reference v2.0.1(Stable 稳定版)
概述
ChatClient(聊天客户端,Java AI 中与 AI 模型交互的核心入口类)提供了一套 Fluent API(流式链式调用 API,即通过连续"点方法"一步步把请求组装出来的编程风格),用来和 AI 模型通信。它同时支持同步和流式两种编程模型。
关于在 ChatClient 中同时混用命令式和响应式编程模型的相关说明,请参阅本文档底部的"实现说明"部分。
这套 Fluent API 提供了一系列方法,用来组装 Prompt(提示词,即传给 AI 模型的输入)的各个组成部分。Prompt 中包含了指导 AI 模型输出内容和行为方式的指令文本。从 API 的角度看,Prompt 由一组消息构成。
AI 模型主要处理两类消息:用户消息——由用户直接输入的内容;系统消息——由系统生成、用来引导整个对话走向的指令。
这些消息中通常含有"占位符",运行时会根据用户输入进行替换,从而让 AI 模型的响应贴合用户的实际请求。
此外还可以指定一些 Prompt 选项,比如使用哪个 AI 模型、以及 temperature(温度参数,控制输出随机性/创造力的设置)等。
白话解读
可以把 ChatClient 理解成 Spring 里调 HTTP 接口的 RestClient/WebClient 的"AI 版":RestClient 面向 HTTP 请求,ChatClient 面向 AI 模型。prompt() 起手,user() 塞问题,call() 发请求,content() 拿结果,一路链下去就行。
创建 ChatClient(Creating a ChatClient)
ChatClient 通过 ChatClient.Builder(构建器)对象创建。你可以直接使用 Spring Boot 为任何 ChatModel(聊天模型,真正与 AI 服务通信的底层模型抽象)自动配置好的 ChatClient.Builder 实例注入进来,也可以自己动手编程式创建一个。
使用自动配置的 ChatClient.Builder
在最简单的场景下,Java AI 的 Spring Boot 自动配置会为你创建一个原型作用域的 ChatClient.Builder Bean,直接注入到你的类里就能用。下面是一个简单示例:接收一个简单的用户请求,返回 String 类型的响应。
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
// 注入自动配置的 Builder,构建 ChatClient 实例
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/ai")
String generation(String userInput) {
// prompt() 进入 Fluent API,user() 设置用户消息内容,
// call() 同步调用 AI 模型,content() 以 String 形式返回响应
return this.chatClient.prompt()
.user(userInput)
.call()
.content();
}
}在这个简单示例中,用户输入成为用户消息的内容。call() 方法向 AI 模型发送请求,content() 方法则把 AI 模型的响应以 String 形式返回。
多 ChatModel 协同工作(Working with Multiple Chat Models)
有不少场景会让你需要在同一个应用里同时使用多个聊天模型,比如:
- 为不同类型的任务使用不同的模型(例如:复杂推理用强大的模型,简单任务用更快、更便宜的模型)
- 实现故障回退机制(一个模型服务不可用时切换到另一个)
- 对不同模型或配置做 A/B 测试
- 让用户根据自己的偏好选择模型
- 组合各有所长的专用模型(一个专门生成代码,另一个专门写创意内容等)
默认情况下,Java AI 只会自动配置一个 ChatClient.Builder Bean。但你的应用可能确实需要多个聊天模型,下面讲讲怎么处理这些场景。
单一模型类型下的多个 ChatClient
这一节讲的是一种常见用法:多个 ChatClient 实例底层都用同一种模型类型,只是各自配置不同。由于自动配置的 ChatClient.Builder 是原型作用域——也就是说,每个注入点都会拿到一个全新的 Builder 实例,你可以直接放心使用:
@Configuration
class ChatClientConfig {
@Bean
ChatClient defaultChatClient(ChatClient.Builder builder) {
// 默认配置的 ChatClient
return builder.build();
}
@Bean
ChatClient customChatClient(ChatClient.Builder builder) {
// 带默认系统文本的 ChatClient
return builder.defaultSystem("You are a helpful assistant.").build();
}
}不同模型类型的 ChatClient
当你需要同时对接多个 AI 模型时,可能会想用 ChatClient.create(chatModel) 或 ChatClient.builder(chatModel) 来分别定义各自的 ChatClient Bean。但是——这么做会绕过自动配置的 ChatClient.Builder,意味着可观测性(observability,即链路追踪、指标采集等监控能力)和 ChatClientBuilderCustomizer(ChatClient 构建器定制器)Bean 都会被忽略掉。
注意
直接用 ChatClient.create(chatModel) 或 ChatClient.builder(chatModel) 会绕过自动配置的 Builder,导致观测能力与定制器全部失效!正确做法是注入 ChatClientBuilderConfigurer 来创建自定义构建器。
为了保留可观测性和定制器,你应该注入 ChatClientBuilderConfigurer(ChatClient 构建器配置器)来创建自定义 Builder。ChatClientBuilderConfigurer 会应用所有已注册的 ChatClientBuilderCustomizer Bean 并接好可观测性——这其实就是在复刻自动配置内部所做的事情。
当应用上下文中存在多个 ChatModel Bean 时,Spring 无法在没有歧义的情况下为自动配置的 ChatClient.Builder Bean 解析 ChatModel 依赖。要解决这个问题,可以把你的某个 ChatClient Bean 标记为 @Primary(首要 Bean,即出现多个候选时的默认选择)。如果你是手动定义这些 ChatModel Bean 的,可能还需要把其中一个 ChatModel Bean 也标记为 @Primary;或者干脆自己定义一个 ChatClient.Builder Bean 来覆盖自动配置的那个。
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ToolCallingAdvisor;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderConfigurer;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class ChatClientConfig {
@Bean
@Primary // 标记为首要 Bean,消除多 ChatClient 时的注入歧义
public ChatClient openAiChatClient(OpenAiChatModel chatModel, ChatClientBuilderConfigurer configurer,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
ObjectProvider<AdvisorObservationConvention> advisorObservationConvention,
ObjectProvider<ToolCallingAdvisor.Builder<?>> toolCallingAdvisorBuilder) {
return buildChatClient(chatModel, configurer, observationRegistry,
chatClientObservationConvention, advisorObservationConvention, toolCallingAdvisorBuilder);
}
@Bean
public ChatClient anthropicChatClient(AnthropicChatModel chatModel, ChatClientBuilderConfigurer configurer,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
ObjectProvider<AdvisorObservationConvention> advisorObservationConvention,
ObjectProvider<ToolCallingAdvisor.Builder<?>> toolCallingAdvisorBuilder) {
return buildChatClient(chatModel, configurer, observationRegistry,
chatClientObservationConvention, advisorObservationConvention, toolCallingAdvisorBuilder);
}
private ChatClient buildChatClient(ChatModel chatModel, ChatClientBuilderConfigurer configurer,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
ObjectProvider<AdvisorObservationConvention> advisorObservationConvention,
ObjectProvider<ToolCallingAdvisor.Builder<?>> toolCallingAdvisorBuilder) {
// 手动创建 Builder,并传入观测相关的组件
ChatClient.Builder builder = ChatClient.builder(chatModel,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
chatClientObservationConvention.getIfUnique(),
advisorObservationConvention.getIfUnique(),
toolCallingAdvisorBuilder.getIfAvailable());
// 通过 configurer 应用所有定制器并接好可观测性,与自动配置行为保持一致
return configurer.configure(builder).build();
}
}之后就可以用 @Qualifier(限定符注解,用于按 Bean 名称精确注入)把这些 Bean 注入到你的应用组件中:
@Configuration
public class ChatClientExample {
@Bean
CommandLineRunner cli(
@Qualifier("openAiChatClient") ChatClient openAiChatClient, // 按名字注入 OpenAI 客户端
@Qualifier("anthropicChatClient") ChatClient anthropicChatClient) { // 按名字注入 Anthropic 客户端
return args -> {
var scanner = new Scanner(System.in);
ChatClient chat;
// 模型选择
System.out.println("\nSelect your AI model:");
System.out.println("1. OpenAI");
System.out.println("2. Anthropic");
System.out.print("Enter your choice (1 or 2): ");
String choice = scanner.nextLine().trim();
if (choice.equals("1")) {
chat = openAiChatClient;
System.out.println("Using OpenAI model");
} else {
chat = anthropicChatClient;
System.out.println("Using Anthropic model");
}
// 使用选定的聊天客户端
System.out.print("\nEnter your question: ");
String input = scanner.nextLine();
String response = chat.prompt(input).call().content();
System.out.println("ASSISTANT: " + response);
scanner.close();
};
}
}多个 OpenAI 兼容 API 端点
你可以使用各自的 Builder 创建多个 OpenAiChatModel 实例,连接到不同的 OpenAI 兼容 API。当你需要同时对接多家提供商时,这招特别有用。
@Service
public class MultiModelService {
private static final Logger logger = LoggerFactory.getLogger(MultiModelService.class);
public void multiClientFlow() {
try {
// 为 Groq(Llama3)创建一个新的 OpenAiChatModel
OpenAiChatModel groqModel = OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder()
.baseUrl("https://api.groq.com/openai/v1")
.apiKey(System.getenv("GROQ_API_KEY"))
.model("llama3-70b-8192")
.temperature(0.5)
.build())
.build();
// 为 GPT-4 创建一个新的 OpenAiChatModel
OpenAiChatModel gpt4Model = OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder()
.baseUrl("https://api.openai.com")
.apiKey(System.getenv("OPENAI_API_KEY"))
.model("gpt-4")
.temperature(0.7)
.build())
.build();
// 同一个 prompt 分别发给两个模型
String prompt = "What is the capital of France?";
String groqResponse = ChatClient.builder(groqModel).build().prompt(prompt).call().content();
String gpt4Response = ChatClient.builder(gpt4Model).build().prompt(prompt).call().content();
logger.info("Groq (Llama3) response: {}", groqResponse);
logger.info("OpenAI GPT-4 response: {}", gpt4Response);
}
catch (Exception e) {
logger.error("Error in multi-client flow", e);
}
}
}ChatClient 的 Fluent API
ChatClient 的 Fluent API 允许你通过重载的 prompt 方法,以三种不同的方式发起链式调用:
prompt():这个无参方法让你从零开始使用 Fluent API,逐步组装 user、system 以及 Prompt 的其他组成部分。prompt(Prompt prompt):接受一个 Prompt 参数,传入一个你已经用 Prompt 的非 Fluent API 自己创建好的 Prompt 实例。prompt(String content):便捷方法,与上一个重载类似,直接传入用户的文本内容。
ChatClient 的响应
ChatClient API 提供了多种方式,通过 Fluent API 来格式化 AI 模型返回的响应。
返回 ChatResponse
AI 模型的响应是一个由 ChatResponse(聊天响应对象)类型定义的"富"结构。它包含响应如何生成的元数据,还可能包含多个响应结果——称为 Generations(生成结果),每个结果都带有自己的元数据。元数据中包括生成响应所消耗的 token(词元,模型处理文本的基本单位,1 个 token 约等于 3/4 个单词)数量。这个信息很重要,因为托管型 AI 模型都是按每次请求消耗的 token 数量来计费的。
下面这个例子演示了在 call() 之后调用 chatResponse(),来返回包含元数据的 ChatResponse 对象:
// 返回包含元数据的 ChatResponse 对象
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();返回实体
很多时候,你希望把返回的 String 映射成一个实体类。entity() 方法就是干这个的。
比如,有这样一个 Java record:
record ActorFilms(String actor, List<String> movies) {}你就可以像下面这样,用 entity() 方法把 AI 模型的输出映射到这个 record 上:
// 把模型输出直接映射为 ActorFilms 实体
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);还有一个重载的 entity 方法,签名为 entity(ParameterizedTypeReference<T> type),让你可以指定泛型 List 之类的类型:
// 使用 ParameterizedTypeReference 返回泛型集合
List<ActorFilms> actorFilms = chatClient.prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});可靠性开关:EntityParamSpec
所有 entity() 重载都接受一个可选的 Consumer<EntityParamSpec>,可以开启两个相互独立、可自由组合的行为:
validateSchema()—— 把 JSON 响应与实体 Schema(结构定义)进行校验,校验失败时自动带上错误反馈重试。useProviderStructuredOutput()—— 把 Schema 作为 API 层面的约束直接发给提供方(provider,即 AI 服务商),而不是以提示文本的方式塞进 Prompt。
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class, spec -> spec
.useProviderStructuredOutput() // 使用提供方原生的结构化输出能力
.validateSchema()); // 校验 Schema,失败时自动重试白话解读
结构化输出(Structured Output)就是"让 AI 别给我一段自由发挥的散文,而是按我给的 Java 类格式吐 JSON"。validateSchema() 相当于"验货+不合格自动退货重买";useProviderStructuredOutput() 则相当于"直接给服务商下死命令:必须按这个格式返回",比靠嘴上叮嘱(提示词)靠谱得多。详见 Structured Output(结构化输出) 章节。
流式响应
stream() 方法让你能拿到异步响应,如下所示:
// 以 Flux 流式获取模型生成的内容
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();你也可以用 chatResponse() 方法来流式获取 ChatResponse:Flux<ChatResponse>。
说明
未来我们会提供一个便捷方法,让你能通过响应式的 stream() 方法直接返回 Java 实体。在此之前,你应该使用 Structured Output Converter(结构化输出转换器)来显式转换聚合后的响应,如下所示。这个例子同时也演示了 Fluent API 中参数的用法——文档后面的章节会更详细地讨论。
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});
Flux<String> flux = this.chatClient.prompt()
.user(u -> u.text("""
Generate the filmography for a random actor.
{format}
""")
.param("format", this.converter.getFormat())) // 把输出格式说明注入模板占位符
.stream()
.content();
// 聚合流中的所有片段,拼接成完整内容
String content = this.flux.collectList().block().stream().collect(Collectors.joining());
// 用转换器把聚合后的内容转为实体列表
List<ActorsFilms> actorFilms = this.converter.convert(this.content);Prompt 模板
ChatClient 的 Fluent API 允许你把用户文本和系统文本写成带变量的模板,运行时变量会被替换掉。
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("Tell me the names of 5 movies whose soundtrack was composed by {composer}") // {composer} 是占位符
.param("composer", "John Williams")) // 运行时注入具体值
.call()
.content();内部实现上,ChatClient 使用 PromptTemplate 类来处理用户和系统文本,并依赖某个 TemplateRenderer(模板渲染器)实现,在运行时用提供的值替换变量。默认情况下,Java AI 使用 StTemplateRenderer 实现,它基于 Terence Parr 开发的开源 StringTemplate 引擎。
Java AI 还提供了 NoOpTemplateRenderer,适用于不需要任何模板处理的场景。
注意
直接在 ChatClient 上配置的 TemplateRenderer(通过 .templateRenderer())只作用于 ChatClient 构建链中直接定义的提示内容(比如通过 .user()、.system() 定义的内容),不会影响 Advisor(如 QuestionAnswerAdvisor)内部使用的模板——那些模板有自己单独的定制机制(参见"自定义 Advisor 模板")。
如果你想换一个模板引擎,可以直接给 ChatClient 提供一个自定义的 TemplateRenderer 接口实现。你也可以继续用默认的 StTemplateRenderer,只是给它做个自定义配置。
比如说,默认情况下模板变量是用 {} 语法标识的。如果你打算在 Prompt 里塞 JSON,那你可能得换一套定界符语法,免得和 JSON 自己的花括号打架。比如可以用 < 和 > 作为定界符:
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("Tell me the names of 5 movies whose soundtrack was composed by <composer>") // 使用 <composer> 占位符
.param("composer", "John Williams"))
.templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) // 自定义定界符
.call()
.content();call() 的返回值
在 ChatClient 上指定 call() 方法之后,响应类型有几种不同的选择。
String content():返回响应的 String 内容。ChatResponse chatResponse():返回 ChatResponse 对象,其中包含多个生成结果以及响应的元数据,比如创建响应时用了多少个 token。ChatClientResponse chatClientResponse():返回 ChatClientResponse 对象,其中包含 ChatResponse 对象和 ChatClient 的执行上下文,让你能访问 Advisor 执行过程中用到的额外数据(比如 RAG 流程中检索到的相关文档)。entity()返回 Java 类型:entity(ParameterizedTypeReference<T> type):用于返回实体类型的集合。entity(Class<T> type):用于返回指定的实体类型。entity(StructuredOutputConverter<T> structuredOutputConverter):用于指定一个 StructuredOutputConverter 实例,把 String 转换成实体类型。entity(ParameterizedTypeReference<T> type, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。entity(Class<T> type, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。entity(StructuredOutputConverter<T> converter, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。
responseEntity()同时返回 ChatResponse 和 Java 类型。当你既要拿到完整的 AI 模型响应(含元数据和生成结果),又要在一次调用中拿到结构化输出实体时,这个方法很有用。responseEntity(Class<T> type):返回一个 ResponseEntity,同时包含完整的 ChatResponse 对象和指定的实体类型。responseEntity(Class<T> type, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。responseEntity(ParameterizedTypeReference<T> type):返回一个 ResponseEntity,同时包含完整的 ChatResponse 对象和实体类型的集合。responseEntity(ParameterizedTypeReference<T> type, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。responseEntity(StructuredOutputConverter<T> structuredOutputConverter):返回一个 ResponseEntity,同时包含完整的 ChatResponse 对象和使用指定 StructuredOutputConverter 转换出的实体。responseEntity(StructuredOutputConverter<T> converter, Consumer<EntityParamSpec> spec):同上,带可选的 EntityParamSpec 配置。
你也可以调用 stream() 方法来替代 call()。
白话解读
调用 call() 本身并不会真正触发 AI 模型执行,它只是告诉 Java AI 这次用同步还是流式方式调用。真正的模型调用发生在你调用 content()、chatResponse()、responseEntity() 这些"取值"方法的时候。所以 call() 和 stream() 更像是"选择交通方式",而取值方法才是"上车出发"。
stream() 的返回值
在 ChatClient 上指定 stream() 方法之后,响应类型有几种不同的选择:
Flux<String> content():返回一个 Flux,流出 AI 模型正在生成的字符串内容。Flux<ChatResponse> chatResponse():返回 ChatResponse 的 Flux,其中包含响应的额外元数据。Flux<ChatClientResponse> chatClientResponse():返回 ChatClientResponse 的 Flux,其中包含 ChatResponse 对象和 ChatClient 的执行上下文,让你能访问 Advisor 执行过程中用到的额外数据(比如 RAG 流程中检索到的相关文档)。
消息元数据
ChatClient 支持给用户消息和系统消息添加元数据。元数据为消息提供了额外的上下文和信息,可以被 AI 模型或下游处理逻辑使用。
给用户消息添加元数据
你可以用 metadata() 方法给用户消息添加元数据:
// 逐个添加元数据键值对
String response = chatClient.prompt()
.user(u -> u.text("What's the weather like?")
.metadata("messageId", "msg-123")
.metadata("userId", "user-456")
.metadata("priority", "high"))
.call()
.content();
// 一次性传入多个元数据条目
Map<String, Object> userMetadata = Map.of(
"messageId", "msg-123",
"userId", "user-456",
"timestamp", System.currentTimeMillis()
);
String response = chatClient.prompt()
.user(u -> u.text("What's the weather like?")
.metadata(userMetadata))
.call()
.content();给系统消息添加元数据
类似地,你也可以给系统消息添加元数据:
// 给系统消息添加元数据
String response = chatClient.prompt()
.system(s -> s.text("You are a helpful assistant.")
.metadata("version", "1.0")
.metadata("model", "gpt-4"))
.user("Tell me a joke")
.call()
.content();默认元数据支持
你也可以在 ChatClient 的 Builder 层面配置默认元数据:
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
// 在 Builder 层面为系统/用户消息配置默认元数据
return builder
.defaultSystem(s -> s.text("You are a helpful assistant")
.metadata("assistantType", "general")
.metadata("version", "1.0"))
.defaultUser(u -> u.text("Default user context")
.metadata("sessionId", "default-session"))
.build();
}
}元数据校验
ChatClient 会对元数据做校验,以保证数据完整性:
- 元数据的 key 不能为 null 或空
- 元数据的 value 不能为 null
- 传入 Map 时,key 和 value 都不能包含 null 元素
// 这会抛出 IllegalArgumentException
chatClient.prompt()
.user(u -> u.text("Hello")
.metadata(null, "value")) // 非法:key 为 null
.call()
.content();
// 这同样会抛出 IllegalArgumentException
chatClient.prompt()
.user(u -> u.text("Hello")
.metadata("key", null)) // 非法:value 为 null
.call()
.content();访问元数据
元数据会被包含在生成的 UserMessage 和 SystemMessage 对象中,可以通过消息的 getMetadata() 方法访问。在 Advisor 中处理消息、或查看对话历史时,这一功能特别有用。
使用默认值(Using Defaults)
在 @Configuration 配置类里给 ChatClient 设置默认系统文本,可以大大简化运行时代码。设定好默认值之后,调用 ChatClient 时只需要提供用户文本就行,不用在运行时代码路径里每次都设置一遍系统文本。
默认系统文本
在下面的例子中,我们会把系统文本配置成"始终用海盗的口吻回答"。为了避免在运行时代码里重复写系统文本,我们在 @Configuration 类中创建 ChatClient 实例:
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
// 在配置类里设定默认系统文本,运行时无需重复设置
return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
.build();
}
}再配一个 @RestController 来调用它:
@RestController
class AIController {
private final ChatClient chatClient;
AIController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/ai/simple")
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("completion", this.chatClient.prompt().user(message).call().content());
}
}用 curl 调用应用端点,结果如下:
❯ curl localhost:8080/ai/simple
{"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"}带参数的默认系统文本
在下面的例子中,我们会在系统文本中放一个占位符,把"用什么口吻回答"从设计期挪到运行时再决定:
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
// 系统文本里放置 {voice} 占位符,运行时再注入具体值
return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
.build();
}
}@RestController
class AIController {
private final ChatClient chatClient;
AIController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/ai")
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
return Map.of("completion",
this.chatClient.prompt()
.system(sp -> sp.param("voice", voice)) // 运行时传入 voice 参数
.user(message)
.call()
.content());
}
}用 httpie 调用应用端点,结果如下:
http localhost:8080/ai voice=='Robert DeNiro'
{
"completion": "You talkin' to me? Okay, here's a joke for ya: Why couldn't the bicycle stand up by itself? Because it was two tired! Classic, right?"
}其他默认值(Other Defaults)
在 ChatClient.Builder 层面,你可以指定默认的 Prompt 配置:
defaultOptions(ChatOptions chatOptions):既可以传入 ChatOptions 类中定义的可移植选项,也可以传入模型特定选项(比如OpenAiChatOptions)。关于各模型专属的 ChatOptions 实现的更多信息,请参阅 JavaDoc。defaultTools(Object… tools):注册一个或多个默认工具,每次请求都可用。接受ToolCallback、ToolCallbackProvider或带@Tool注解方法的 POJO 的混合传入。defaultToolContext(Map<String, Object> toolContext):为工具执行设置默认上下文。defaultSystem(String text)、defaultSystem(Resource text)、defaultSystem(Consumer<PromptSystemSpec> systemSpecConsumer):这些方法用来定义默认系统文本。defaultUser(String text)、defaultUser(Resource text)、defaultUser(Consumer<UserSpec> userSpecConsumer):这些方法用来定义默认用户文本。Consumer<UserSpec>允许你用 lambda 同时指定用户文本和任何默认参数。defaultTemplateRenderer(TemplateRenderer templateRenderer):为 Prompt 模板设置默认的 TemplateRenderer。defaultAdvisors(Advisor… advisor)、defaultAdvisors(List<Advisor> advisors):Advisor(顾问,一种请求拦截器,可在调用前后修改和增强 Prompt)允许你修改用于创建 Prompt 的数据。比如QuestionAnswerAdvisor实现就是通过在与用户文本相关的上下文信息中追加 Prompt,来实现检索增强生成模式的。defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):这个方法允许你用一个 Consumer 通过 AdvisorSpec 配置多个 Advisor。Consumer<AdvisorSpec>让你可以用 lambda 来添加 Advisor,比如QuestionAnswerAdvisor——它支持检索增强生成,会基于用户文本把相关上下文信息追加到 Prompt 中。
构建期设定的默认选项与运行时传入的选项,会按一定的规则合并(merge)成最终生效的配置,下图展示了这一合并机制:

在运行时,你可以使用对应的不带 default 前缀的方法来覆盖这些默认值:
options(ChatOptions.Builder optionsCustomizer)tools(Object… tools)toolContext(Map<String, Object> toolContext)messages(Message… messages)、messages(List<Message> messages)system(String text)、system(Resource text)、system(Consumer<PromptSystemSpec> systemSpecConsumer)user(String text)、user(Resource text)、user(Consumer<UserSpec> userSpecConsumer)templateRenderer(TemplateRenderer templateRenderer)advisors(Advisor… advisor)、advisors(List<Advisor> advisors)advisors(Consumer<AdvisorSpec> advisorSpecConsumer)
派生 ChatClient(Mutating the ChatClient)
你可以用 mutate() 方法,基于一个已有的 ChatClient(或 ChatClientRequestSpec,即单次请求规格对象)复制其设置来创建新的实例:
- 在
ChatClient上:Builder mutate()返回一个用该客户端默认设置初始化好的ChatClient.Builder。 - 在
ChatClientRequestSpec上:Builder mutate()返回一个用当前请求的设置初始化好的ChatClient.Builder。
这在"基于现有配置做微调派生出新客户端或新请求、又不想从头重配一遍"的场景下非常好用。
Advisor(顾问)
Advisor API 提供了一种灵活而强大的方式,让你能够在 Spring 应用中拦截、修改和增强 AI 驱动的交互。Advisor 的详细介绍请参阅 Advisors(顾问) 专章。
调用 AI 模型时的一个常见模式,就是把上下文数据追加或融合到 Prompt 中。
这些上下文数据可以有不同类型,常见的包括:
- 你自己的数据:AI 模型没在训练中见过的数据。即使模型见过类似数据,追加的上下文数据在生成响应时也会被优先考虑。
- 对话历史:聊天模型的 API 是无状态的。如果你告诉 AI 模型你叫什么名字,它在下一次交互时并不会记得。每次请求都必须带上对话历史,才能让模型在生成响应时把之前的交互考虑进去。
白话解读
Advisor 就像是 Spring MVC 里的 HandlerInterceptor 或者 Servlet Filter:请求发出去之前、响应回来之后,都能"插一脚"做加工。比如自动补历史聊天记录(ChatMemoryAdvisor)、自动查知识库塞资料,都是靠 Advisor 实现的。
ChatClient 中的 Advisor 配置
ChatClient 的 Fluent API 提供了一个 AdvisorSpec 接口来配置 Advisor。这个接口提供了添加参数、一次性设置多个参数、以及往链上加一个或多个 Advisor 的方法。
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}Advisor 加进链里的顺序至关重要,因为它决定了它们的执行顺序。每个 Advisor 都会以某种方式修改 Prompt 或上下文,而一个 Advisor 做的修改会传给链中的下一个。
ChatClient.builder(chatModel)
.build()
.prompt()
.advisors(a -> a
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(), // 先执行:把对话历史补进 Prompt
QuestionAnswerAdvisor.builder(vectorStore).build() // 后执行:基于用户问题做检索增强
)
.param(ChatMemory.CONVERSATION_ID, conversationId)) // 必须提供会话 ID
.user(userText)
.call()
.content();在这个配置中,MessageChatMemoryAdvisor 会先执行,把对话历史加入 Prompt;然后,QuestionAnswerAdvisor 会基于用户问题和已补入的对话历史执行搜索,从而可能给出更相关的检索结果。
注意
每次使用记忆 Advisor 的调用,都必须通过 .param() 提供 ChatMemory.CONVERSATION_ID。漏掉它会在运行时抛出 IllegalArgumentException。不存在默认的会话 ID。
检索增强生成(RAG)
请参阅 RAG(检索增强生成) 指南,了解如何通过 QuestionAnswerAdvisor 实现"基于你的文档问答"。
日志
SimpleLoggerAdvisor 是一个用来记录 ChatClient 请求和响应数据的 Advisor。这在调试和监控 AI 交互时很有用。
Java AI 为 LLM 和向量库交互提供了可观测性支持。更多信息请参阅 Observability(可观测性) 指南。
要开启日志,在创建 ChatClient 时把 SimpleLoggerAdvisor 加到 Advisor 链中即可。建议把它加在链的靠后位置:
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor()) // 把日志 Advisor 加到链的末端附近
.user("Tell me a joke?")
.call()
.chatResponse();要看到日志输出,把 Advisor 包的日志级别设为 DEBUG:
# 加到 application.properties 或 application.yaml 中
logging.level.org.springframework.ai.chat.client.advisor=DEBUG你可以使用下面这个构造器来自定义 AdvisedRequest 和 ChatResponse 中哪些数据会被记录:
SimpleLoggerAdvisor(
Function<ChatClientRequest, String> requestToString,
Function<ChatResponse, String> responseToString,
int order
)使用示例:
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(), // 自定义请求日志格式
response -> "Custom response: " + response.getResult(), // 自定义响应日志格式
0 // 执行顺序
);这让你能够按自己的需要定制记录下来的信息。
注意
在生产环境中记录敏感信息时要谨慎!
工具调用
除非显式禁用自动注册(见下文),ChatClient 总是会在 Advisor 链中自动注册一个 ToolCallingAdvisor(工具调用顾问)。这样一来,即使本次调用没有静态配置任何工具,其他 Advisor 在运行时动态注入的工具也能被正确处理。
String response = ChatClient.builder(chatModel)
.build()
.prompt("What day is tomorrow?")
.tools(new DateTimeTools()) // ToolCallingAdvisor 会被自动注册
.call()
.content();默认情况下,ToolCallingAdvisor 注册在 Ordered.HIGHEST_PRECEDENCE + 300 的位置。
禁用自动注册
全局禁用(所有调用)
设置 spring.ai.chat.client.tool-calling.enabled=false,即可为自动配置的 ChatClient 的每一次调用禁用自动注册:
spring.ai.chat.client.tool-calling.enabled=false设置该属性后,工具仍会作为定义发送给 AI 模型,但响应中的工具调用不会被自动执行。此时请使用"用户控制的工具执行"模式,自己驱动整个循环。
单次调用禁用
你可以使用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 为单次调用禁用自动注册:
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false)) // 单次调用禁用自动注册
.call()
.content();或者你也可以提供自己的 ToolCallingAdvisor(或任何实现了 ToolAdvisor 的 Advisor)——这种情况下自动注册会被自动抑制,因为链里已经包含一个 ToolAdvisor 了。
可以通过 .advisors() 传入自定义 Advisor:
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(customAdvisor) // 链中已有 ToolAdvisor,自动注册被抑制
.call()
.content();自定义默认 ToolCallingAdvisor
spring.ai.chat.client.tool-calling.* 系列属性用来控制自动配置的 ToolCallingAdvisor:
| 属性 | 默认值 | 说明 |
|---|---|---|
spring.ai.chat.client.tool-calling.enabled | true | 设为 false 可为所有调用禁用 ToolCallingAdvisor 自动注册。工具仍会发送给模型,但工具调用不会自动执行。 |
spring.ai.chat.client.tool-calling.advisor-order | Ordered.HIGHEST_PRECEDENCE + 300 | 自动注册的 ToolCallingAdvisor 在 Advisor 链中的插入位置。 |
Spring Boot:Advisor 顺序属性
调整自动注册 Advisor 最简单的方式,就是通过 spring.ai.chat.client.tool-calling.advisor-order 属性。它控制 ToolCallingAdvisor 被插入到 Advisor 链的哪个位置:
spring.ai.chat.client.tool-calling.advisor-order=0白话解读
顺序值的判断口诀:该值要低于"需要在工具调用循环内部反复执行的 Advisor"的 order(这些 Advisor 每轮迭代都会跑一遍),高于"只需要在循环外执行一次的 Advisor"的 order。默认值是 ToolCallingAdvisor.DEFAULT_ORDER。
Spring Boot:用户控制的工具执行
如果你想观察工具调用循环的每一次迭代——比如把中间结果块转发给前端 UI——可以按单次调用禁用自动注册的 Advisor,然后用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 自己驱动循环。完整示例请参阅 ToolCallingAdvisor(工具调用顾问) 章节。
Spring Boot:自定义 ToolCallingAdvisor.Builder Bean
想做得更深入——比如提供一个自定义的 ToolCallingManager(工具调用管理器)——可以声明一个 ToolCallingAdvisor.Builder<?> Bean。由于 Spring Boot 对自动配置 Bean 使用了 @ConditionalOnMissingBean,你的 Bean 会优先生效:
@Bean
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(ToolCallingManager myToolCallingManager) {
return ToolCallingAdvisor.builder()
.toolCallingManager(myToolCallingManager) // 注入自定义的 ToolCallingManager
.advisorOrder(Ordered.LOWEST_PRECEDENCE);
}ChatClient.Builder Bean 随后会自动装配上这个自定义 Builder。
不使用 Spring Boot
不用 Spring Boot 自动配置时,直接把配置好的 ToolCallingAdvisor.Builder 传给 ChatClient.builder():
ToolCallingManager customManager = ToolCallingManager.builder()
// 自定义解析器、异常处理器等
.build();
ChatClient chatClient = ChatClient
.builder(chatModel, observationRegistry, null, null,
ToolCallingAdvisor.builder().toolCallingManager(customManager))
.build();
// 每个注册了工具的 prompt,都会在自动注册的 Advisor 中使用 customManager
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.call()
.content();ToolAdvisor 标记接口
ToolAdvisor 是一个标记接口,用来向 ChatClient 表明"Advisor 链中已经有人负责工具执行了"。在自定义 Advisor 上实现这个接口,可以防止再自动注册出第二个 ToolCallingAdvisor。
更多细节请参阅 ToolCallingAdvisor(工具调用顾问)。
聊天记忆
ChatMemory(聊天记忆)接口表示聊天对话记忆的存储。它提供了向对话添加消息、从对话中检索消息、以及清空对话历史的方法。
目前有一个内置实现:MessageWindowChatMemory。
MessageWindowChatMemory 是一种聊天记忆实现,它会维护一个最多到指定上限(默认 20 条消息)的消息窗口。当消息数量超过这个上限时,较旧的消息会被淘汰,但系统消息会被保留。如果加入了新的系统消息,之前所有的系统消息都会从记忆中移除。这样既保证了对话始终有最新上下文可用,又能控制内存占用不失控。
MessageWindowChatMemory 底层由 ChatMemoryRepository(聊天记忆仓库)抽象支撑,它提供了多种存储实现,包括:InMemoryChatMemoryRepository、JdbcChatMemoryRepository、CassandraChatMemoryRepository、Neo4jChatMemoryRepository、MongoChatMemoryRepository 和 RedisChatMemoryRepository。
所有记忆 Advisor 都必须提供 ChatMemory.CONVERSATION_ID 参数,每次调用都得通过 .advisors(a → a.param(ChatMemory.CONVERSATION_ID, conversationId)) 传入。漏掉它会抛出 IllegalArgumentException。不存在默认的会话 ID。
更多细节和用法示例,请参阅 Chat Memory(聊天记忆) 文档。
白话解读
为什么需要聊天记忆?因为大模型的 API 是"金鱼记忆"——每次请求都是全新的,它压根不记得上一句你说了啥。ChatMemory 的作用就是把历史消息存起来,每次请求时由 Advisor 自动把最近的对话塞回去。MessageWindowChatMemory 就是"只留最近 20 条"的滑动窗口策略,防止 token 爆炸。
MemoryAdvisor 标记接口
MemoryAdvisor 是一个由 BaseChatMemoryAdvisor 扩展的标记接口。DefaultChatClient 用这个标记在自动注册逻辑中探测下游的记忆 Advisor。
如果你自定义的记忆 Advisor 没有继承 BaseChatMemoryAdvisor、但又希望参与这个探测,就应该实现 MemoryAdvisor 接口。
实现说明
在 ChatClient 中同时使用命令式和响应式两种编程模型,是这个 API 比较独特的一面。通常一个应用要么是响应式的,要么是命令式的,不会两者兼有。
- 自定义某个 Model 实现的 HTTP 客户端交互时,
RestClient(命令式)和WebClient(响应式)都必须配置。 - 流式调用只支持响应式栈。因此,命令式应用必须引入响应式栈(例如
spring-boot-starter-webflux)。 - 非流式调用只支持 Servlet 栈。因此,响应式应用必须引入 Servlet 栈(例如
spring-boot-starter-web),并且要预期部分调用是阻塞的。 - 工具调用是命令式的,会导致阻塞式工作流。这也会造成 Micrometer 观测数据不完整/中断(例如 ChatClient 的 span 和工具调用的 span 没有串联起来,前者的 span 会因此一直处于未完成状态)。
- 内置 Advisor 在标准调用时执行阻塞操作,在流式调用时执行非阻塞操作。Advisor 流式调用所用的 Reactor Scheduler(调度器)可以通过各 Advisor 类上的 Builder 来配置。
白话解读
最后这一段是"踩坑预告":因为 Java AI 底层同时依赖两套 HTTP 客户端(同步的 RestClient 和响应式的 WebClient),所以哪怕你写的是纯命令式应用,想用流式输出也得带上 webflux 依赖;反过来纯响应式应用也得带上 web 依赖。另外工具调用目前是阻塞式的,做全链路追踪时 span 可能接不上,别慌,这是已知行为。
Advisors(顾问)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/advisors.html 📌 原文版本:Java AI Reference v2.0.1(Stable 稳定版)
Advisors API 介绍
Java AI 的 Advisors API 提供了一种灵活又强大的方式,可以在你的 Spring 应用里拦截、修改、增强和 AI 相关的交互。借助于 Advisors API,开发者能够写出更复杂、更可复用、更易维护的 AI 组件。
它主要的好处有这么几个:
- 把那些"写一次就能反复用"的生成式 AI 套路(比如问答、记忆、RAG)封装起来;
- 在数据发往大语言模型(LLM)之前、以及从 LLM 回来之后做转换;
- 同一套代码可以在不同模型、不同业务场景下复用,真正做到"可移植"。
下面这段示例代码展示了如何用 ChatClient API 把现有的 Advisor 给配置起来:
ChatMemory chatMemory = ... // 初始化你的聊天记忆存储
VectorStore vectorStore = ... // 初始化你的向量数据库
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(), // 聊天记忆 Advisor
QuestionAnswerAdvisor.builder(vectorStore).build() // RAG(检索增强生成)Advisor
)
.build();
var conversationId = "678";
String response = this.chatClient.prompt()
// 在运行时给 Advisor 传参数
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(userText)
.call()
.content();白话解读
建议把 Advisor 通过 builder 的 defaultAdvisors() 方法在构建时就注册好,而不是每次调用前临时塞。这样 Advisor 才能稳定地参与"观测栈",你可以在监控系统里看到它每次执行的指标和链路追踪。
另外,Advisor 也会自动接入 Java AI 的可观测性(Observability)栈,因此你可以直接看到跟它执行相关的指标和链路追踪信息。
核心组件(Core Components)
Advisor API 主要由以下几部分组成:
CallAdvisor和CallAdvisorChain:用在非流式场景下;StreamAdvisor和StreamAdvisorChain:用在流式场景下;ChatClientRequest:表示"尚未密封"的 Prompt 请求;ChatClientResponse:表示聊天补全(Chat Completion)响应。
请求与响应里都持有一个 advise-context(Advisor 上下文对象),用来在整条 Advisor 链里共享状态。

adviseCall() 和 adviseStream() 是 Advisor 最关键的两个方法,它们一般会做这些事情:
- 检查尚未密封的 Prompt 数据;
- 按需定制、增强 Prompt 数据;
- 调用 Advisor 链里的下一个节点;
- 也可以选择"拦截"请求——不往下走,自己直接把响应填好;
- 检查聊天补全响应;
- 必要时抛异常,表示处理出错。
另外还有两个辅助方法:
getOrder():决定当前 Advisor 在链里的执行顺序;getName():给当前 Advisor 起一个唯一的名字。
由 Java AI 框架创建的 Advisor Chain(顾问链) 会按照各个 Advisor 的 getOrder() 值依次执行多个 Advisor。值越小,越先执行。链的最后一个 Advisor 是框架自动加上的,它负责真正把请求发给 LLM。
下面这张流程图展示了 Advisor 链和 Chat Model 之间的交互:

流程说明:
- Java AI 框架根据用户的 Prompt 生成一个
ChatClientRequest,并附带一个空的 Advisor 上下文对象; - 链里的每个 Advisor 都会处理这个请求,按需修改;也可以选择直接"拦截",不再调用下一个节点——此时该 Advisor 需自己负责把响应填好;
- 框架提供的最后一个 Advisor 真正把请求发给 Chat Model;
- Chat Model 的响应沿着 Advisor 链往回传,并被封装成
ChatClientResponse,里面还带着那个共享的上下文对象; - 每个 Advisor 都可以对响应做处理或修改;
- 最终的
ChatClientResponse被剥出ChatCompletion后返回给调用方。
Advisor 执行顺序(Advisor Order)
Advisor 链的执行顺序由 getOrder() 方法决定。要点如下:
- order 值越小,越先执行;
- Advisor 链的工作方式类似一个栈(stack):
- 链里的第一个 Advisor 是最先处理请求的;
- 它也是最后处理响应的;
- 如果你想精细控制执行顺序:
- 把 order 设得接近
Ordered.HIGHEST_PRECEDENCE,就能让该 Advisor 在链中最先执行(请求侧最先、响应侧最后); - 把 order 设得接近
Ordered.LOWEST_PRECEDENCE,就能让该 Advisor 在链中最后执行(请求侧最后、响应侧最先);
- 把 order 设得接近
- 值越大,优先级越低;
- 如果多个 Advisor 的 order 值相同,它们的执行顺序就不被保证。
小知识
之所以"order 越小越先执行"和"先入后出"看起来有点矛盾,是因为 Advisor 链本身就像一个栈:
- 优先级最高(order 最小)的 Advisor 会被放到栈顶;
- 栈展开时它最先处理请求;
- 栈回卷时它最后处理响应。
下面是 Spring 的 Ordered 接口语义,对照看一下:
public interface Ordered {
/**
* 最高优先级常量。
* @see java.lang.Integer#MIN_VALUE
*/
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
/**
* 最低优先级常量。
*/
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
/**
* 获取对象的 order 值。
* 值越大,优先级越低。因此 order 值最小的对象拥有最高的优先级。
* order 值相同的对象,最终的相对位置是任意的。
*/
int getOrder();
}如果你既想在请求侧排在最前面,又想在响应侧也排在最前面,可以这么做:
- 用两个 Advisor 分别负责请求侧和响应侧;
- 给它们设置不同的 order 值;
- 通过 Advisor 上下文对象在它们之间共享状态。
API 总览(API Overview)
Advisor 主要的几个接口都在包 org.springframework.ai.chat.client.advisor.api 下。下面是你自己写 Advisor 时一定会遇到的核心接口:
public interface Advisor extends Ordered {
String getName();
}同步和响应式两个子接口分别是:
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);
}和:
public interface StreamAdvisor extends Advisor {
Flux<ChatClientResponse> adviseStream(
ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);
}要在你的 Advisor 实现里"把链路往下传",需要分别调用 CallAdvisorChain 和 StreamAdvisorChain:
public interface CallAdvisorChain extends AdvisorChain {
/**
* 用给定的请求调用 CallAdvisorChain 里的下一个 CallAdvisor。
*/
ChatClientResponse nextCall(ChatClientRequest chatClientRequest);
/**
* 返回创建该链时包含的所有 CallAdvisor 实例列表。
*/
List<CallAdvisor> getCallAdvisors();
}和:
public interface StreamAdvisorChain extends AdvisorChain {
/**
* 用给定的请求调用 StreamAdvisorChain 里的下一个 StreamAdvisor。
*/
Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);
/**
* 返回创建该链时包含的所有 StreamAdvisor 实例列表。
*/
List<StreamAdvisor> getStreamAdvisors();
}实现一个 Advisor(Implementing an Advisor)
要写一个 Advisor,要么实现 CallAdvisor,要么实现 StreamAdvisor(也可以两个都实现)。最关键的就是实现 nextCall()(非流式)或 nextStream()(流式)。
示例(Examples)
下面用几个动手的例子说明 Advisor 是怎么用来"观察"和"增强"链路的。
日志 Advisor(Logging Advisor)
我们可以写一个最简单的日志 Advisor,在调用下一个 Advisor 之前打印 ChatClientRequest,调用之后再打印 ChatClientResponse。注意它只负责观察,并不会修改任何东西。这种实现同时支持非流式和流式两种场景。
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {
private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);
@Override
public String getName() { // (1) 给 Advisor 起一个唯一的名字
return this.getClass().getSimpleName();
}
@Override
public int getOrder() { // (2) 通过 order 控制执行顺序,值越小越先执行
return 0;
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
logRequest(chatClientRequest); // 调用前:记录请求
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
logResponse(chatClientResponse); // 调用后:记录响应
return chatClientResponse;
}
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
StreamAdvisorChain streamAdvisorChain) {
logRequest(chatClientRequest); // 流式场景:调用前记录请求
Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);
// (3) MessageAggregator 是个工具类,把 Flux 流聚合成单个 ChatClientResponse,
// 方便做日志这种"看完整响应"的操作。注意它只读,不能改响应内容。
return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse);
}
private void logRequest(ChatClientRequest request) {
logger.debug("request: {}", request);
}
private void logResponse(ChatClientResponse chatClientResponse) {
logger.debug("response: {}", chatClientResponse);
}
}白话解读
这个 Logging Advisor 是个非常好的入门案例:它什么也不改,只是"路过"就把请求和响应记录下来。在真实项目里,你可以照着这个模板,套上你自己的埋点、监控、安全审计逻辑,几乎不用动业务代码。
Re-Reading (Re2) Advisor(重读增强 Advisor)
文章 "Re-Reading Improves Reasoning in Large Language Models" 提出了一种叫 Re-Reading(Re2) 的技巧,能让 LLM 的推理能力更强。Re2 技巧要求把用户的输入这样增强一下:
{Input_Query}
Read the question again: {Input_Query}把 Re2 应用到用户输入上的 Advisor 实现大致是这样的:
public class ReReadingAdvisor implements BaseAdvisor {
// Re2 提示词模板:先放一次原问题,再"请再读一遍问题"再放一次
private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
{re2_input_query}
Read the question again: {re2_input_query}
""";
private final String re2AdviseTemplate;
private int order = 0;
public ReReadingAdvisor() {
this(DEFAULT_RE2_ADVISE_TEMPLATE);
}
public ReReadingAdvisor(String re2AdviseTemplate) {
this.re2AdviseTemplate = re2AdviseTemplate;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { // (1) before 方法负责增强用户的输入,应用 Re-Reading 技巧
String augmentedUserText = PromptTemplate.builder()
.template(this.re2AdviseTemplate)
.variables(Map.of("re2_input_query", chatClientRequest.prompt().getUserMessage().getText()))
.build()
.render();
return chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
return chatClientResponse; // 响应不做任何修改,原样返回
}
@Override
public int getOrder() { // (2) 通过 order 控制执行顺序,值越小越先执行
return this.order;
}
public ReReadingAdvisor withOrder(int order) {
this.order = order;
return this;
}
}白话解读
Re2 的核心思想很朴素:让模型把问题读两遍。实验证明,这种简单的"重读"操作能显著提高模型在数学推理、逻辑推理等任务上的表现。这个 Advisor 几乎不消耗额外资源,非常适合作为"低成本增强"挂在链头。
Java AI 内置 Advisor(Built-in Advisors)
Java AI 框架自带了好几个开箱即用的 Advisor,可以直接增强你的 AI 交互。下面是它们的概览:
聊天记忆 Advisor(Chat Memory Advisors)
这两个 Advisor 负责把对话历史存到聊天记忆存储里:
MessageChatMemoryAdvisor从记忆中取回历史,并把它作为一组消息(messages)追加到 Prompt 里。这种方式能保留对话历史的结构。但请注意:并非所有 AI 模型都支持这种追加 messages 的方式。VectorStoreChatMemoryAdvisor从VectorStore(向量数据库)里取回记忆,并把检索到的内容注入到 Prompt 的 system 文本里。这个 Advisor 特别适合"在大数据里搜最相关片段"的场景。
问答 Advisor(Question Answering Advisor)
QuestionAnswerAdvisor借助向量数据库做问答,实现了最朴素的 RAG(Naive RAG) 模式。RetrievalAugmentationAdvisor一个更"模块化"的 RAG Advisor,使用包org.springframework.ai.rag下的构建块来实现常见的 RAG 流程,遵循 Modular RAG Architecture(模块化 RAG 架构)。详见 RAG(检索增强生成)。
推理 Advisor(Reasoning Advisor)
ReReadingAdvisor实现"重读"策略(RE2),用来增强模型在输入阶段对问题的理解。来源文章:Re-Reading Improves Reasoning in LLMs。
工具调用 Advisor(Tool Calling Advisor)
ToolCallingAdvisor把"工具调用循环"作为 Advisor 链的一环来处理。这个 Advisor 总是会被ChatClient自动注册(除非你显式禁用),因此即使没有静态配置工具,其他 Advisor 在运行时动态注入的工具也能正常使用。它会执行模型请求的工具调用,把结果发回去,再迭代到模型不再请求工具为止。它实现了ToolAdvisor(一个标记接口),用以阻止第二个ToolCallingAdvisor被自动注册。完整文档请看:Recursive Advisors - ToolCallingAdvisor 和 Tool Calling(工具调用)。
内容安全 Advisor(Content Safety Advisor)
SafeGuardAdvisor一个简单实用的 Advisor,用来阻止模型生成有害或不当的内容。
流式 vs 非流式(Streaming vs Non-Streaming)

要点对照:
- 非流式 Advisor 处理的是完整的请求和完整的一次性响应;
- 流式 Advisor 把请求和响应都当成"持续流动"的流来处理,响应侧通常用响应式编程的
Flux。
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {
return Mono.just(chatClientRequest)
.publishOn(Schedulers.boundedElastic())
.map(request -> {
// 这段逻辑既可以在阻塞线程里执行,也可以在非阻塞线程里执行
// Advisor 的"next 之前"逻辑写在这里
})
.flatMapMany(request -> chain.nextStream(request))
.map(response -> {
// Advisor 的"next 之后"逻辑写在这里
});
}最佳实践(Best Practices)
- 让每个 Advisor 只做一件事:这样模块化更好,可读性更高;
- 用
adviseContext在多个 Advisor 之间共享状态,需要时才用; - 流式和非流式两套都实现,适配面更广;
- 仔细规划 Advisor 在链里的顺序,保证数据流向是对的。
破坏性变更(Breaking API Changes)
版本历史
本节记录 Advisor API 在历史版本中的重大变更,方便从旧版本迁移的用户对照。
Advisor 接口变更
在 1.0 M2 版本:当时是两个分开的接口
RequestAdvisor和ResponseAdvisor:RequestAdvisor在ChatModel.call和ChatModel.stream之前被调用;ResponseAdvisor在它们之后被调用。
在 1.0 M3 版本:这两个接口被替换成:
CallAroundAdvisor;StreamAroundAdvisor。 原先在ResponseAdvisor上的StreamResponseMode被移除。
在 1.0.0 版本:又做了一次重命名:
CallAroundAdvisor→CallAdvisor;StreamAroundAdvisor→StreamAdvisor;CallAroundAdvisorChain→CallAdvisorChain;StreamAroundAdvisorChain→StreamAdvisorChain;AdvisedRequest→ChatClientRequest;AdvisedResponse→ChatClientResponse。
上下文 Map 的处理方式变更
在 1.0 M2 版本:
- 上下文 Map 是一个独立的方法参数;
- Map 是可变的,会沿着链往下传。
在 1.0 M3 版本:
- 上下文 Map 被合并进
AdvisedRequest和AdvisedResponse这两个 record 里; - Map 变成不可变的;
- 想要更新上下文,需要调用
updateContext方法——它会返回一个包含新内容的、不可变的 Map。
- 上下文 Map 被合并进
Recursive Advisors(递归顾问)
📌 原文地址:https://docs.spring.io/java-ai/reference/api/advisors-recursive.html 📌 原文版本:Java AI Reference v2.0.1(Stable 稳定版)
什么是递归顾问(What is a Recursive Advisor)
递归顾问是一种特殊的 Advisor(顾问),它能反复循环执行下游的顾问链(Advisor Chain)。这种模式在需要重复调用 LLM(大语言模型)直到满足某个条件时特别有用,比如下面这些场景:
- 循环执行工具调用(Tool Calling),直到没有更多工具需要被调用;
- 校验结构化输出(Structured Output),如果校验失败就重试;
- 实现评估逻辑,并在评估过程中修改请求内容;
- 实现重试逻辑,并在重试时修改请求内容。

CallAdvisorChain.copy(CallAdvisor after) 方法是实现递归顾问模式的关键工具。它会创建一个新的顾问链,只包含指定顾问之后的那些顾问,然后让递归顾问按需调用这个子链。这种设计确保了以下几点:
- 递归顾问可以在链中剩余的顾问之间循环;
- 链中的其他顾问可以观察和拦截每一次循环迭代;
- 顾问链保持正确的顺序和可观察性(Observability,即可追踪每次调用);
- 递归顾问不会重复执行排在它前面的顾问。
白话解读
递归顾问就像一个"循环器":普通顾问只能跑一遍就完事儿,而递归顾问可以反复跑下游的顾问链,直到满足条件为止。关键是 copy() 方法——它能让递归顾问只看到"自己之后的"顾问,避免把自己也套进去造成死循环。
内置的递归顾问(Built-in Recursive Advisors)
Java AI 内置了两个递归顾问,用来说明这种模式。
ToolCallingAdvisor
ToolCallingAdvisor 把工具调用循环实现成了顾问链的一部分,而不是依赖每个 ChatModel 内部的执行逻辑。只要工具存在,它就会被 DefaultChatClient 自动注册,是 ChatClient 驱动工具增强对话(Tool-Augmented Conversation,即带工具的对话)的默认机制。
主要特点(Highlights):
- 循环遍历顾问链,直到
ToolExecutionEligibilityChecker(工具执行资格检查器)报告没有更多工具调用为止; - 用
callAdvisorChain.copy(this)创建子链来做递归调用——其他顾问可以观察和拦截每一次循环; - 支持 return-direct(直接返回):当某个工具的结果带有
returnDirect = true时,顾问会跳出循环,直接把工具结果返回给调用方,而不再发回给 LLM; - 实现了
ToolAdvisor标记接口(Marker Interface,即没有方法、只用来"打标记"的接口),DefaultChatClient用它来强制"单一工具顾问"的不变量(Invariant,即永远成立的约束)——自定义子类可以透明地注册成替代品; - 累计每次循环的 token 使用量(Token Usage),所以最终的
ChatResponse(聊天响应)会报告所有模型调用的累计用量,而不仅仅是最后一次的用量(参见下文《在自定义递归顾问中累计 Token 用量》一节)。
简短示例:
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager) // 工具调用管理器
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300) // 顾问执行顺序(优先级很高)
.build();
var chatClient = ChatClient.builder(chatModel) // 用 chatModel 构建 ChatClient
.defaultAdvisors(toolCallingAdvisor) // 把工具调用顾问设为默认顾问
.build();关于完整的 builder API、钩子方法(Hook Methods,即可被子类覆写的扩展点)、配置选项、与内存顾问(Memory Advisor)的顺序交互、用户控制的执行模式,以及自定义子类的扩展模式,请参考 ToolCallingAdvisor(工具调用顾问) 一章。
关于工具循环如何融入更广泛的工具调用架构,请参考 Tool Calling(工具调用) 一章。
关于一个使用 ToolCallingAdvisor 扩展钩子实现渐进式工具披露(Progressive Tool Disclosure,即按需逐步把工具暴露给模型)的具体子类,请参考 Tool Search Tool(工具搜索工具) 一章。
StructuredOutputValidationAdvisor
StructuredOutputValidationAdvisor(结构化输出校验顾问)会拿 JSON Schema(JSON 模式定义) 去校验 LLM 输出的结构化 JSON,校验失败就重试,重试次数可配置。
关键特性:
- 从期望的输出类型自动推导出 JSON Schema,或者接受一个预先提供的 schema 字符串;
- 根据 schema 校验 LLM 的响应;
- 校验失败时重试(默认:最多重试 3 次);
- 在重试时把校验错误信息附加到 Prompt(提示词)里,帮助模型自我修正;
- 用
callAdvisorChain.copy(this)创建子链来做递归调用; - 累计每次校验尝试的 token 使用量,所以返回的
ChatResponse会报告所有重试的累计用量,而不仅仅是最后一次的用量(参见下文《在自定义递归顾问中累计 Token 用量》一节); - 可选地支持自定义
JsonMapper(JSON 映射器)。
该顾问可以通过 outputType(自动推导 schema)或 outputJsonSchema(预先提供 schema 字符串)来配置;这两个选项互斥。
使用 outputType 的示例:
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputType(MyResponseType.class) // 指定期望的输出类型,自动推导 schema
.maxRepeatAttempts(3) // 最多重试 3 次
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(validationAdvisor) // 把校验顾问设为默认顾问
.build();使用预先提供 JSON Schema 的示例:
var validationAdvisor = StructuredOutputValidationAdvisor.builder()
.outputJsonSchema(myConverter.getJsonSchema()) // 直接传入已经准备好的 schema 字符串
.build();另外,也可以直接在 entity() 调用上启用 schema 校验,不用手动配置顾问,通过 EntityParamSpec 消费者(Consumer)即可:
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.") // 用户输入:"给一个随机演员生成作品列表"
.call()
.entity(ActorFilms.class, spec -> spec.validateSchema()); // 解析为 ActorFilms 类型,并启用 schema 校验请参考 Schema Validation & Self-Correction(模式校验与自我修正) 了解高级用法。
白话解读
这两个内置顾问是递归模式的"教科书示例":ToolCallingAdvisor 解决"工具要调好几次"的问题,StructuredOutputValidationAdvisor 解决"输出格式不对就重试"的问题。它们都遵循一个套路——用 copy() 拿到自己之后的子链,然后循环跑直到满足条件。
在自定义递归顾问中累计 Token 用量(Accumulating Token Usage in Custom Recursive Advisors)
递归顾问每次调用会执行不止一次模型调用,所以它必须把每一次调用的 token 用量累计起来,才能报告正确的总和;否则调用方只能看到最后一次调用的用量。Java AI 提供了 org.springframework.ai.chat.client.advisor.UsageAccumulator 来简化这件事。每次 adviseCall 调用创建一个实例(或者在 Flux.defer 里按订阅创建一个,让它绑定到订阅上),用 addRoundResponse(...) 把每一轮的响应累计进来,最后用 applyAccumulatedUsage(...) 把累计的总量写到最终响应上:
UsageAccumulator usage = new UsageAccumulator(); // 创建一个用量累计器
ChatClientResponse response;
do {
response = callAdvisorChain.copy(this).nextCall(request); // 调用子链(递归执行)
usage.addRoundResponse(response.chatResponse()); // 累计这一轮的响应用量
// ... 决定是否需要再跑一轮 ...
}
while (loopAgain);
return usage.applyAccumulatedUsage(response); // 把累计的总量写到最终响应上底层的 token 计算逻辑在 org.springframework.ai.support.UsageCalculator 里(accumulateResponseUsage 和 withUsage 两个方法),UsageAccumulator 是它的一层包装。
白话解读
这个累计用量的小细节很容易被忽略:因为递归顾问会跑好几轮 LLM,每一轮都会消耗 token。如果不把每轮的用量加起来,最终用户看到的 token 统计就只有最后一轮的,那账单和限流计算都会出问题。UsageAccumulator 就是帮你自动累加用的。