主题
Chat Client API
ChatClient 提供了一个用于与 AI 模型通信的流式(fluent)API。 它同时支持同步与流式两种编程模型。
NOTE
关于 ChatClient 中命令式与响应式编程模型的混合使用,请参阅本文档末尾的实现说明
该流式 API 提供了若干方法,用于构建作为输入传给 AI 模型的 Prompt 的各个组成部分。 Prompt 包含用于引导 AI 模型输出与行为的指令文本。从 API 的角度看,提示词由一组消息构成。
AI 模型处理两类主要消息:用户消息,即来自用户的直接输入;以及系统消息,由系统生成以引导对话。
这些消息通常包含占位符,会在运行时依据用户输入被替换,从而让 AI 模型的响应用户输入定制。
此外还可以指定 Prompt 选项,例如要使用的 AI 模型名称,以及控制生成输出随机性或创造性的温度设置。
创建 ChatClient
ChatClient 通过一个 ChatClient.Builder 对象创建。 你可以为任何 ChatModel Spring Boot 自动配置获得一个自动配置的 ChatClient.Builder 实例,或以编程方式创建一个。
使用自动配置的 ChatClient.Builder
在最简单的场景中,Spring AI 提供 Spring Boot 自动配置,为你创建一个可注入到你类中的原型(prototype)ChatClient.Builder bean。 下面是一个针对简单用户请求获取 String 响应的简单示例。
java
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/ai")
String generation(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.call()
.content();
}
}在这个简单示例中,用户输入设置用户消息的内容。 call() 方法向 AI 模型发送一个请求,content() 方法把 AI 模型的响应作为 String 返回。
处理多个聊天模型
有若干场景你可能需要在一个应用中处理多个聊天模型:
- 为不同类型的任务使用不同模型(例如,用强大模型做复杂推理,用更快更便宜的模型做简单任务)
- 在某个模型服务不可用时实现回退机制
- 对不同模型或配置做 A/B 测试
- 依据用户偏好,让用户在多个模型间选择
- 组合专职模型(一个用于代码生成,另一个用于创意内容等)
默认情况下,Spring AI 自动配置单个 ChatClient.Builder bean。 然而,你可能需要在应用中处理多个聊天模型。 下面是如何处理这种场景:
单模型类型的多个 ChatClient
本节涵盖一个常见用例:你需要创建多个 ChatClient 实例,它们都使用同一底层模型类型但配置不同。 你可以直接使用自动配置的 ChatClient.Builder,因为它是原型作用域的,意味着每个注入点都会创建一个新实例:
java
@Configuration
class ChatClientConfig {
@Bean
ChatClient defaultChatClient(ChatClient.Builder builder) {
return builder.build();
}
@Bean
ChatClient customChatClient(ChatClient.Builder builder) {
return builder.defaultSystem("You are a helpful assistant.").build();
}
}针对不同模型类型的 ChatClient
在处理多个 AI 模型时,你可能会想用 ChatClient.create(chatModel) 或 ChatClient.builder(chatModel) 定义独立的 ChatClient bean。 然而这样做会绕过自动配置的 ChatClient.Builder,意味着可观测性和 ChatClientBuilderCustomizer bean 会被忽略。
为保留可观测性与定制器,你应注入 ChatClientBuilderConfigurer 来创建自定义构建器。 ChatClientBuilderConfigurer 会应用所有已注册的 ChatClientBuilderCustomizer bean 并装配可观测性,复刻自动配置内部所做的行为。
当应用上下文中存在多个 ChatModel bean 时,Spring 无法无歧义地解析自动配置的 ChatClient.Builder bean 所依赖的 ChatModel。 为解决这一点,你可以把某个 ChatClient bean 标记为 @Primary。若你手动定义 ChatModel,可能也需要把其中一个标记为 @Primary;或者,定义你自己的 ChatClient.Builder bean 来覆盖自动配置的那个。
java
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
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) {
ChatClient.Builder builder = ChatClient.builder(chatModel,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
chatClientObservationConvention.getIfUnique(),
advisorObservationConvention.getIfUnique(),
toolCallingAdvisorBuilder.getIfAvailable());
return configurer.configure(builder).build();
}
}随后你可以使用 @Qualifier 注解把这些 bean 注入到你的应用组件:
java
@Configuration
public class ChatClientExample {
@Bean
CommandLineRunner cli(
@Qualifier("openAiChatClient") ChatClient openAiChatClient,
@Qualifier("anthropicChatClient") ChatClient anthropicChatClient) {
return args -> {
var scanner = new Scanner(System.in);
ChatClient chat;
// Model selection
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");
}
// Use the selected chat client
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 端点
你可以使用 OpenAiChatModel 的构建器创建多个实例,以连接到不同的 OpenAI 兼容 API。 当你需要处理多个服务商时,这尤其有用。
java
@Service
public class MultiModelService {
private static final Logger logger = LoggerFactory.getLogger(MultiModelService.class);
public void multiClientFlow() {
try {
// Create a new OpenAiChatModel for Groq (Llama3)
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();
// Create a new OpenAiChatModel for GPT-4
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();
// Simple prompt for both models
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 流式 API
ChatClient 流式 API 允许你通过重载的 prompt 方法,以三种不同方式发起流式 API 来创建提示词:
prompt():这个无参方法让你开始使用流式 API,从而构建用户、系统以及提示词的其他部分。prompt(Prompt prompt):这个方法接收一个Prompt参数,让你传入一个你用 Prompt 的非流式 API 创建的Prompt实例。prompt(String content):这是一个与上一个重载类似的便捷方法。它接收用户的文本内容。
ChatClient 响应
ChatClient API 提供了多种方式来格式化来自 AI 模型的响应。
返回 ChatResponse
来自 AI 模型的响应是一个由 ChatResponse 类型定义的丰富结构。 它包含关于响应如何生成的元数据,并可包含多个响应,即所谓的 Generation,每个都有自己的元数据。 元数据包含用于生成响应的令牌数(每个令牌约 3/4 个词)。 这一信息很重要,因为托管 AI 模型按每次请求所用令牌数计费。
下面这个示例通过在 call() 方法后调用 chatResponse(),返回包含元数据的 ChatResponse 对象。
java
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();返回实体
你常常想返回一个从返回的 String 映射而来的实体类。 entity() 方法提供了这一功能。
例如,给定下面的 Java record:
java
record ActorFilms(String actor, List<String> movies) {}你可以像下面这样,轻松用 entity() 方法把 AI 模型的输出映射到这个 record:
java
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);还有一个重载的 entity 方法,签名为 entity(ParameterizedTypeReference<T> type),让你得以指定诸如泛型 List 之类的类型:
java
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 响应,失败时带错误反馈自动重试。useProviderStructuredOutput()—— 把模式作为 API 级约束(而非提示词文本)发送给服务商。
java
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class, spec -> spec
.useProviderStructuredOutput()
.validateSchema());关于这些开关的完整细节、受支持的服务商、限制以及更底层的转换器 API,参见 结构化输出 参考。
流式响应
stream() 方法让你能像下面这样获取异步响应:
java
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();你也可以用 Flux<ChatResponse> chatResponse() 方法来流式传输 ChatResponse。
未来我们会提供一个便捷方法,让你在响应式 stream() 方法上返回一个 Java 实体。 在此期间,你应使用 结构化输出转换器 像下面这样显式转换聚合后的响应。 这也演示了流式 API 中参数的用法,将在文档后面章节更详细地讨论。
java
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);提示词模板
ChatClient 流式 API 让你能把用户和系统文本作为模板提供,其中的变量在运行时被替换。
java
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("Tell me the names of 5 movies whose soundtrack was composed by {composer}")
.param("composer", "John Williams"))
.call()
.content();在内部,ChatClient 使用 PromptTemplate 类来处理用户和系统文本,并依赖给定的 TemplateRenderer 实现把变量替换为运行时提供的值。 默认情况下,Spring AI 使用 StTemplateRenderer 实现,它基于由 Terence Parr 开发的开源 StringTemplate 引擎。
Spring AI 还提供了 NoOpTemplateRenderer,用于无需模板处理的场景。
NOTE
直接在 ChatClient 上配置的 TemplateRenderer(通过 .templateRenderer())只适用于在 ChatClient 构建器链中直接定义的提示词内容(例如通过 .user()、.system())。
它不会影响 顾问(如 QuestionAnswerAdvisor)内部使用的模板,后者有自己的模板定制机制(参见自定义顾问模板)。
如果你更想使用不同的模板引擎,可以直接向 ChatClient 提供 TemplateRenderer 接口的自定义实现。你也可以继续使用默认的 StTemplateRenderer,只是采用自定义配置。
例如,默认情况下模板变量以 {} 语法标识。 如果你打算在提示词中包含 JSON,可能想用不同的语法以避免与 JSON 语法冲突。例如,你可以使用 < 和 > 定界符。
java
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("Tell me the names of 5 movies whose soundtrack was composed by <composer>")
.param("composer", "John Williams"))
.templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.call()
.content();call() 返回值
在 ChatClient 上指定 call() 方法后,响应类型有几种不同选项。
String content(): 返回响应的字符串内容ChatResponse chatResponse(): 返回ChatResponse对象,其中含多个生成结果以及响应元数据(例如本次响应用了多少 token)。ChatClientResponse chatClientResponse(): 返回ChatClientResponse对象,含ChatResponse与 ChatClient 执行上下文,可访问 advisor 执行期间用到的额外数据(例如 RAG 流程中检索到的相关文档)。entity()返回某个 Java 类型entity(ParameterizedTypeReference<T> type): 用于返回实体类型的Collection。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 类型。当你需要在一次调用里既拿到完整的模型响应(含元数据与生成结果)又拿到结构化输出实体时很有用。responseEntity(Class<T> type): 用于返回一个ResponseEntity,同时包含完整的ChatResponse与指定的实体类型。responseEntity(Class<T> type, Consumer<EntityParamSpec> spec): 同上,可带可选的 EntityParamSpec 配置。responseEntity(ParameterizedTypeReference<T> type): 用于返回一个ResponseEntity,同时包含完整的ChatResponse与实体类型的Collection。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()。
NOTE
调用 call() 方法并不会真正触发 AI 模型执行。它只是告诉 Spring AI 使用同步还是流式调用。
实际的 AI 模型调用发生在 content()、chatResponse()、responseEntity() 等方法被调用之时。
stream() 返回值
在 ChatClient 上指定 stream() 方法后,响应类型有几种选项:
Flux<String> content():返回由 AI 模型生成的字符串的Flux。Flux<ChatResponse> chatResponse():返回ChatResponse对象的Flux,其中包含关于响应的额外元数据。Flux<ChatClientResponse> chatClientResponse():返回ChatClientResponse对象的Flux,它包含ChatResponse对象和 ChatClient 执行上下文,让你得以访问顾问执行期间使用的额外数据(例如在 RAG 流程中检索到的相关文档)。
消息元数据
ChatClient 支持为用户和系统消息添加元数据。 元数据提供关于消息的额外上下文与信息,可供 AI 模型或下游处理使用。
为用户消息添加元数据
你可以使用 metadata() 方法为用户消息添加元数据:
java
// Adding individual metadata key-value pairs
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();
// Adding multiple metadata entries at once
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();为系统消息添加元数据
类似地,你可以为系统消息添加元数据:
java
// Adding metadata to system messages
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 构建器层面配置默认元数据:
java
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.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 会校验元数据以确保数据完整性:
- 元数据键不能为 null 或空
- 元数据值不能为 null
- 传入 Map 时,键和值都不能包含 null 元素
java
// This will throw an IllegalArgumentException
chatClient.prompt()
.user(u -> u.text("Hello")
.metadata(null, "value")) // Invalid: null key
.call()
.content();
// This will also throw an IllegalArgumentException
chatClient.prompt()
.user(u -> u.text("Hello")
.metadata("key", null)) // Invalid: null value
.call()
.content();访问元数据
元数据被包含在生成的 UserMessage 和 SystemMessage 对象中,可通过消息的 getMetadata() 方法访问。 在顾问中处理消息或检查对话历史时,这尤其有用。
使用默认值
在一个 @Configuration 类中创建一个带默认系统文本的 ChatClient,可简化运行时代码。 通过设置默认值,你在调用 ChatClient 时只需指定用户文本,无需在运行时代码路径里为每个请求设置系统文本。
默认系统文本
在下面的示例中,我们把系统文本配置为总以海盗的口吻回复。 为避免在运行时代码里重复系统文本,我们在一个 @Configuration 类中创建一个 ChatClient 实例。
java
@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:
java
@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 调用应用端点时,结果是:
bash
❯ curl localhost:8080/ai/simple
{"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"}带参数的默认系统文本
在下面的示例中,我们用一个占位符在系统文本中指定补全的语气,是在运行时而非设计时确定。
java
@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 {voice}")
.build();
}
}java
@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))
.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?"
}其他默认值
在 ChatClient.Builder 层面,你可以指定默认的提示词配置。
defaultOptions(ChatOptions chatOptions):传入ChatOptions类定义的可移植选项,或诸如OpenAiChatOptions这样的模型特定选项。 关于模型特定ChatOptions实现的更多信息,请参阅 JavaDocs。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):为提示词模板设置默认的TemplateRenderer。defaultAdvisors(Advisor... advisor)、defaultAdvisors(List<Advisor> advisors):顾问允许修改用于创建Prompt的数据。QuestionAnswerAdvisor实现通过在提示词后附加与用户文本相关的上下文信息,来启用检索增强生成模式。defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):这个方法让你定义一个Consumer,通过AdvisorSpec配置多个顾问。顾问可以修改用于创建最终Prompt的数据。Consumer<AdvisorSpec>让你指定一个 lambda 来添加顾问,例如QuestionAnswerAdvisor,它通过依据用户文本附加相关上下文信息来支持检索增强生成。
你可以在运行时通过对应的、不带 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
你可以使用 mutate() 方法,用一个现有 ChatClient 的设置副本创建一个新的 ChatClient(或 ChatClientRequestSpec):
- 在
ChatClient上:Builder mutate()返回一个用该客户端默认设置初始化的ChatClient.Builder。 - 在
ChatClientRequestSpec上:Builder mutate()返回一个用该请求当前设置初始化的ChatClient.Builder。
这对在不重新定义所有选项的情况下创建派生的客户端或请求很有用。
顾问
顾问 API 提供了一种灵活而强大的方式,拦截、修改并增强你的 Spring 应用中由 AI 驱动的交互。
用用户文本调用 AI 模型时的一个常见模式,是用上下文数据来追加或增强提示词。
这些上下文数据可以有不同类型。常见类型包括:
你自己的数据:这是 AI 模型未曾训练过的数据。 即便模型见过类似数据,追加的上下文数据在生成响应时也占优先。
对话历史:聊天模型的 API 是无状态的。 如果你告诉 AI 模型你的名字,它不会在后续交互中记住它。 对话历史必须随每个请求一并发送,以确保生成响应时会考虑先前的交互。
ChatClient 中的顾问配置
ChatClient 流式 API 提供了一个用于配置顾问的 AdvisorSpec 接口。 该接口提供了添加参数、一次性设置多个参数,以及向链中添加一个或多个顾问的方法。
java
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}WARNING
顾问被加入链中的先后顺序至关重要,因为它决定了它们执行的次序。
每个顾问都以某种方式修改提示词或上下文,而一个顾问所做的更改会传递给链中的下一个。
java
ChatClient.builder(chatModel)
.build()
.prompt()
.advisors(a -> a
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(userText)
.call()
.content();在此配置中,MessageChatMemoryAdvisor 会先执行,把对话历史加入提示词。 随后,QuestionAnswerAdvisor 会基于用户问题和已加入的对话历史执行搜索,从而可能提供更有相关性的结果。
NOTE
每个使用记忆顾问的调用都必须通过 .param() 提供 ChatMemory.CONVERSATION_ID。省略它会在运行时抛出 IllegalArgumentException。
检索增强生成
请参阅 检索增强生成 指南。
日志
SimpleLoggerAdvisor 是一个记录 ChatClient 的 request 与 response 数据的顾问。 这对调试和监控你的 AI 交互很有用。
TIP
Spring AI 支持对 LLM 和向量库交互的可观测性。
更多信息请参阅可观测性指南。
要启用日志,请在创建 ChatClient 时把 SimpleLoggerAdvisor 加入顾问链。 建议把它加在链靠后的位置:
java
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.user("Tell me a joke?")
.call()
.chatResponse();要查看日志,请把顾问包的日志级别设为 DEBUG:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG把这添加到你的 application.properties 或 application.yaml 文件。
你可以使用以下构造器,定制从 AdvisedRequest 和 ChatResponse 中记录哪些数据:
java
SimpleLoggerAdvisor(
Function<ChatClientRequest, String> requestToString,
Function<ChatResponse, String> responseToString,
int order
)示例用法:
java
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(),
response -> "Custom response: " + response.getResult(),
0
);这让你能按自己的具体需求定制所记录的信息。
TIP
在生产环境中记录敏感信息时要谨慎。
工具调用
ChatClient 总是会在顾问链中自动注册一个 ToolCallingAdvisor,除非显式禁用自动注册(见下文)。 这确保了由另一个顾问在运行时动态注入的工具能被正确处理,即便该调用上没有配置任何静态工具。
java
String response = ChatClient.builder(chatModel)
.build()
.prompt("What day is tomorrow?")
.tools(new DateTimeTools()) // ToolCallingAdvisor is auto-registered
.call()
.content();ToolCallingAdvisor 默认在 Ordered.HIGHEST_PRECEDENCE + 300 处注册。
禁用自动注册
全局(所有调用)
设置 spring.ai.chat.client.tool-calling.enabled=false 可禁用自动配置的 ChatClient 所发起的每次调用的自动注册:
properties
spring.ai.chat.client.tool-calling.enabled=false设置此属性后,工具仍会作为定义发送给 AI 模型,但响应中的工具调用不会被自动执行。 使用用户控制工具执行自行驱动该循环。
逐次调用
你可以使用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 为单次调用禁用自动注册:
java
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.content();或者你可以提供你自己的 ToolCallingAdvisor(或任何其他实现 ToolAdvisor 的顾问)——在这种情况下,由于链中已包含一个 ToolAdvisor,自动注册会被自动抑制。
你可以通过 .advisors() 传入一个自定义顾问:
java
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(customAdvisor) // auto-registration suppressed
.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 在顾问链中的位置。 |
Spring Boot:顾问顺序属性
调优自动注册顾问的最简单方式是通过 spring.ai.chat.client.tool-calling.advisor-order 属性。 它控制 ToolCallingAdvisor 被插入到顾问链中的位置:
properties
spring.ai.chat.client.tool-calling.advisor-order=0该值必须低于任何应在工具调用循环内部运行(即每次迭代重复)的顾问的顺序,并高于任何应在外部运行(即每用户请求只执行一次)的顾问。 默认值为 ToolCallingAdvisor.DEFAULT_ORDER。
Spring Boot:用户控制工具执行
要观察工具调用循环的每一次迭代——例如把中间的块转发到 UI——请逐次调用禁用自动注册的顾问,用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 自行驱动该循环。 完整示例见用户控制工具执行 —— 配合 ChatClient。
Spring Boot:自定义 ToolCallingAdvisor.Builder bean
为了更深入的定制——例如提供一个自定义 ToolCallingManager——声明一个 ToolCallingAdvisor.Builder<?> bean。 Spring Boot 在自动配置 bean 上的 @ConditionalOnMissingBean 意味着你的 bean 优先:
java
@Bean
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(ToolCallingManager myToolCallingManager) {
return ToolCallingAdvisor.builder()
.toolCallingManager(myToolCallingManager)
.advisorOrder(Ordered.LOWEST_PRECEDENCE);
}随后 ChatClient.Builder bean 会自动装配这个自定义构建器。
不使用 Spring Boot
当不使用 Spring Boot 自动配置时,直接把一个预配置的 ToolCallingAdvisor.Builder 传给 ChatClient.builder():
java
ToolCallingManager customManager = ToolCallingManager.builder()
// custom resolver, exception processor, etc.
.build();
ChatClient chatClient = ChatClient
.builder(chatModel, observationRegistry, null, null,
ToolCallingAdvisor.builder().toolCallingManager(customManager))
.build();
// Every prompt that registers tools will use customManager in the auto-registered advisor
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.call()
.content();ToolAdvisor 标记接口
ToolAdvisor 是一个标记接口,向 ChatClient 表明顾问链已处理工具执行。在自定义顾问上实现此接口可防止自动注册第二个 ToolCallingAdvisor。
更多细节见扩展循环:自定义 ToolAdvisor。
聊天记忆
ChatMemory 接口表示聊天对话记忆的存储。 它提供了向对话添加消息、从对话检索消息以及清除对话历史的方法。
目前有一个内置实现:MessageWindowChatMemory。
MessageWindowChatMemory 是一个聊天记忆实现,维护一个消息窗口,至多到指定的最大数量(默认:20 条消息)。 当消息数超过此限制时,较旧的消息会被驱逐,但系统消息会被保留。若添加了一个新的系统消息,所有先前的系统消息会从记忆中移除。这确保对话的最新上下文始终可用,同时让内存占用保持有界。
MessageWindowChatMemory 由 ChatMemoryRepository 抽象支撑,该抽象为聊天对话记忆提供存储实现。 有多个可用实现,包括 InMemoryChatMemoryRepository、JdbcChatMemoryRepository、CassandraChatMemoryRepository、Neo4jChatMemoryRepository、MongoChatMemoryRepository 和 RedisChatMemoryRepository。
WARNING
ChatMemory.CONVERSATION_ID 参数对所有记忆顾问都是必需的。它必须在每次调用中通过 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) 提供。省略它会抛出 IllegalArgumentException。没有默认的对话 ID。
更多细节和用法示例,参见聊天记忆文档。
MemoryAdvisor 标记接口
MemoryAdvisor 是一个被 BaseChatMemoryAdvisor 继承的标记接口。 DefaultChatClient 用此标记在自动注册逻辑中检测下游的记忆顾问。
不继承 BaseChatMemoryAdvisor 但应参与此检测的自定义记忆顾问,应实现 MemoryAdvisor。
实现说明
ChatClient 中命令式与响应式编程模型的混合使用,是该 API 的一个独特之处。 通常一个应用要么是响应式的,要么是命令式的,而非两者兼有。
在定制某个 Model 实现的 HTTP 客户端交互时,RestClient 和 WebClient 都必须配置。
流式仅通过响应式技术栈支持。 由于此原因,命令式应用必须引入响应式技术栈(例如 spring-boot-starter-webflux)。
非流式仅通过 Servlet 技术栈支持。 由于此原因,响应式应用必须引入 Servlet 技术栈(例如 spring-boot-starter-web),并预期某些调用会阻塞。
工具调用是命令式的,会导致阻塞式工作流。 这也会导致部分/中断的 Micrometer 观测(例如 ChatClient 的 span 与工具调用的 span 未连接,前者因此保持未完成)。
内置顾问为标准调用执行阻塞操作,为流式调用执行非阻塞操作。 顾问流式调用所使用的 Reactor Scheduler 可通过每个 Advisor 类的 Builder 来配置。