ChatClient 提供了一个流式 API(Fluent API)用于与 AI 模型通信。它同时支持同步和流式编程模型。
请参阅本文档底部的“实现说明”,了解与 ChatClient 中命令式和响应式编程模型结合使用相关的信息。
该流式 API 提供了一些方法,用于构建作为输入传递给 AI 模型的提示(Prompt)的组成部分。提示包含指导性文本,用于引导 AI 模型的输出和行为。从 API 的角度来看,提示由消息(messages)的集合组成。
AI 模型处理两种主要类型的消息:用户消息(user messages),即来自用户的直接输入;以及系统消息(system messages),即系统生成的用于引导对话的消息。
这些消息通常包含占位符(placeholders),这些占位符在运行时根据用户输入被替换,以定制 AI 模型对用户输入的响应。
还可以指定提示选项(Prompt options),例如要使用的 AI 模型名称,以及控制生成输出随机性或创造性的温度(temperature)设置。
创建 ChatClient
ChatClient 使用 ChatClient.Builder
对象创建。您可以通过任何启用了 Spring Boot 自动配置的 ChatModel
获取一个自动配置的 ChatClient.Builder
实例,或者以编程方式创建一个。
使用自动配置的 ChatClient.Builder
在最简单的用例中,Spring AI 提供了 Spring Boot 自动配置,为您创建一个原型(prototype)的 ChatClient.Builder
bean,以便注入到您的类中。下面是一个简单的示例,展示如何获取对简单用户请求的字符串响应。
@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 模型的响应作为字符串返回。
使用多个聊天模型
在单个应用程序中,您可能需要使用多个聊天模型的场景包括:
* 为不同类型的任务使用不同的模型(例如,强大的模型用于复杂推理,更快、更便宜的模型用于简单任务)
* 当某个模型服务不可用时实现回退机制
* 对不同模型或配置进行 A/B 测试
* 根据用户偏好提供模型选择
* 组合专用模型(一个用于代码生成,另一个用于创意内容等)
默认情况下,Spring AI 自动配置一个单一的 ChatClient.Builder
bean。但是,您的应用程序中可能需要使用多个聊天模型。以下是处理此场景的方法:
在所有情况下,您都需要通过设置属性 spring.ai.chat.client.enabled=false
来禁用 ChatClient.Builder
的自动配置。
这使您可以手动创建多个 ChatClient
实例。
使用单一模型类型的多个 ChatClient
本节介绍一个常见用例:您需要创建多个 ChatClient
实例,它们都使用相同的基础模型类型,但配置不同。
// Create ChatClient instances programmatically
ChatModel myChatModel = ... // already autoconfigured by Spring Boot
ChatClient chatClient = ChatClient.create(myChatModel);
// Or use the builder for more control
ChatClient.Builder builder = ChatClient.builder(myChatModel);
ChatClient customChatClient = builder
.defaultSystemPrompt("You are a helpful assistant.")
.build();
用于不同模型类型的 ChatClient
当使用多个 AI 模型时,您可以为每个模型定义独立的 ChatClient
bean:
import org.springframework.ai.chat.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
@Bean
public ChatClient anthropicChatClient(AnthropicChatModel chatModel) {
return ChatClient.create(chatModel);
}
}
然后,您可以使用 @Qualifier
注解将这些 bean 注入到您的应用程序组件中:
@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 端点
OpenAiApi
和 OpenAiChatModel
类提供了一个 mutate()
方法,允许您基于现有实例创建具有不同属性的变体。这在您需要与多个 OpenAI 兼容的 API 交互时特别有用。
@Service
public class MultiModelService {
private static final Logger logger = LoggerFactory.getLogger(MultiModelService.class);
@Autowired
private OpenAiChatModel baseChatModel;
@Autowired
private OpenAiApi baseOpenAiApi;
public void multiClientFlow() {
try {
// Derive a new OpenAiApi for Groq (Llama3)
OpenAiApi groqApi = baseOpenAiApi.mutate()
.baseUrl("https://api.groq.com/openai")
.apiKey(System.getenv("GROQ_API_KEY"))
.build();
// Derive a new OpenAiApi for OpenAI GPT-4
OpenAiApi gpt4Api = baseOpenAiApi.mutate()
.baseUrl("https://api.openai.com")
.apiKey(System.getenv("OPENAI_API_KEY"))
.build();
// Derive a new OpenAiChatModel for Groq
OpenAiChatModel groqModel = baseChatModel.mutate()
.openAiApi(groqApi)
.defaultOptions(OpenAiChatOptions.builder().model("llama3-70b-8192").temperature(0.5).build())
.build();
// Derive a new OpenAiChatModel for GPT-4
OpenAiChatModel gpt4Model = baseChatModel.mutate()
.openAiApi(gpt4Api)
.defaultOptions(OpenAiChatOptions.builder().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 构建提示:
1. prompt()
: 此无参方法让您开始使用流式 API,允许您逐步构建用户消息、系统消息和其他提示部分。
2. prompt(Prompt prompt)
: 此方法接受一个 Prompt
参数,让您可以传入一个使用 Prompt 的非流式 API 创建的 Prompt
实例。
3. prompt(String content)
: 这是一个便捷方法,与前一个重载方法类似。它直接接收用户的文本内容作为用户消息。
ChatClient 响应
ChatClient API 通过其流式 API 提供了几种格式化 AI 模型响应的方法。
返回 ChatResponse
AI 模型的响应是一个由 ChatResponse
类型定义的丰富结构。它包含有关响应如何生成的元数据,并且可以包含多个响应(称为 Generations),每个响应都有自己的元数据。元数据包括用于创建响应的令牌数(每个令牌大约相当于 3/4 个单词)。此信息很重要,因为托管 AI 模型会根据每个请求使用的令牌数收费。
下面示例展示了如何在 call()
方法之后调用 chatResponse()
方法,返回包含元数据的 ChatResponse
对象。
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
返回实体 (Entity)
您通常希望将返回的字符串映射到一个实体类。`entity()` 方法提供了此功能。
例如,给定以下 Java record:
record ActorFilms(String actor, List<String> movies) {}
您可以使用 entity()
方法轻松地将 AI 模型的输出映射到此记录,如下所示:
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
还有一个重载的 entity
方法,其签名为 entity(ParameterizedTypeReference<T> type)
,允许您指定泛型集合等类型:
List<ActorFilms> actorFilms = chatClient.prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
流式响应 (Streaming Responses)
stream()
方法允许您获取异步响应,如下所示:
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();
您也可以使用 Flux<ChatResponse> chatResponse()
方法来流式传输 ChatResponse
。
未来,我们将提供一个便捷方法,让您通过响应式的 stream()
方法返回 Java 实体。目前,您应该使用结构化输出转换器(Structured Output Converter)来显式转换聚合后的响应,如下所示。这也演示了在流式 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<ActorFilms> actorFilms = this.converter.convert(this.content);
提示模板 (Prompt Templates)
ChatClient 流式 API 允许您提供包含变量的用户和系统文本模板,这些变量在运行时被替换。
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
,用于不需要模板处理的情况。
直接在 ChatClient 上配置的
TemplateRenderer
(通过.templateRenderer()
)仅适用于在 ChatClient 构建器链中直接定义的提示内容(例如通过.user()
,.system()
)。它不会影响像QuestionAnswerAdvisor
这样的顾问(Advisor)内部使用的模板,这些模板有自己的模板定制机制(参见自定义顾问模板)。
如果您希望使用不同的模板引擎,可以直接向 ChatClient 提供 TemplateRenderer
接口的自定义实现。您也可以继续使用默认的 StTemplateRenderer
,但使用自定义配置。
例如,默认情况下,模板变量由 {}
语法标识。如果您计划在提示中包含 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>")
.param("composer", "John Williams"))
.templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.call()
.content();
call() 的返回值
在 ChatClient 上指定 call()
方法后,有几种不同的响应类型选项:
* String content()
: 返回响应的字符串内容。
* ChatResponse chatResponse()
: 返回 ChatResponse
对象,该对象包含多个生成(generations)以及有关响应的元数据,例如用于创建响应的令牌数。
* ChatClientResponse chatClientResponse()
: 返回一个 ChatClientResponse
对象,该对象包含 ChatResponse
对象和 ChatClient 执行上下文,使您可以访问在执行顾问(advisors)期间使用的附加数据(例如,在 RAG 流程中检索到的相关文档)。
* entity()
返回 Java 类型:
* entity(ParameterizedTypeReference<T> type)
: 用于返回实体类型的集合。
* entity(Class<T> type)
: 用于返回特定的实体类型。
* entity(StructuredOutputConverter<T> structuredOutputConverter)
: 用于指定 StructuredOutputConverter
的实例,以将字符串转换为实体类型。
您也可以调用 stream()
方法代替 call()
。
stream() 的返回值
在 ChatClient 上指定 stream()
方法后,有几种响应类型选项:
* Flux<String> content()
: 返回 AI 模型正在生成的字符串的 Flux 流。
* Flux<ChatResponse> chatResponse()
: 返回 ChatResponse
对象的 Flux 流,其中包含有关响应的附加元数据。
* Flux<ChatClientResponse> chatClientResponse()
: 返回 ChatClientResponse
对象的 Flux 流,该对象包含 ChatResponse
对象和 ChatClient 执行上下文,使您可以访问在执行顾问(advisors)期间使用的附加数据(例如,在 RAG 流程中检索到的相关文档)。
使用默认值 (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 调用应用程序端点时,结果是:
```bash
❯ curl localhost:8080/ai/simple
{"completion":"为什么海盗要去喜剧俱乐部?为了听些'啊哈'级别的笑话!啊哈,伙计!"}
```
带参数的默认系统文本
在以下示例中,我们将在系统文本中使用占位符,以便在运行时而非设计时指定回复的语气。
@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();
}
}
@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 调用应用程序端点时,结果是:
```bash
http localhost:8080/ai voice=='Robert DeNiro'
{
"completion": "你在跟我说话吗?好吧,给你讲个笑话:为什么自行车自己站不起来?因为它'太累了'(双关:two tired / too tired)!经典吧?"
}
```
其他默认值
在 ChatClient.Builder
级别,您可以指定默认的提示配置。
* defaultOptions(ChatOptions chatOptions)
: 传入在 ChatOptions
类中定义的可移植选项,或特定于模型的选项(如 OpenAiChatOptions
中的选项)。有关特定于模型的 ChatOptions
实现的更多信息,请参阅 JavaDocs。
* defaultFunction(String name, String description, java.util.function.Function<I, O> function)
: name
用于在用户文本中引用该函数。`description` 解释函数的目的,并帮助 AI 模型选择正确的函数以生成准确的响应。`function` 参数是一个 Java 函数实例,模型将在必要时执行它。
* defaultFunctions(String… functionNames)
: 应用程序上下文中定义的 java.util.Function
的 bean 名称。
* defaultUser(String text)
, defaultUser(Resource text)
, defaultUser(Consumer<UserSpec> userSpecConsumer)
: 这些方法让您定义用户文本。`Consumer<UserSpec>` 允许您使用 lambda 表达式指定用户文本以及任何默认参数。
* defaultAdvisors(Advisor… advisor)
: 顾问(Advisors)允许修改用于创建提示(Prompt)的数据。`QuestionAnswerAdvisor` 实现通过向提示附加与用户文本相关的上下文信息,实现了检索增强生成(RAG)的模式。
* defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer)
: 此方法允许您定义一个 Consumer
来使用 AdvisorSpec
配置多个顾问。顾问可以修改用于创建最终提示的数据。`Consumer<AdvisorSpec>` 让您可以指定一个 lambda 表达式来添加顾问,例如 QuestionAnswerAdvisor
,它通过根据用户文本向提示附加相关上下文信息来支持检索增强生成。
您可以在运行时使用不带 default
前缀的相应方法覆盖这些默认值。
* options(ChatOptions chatOptions)
* function(String name, String description, java.util.function.Function<I, O> function)
* functions(String… functionNames)
* user(String text)
, user(Resource text)
, user(Consumer<UserSpec> userSpecConsumer)
* advisors(Advisor… advisor)
* advisors(Consumer<AdvisorSpec> advisorSpecConsumer)
顾问 (Advisors)
顾问(Advisors)API 提供了一种灵活而强大的方式来拦截、修改和增强 Spring 应用程序中 AI 驱动的交互。
使用用户文本调用 AI 模型时,一个常见模式是向提示中添加或补充上下文数据。
这种上下文数据可以是不同类型。常见类型包括:
* 您自己的数据:这是 AI 模型未训练过的数据。即使模型见过类似数据,附加的上下文数据在生成响应时也具有优先权。
* 对话历史:聊天模型的 API 是无状态的。如果您告诉 AI 模型您的名字,它不会在后续交互中记住它。对话历史必须随每个请求一起发送,以确保在生成响应时考虑之前的交互。
ChatClient 中的顾问配置
ChatClient 流式 API 提供了一个 AdvisorSpec
接口用于配置顾问。此接口提供添加参数、一次性设置多个参数以及向链中添加一个或多个顾问的方法。
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}
顾问添加到链中的顺序至关重要,因为它决定了它们的执行序列。每个顾问都会以某种方式修改提示或上下文,并且一个顾问所做的更改会传递给链中的下一个顾问。
ChatClient.builder(chatModel)
.build()
.prompt()
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.user(userText)
.call()
.content();
在此配置中,`MessageChatMemoryAdvisor` 将首先执行,将对话历史添加到提示中。然后,`QuestionAnswerAdvisor` 将基于用户的问题和添加的对话历史执行其搜索,可能提供更相关的结果。
了解问题解答顾问 (Question Answer Advisor)
检索增强生成 (Retrieval Augmented Generation)
请参阅检索增强生成指南。
日志记录 (Logging)
SimpleLoggerAdvisor
是一个记录 ChatClient 请求和响应数据的顾问。这对于调试和监控您的 AI 交互非常有用。
Spring AI 支持对 LLM 和向量存储交互的可观测性。有关更多信息,请参阅可观测性指南。
要启用日志记录,请在创建 ChatClient 时将其添加到顾问链中。建议将其添加到链的末尾附近:
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.user("Tell me a joke?")
.call()
.chatResponse();
要查看日志,请将顾问包的日志级别设置为 DEBUG
:
```properties
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
```
将此添加到您的 application.properties
或 application.yaml
文件中。
您可以通过使用以下构造函数来自定义从 AdvisedRequest
和 ChatResponse
记录哪些数据:
SimpleLoggerAdvisor(
Function<AdvisedRequest, String> requestToString,
Function<ChatResponse, String> responseToString
)
示例用法:
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
request -> "Custom request: " + request.userText,
response -> "Custom response: " + response.getResult()
);
这使您可以根据特定需求定制记录的信息。
注意: 在生产环境中记录敏感信息时要格外小心。
聊天记忆 (Chat Memory)
ChatMemory
接口表示聊天对话记忆的存储。它提供了向对话添加消息、从对话中检索消息以及清除对话历史的方法。
目前有一个内置实现:`MessageWindowChatMemory`。
MessageWindowChatMemory
是一种聊天记忆实现,它维护一个最多达到指定最大大小(默认值:20 条消息)的消息窗口。当消息数量超过此限制时,较旧的消息会被逐出,但系统消息会被保留。如果添加了新的系统消息,所有以前的系统消息将从记忆中移除。这确保了对话的最新上下文始终可用,同时将内存使用控制在有限范围内。
MessageWindowChatMemory
由 ChatMemoryRepository
抽象支持,该抽象为聊天对话记忆提供了存储实现。有几种可用的实现,包括 InMemoryChatMemoryRepository
、`JdbcChatMemoryRepository`、`CassandraChatMemoryRepository` 和 Neo4jChatMemoryRepository
。
有关更多详细信息和用法示例,请参阅聊天记忆文档。
实现说明 (Implementation Notes)
ChatClient 中命令式和响应式编程模型的结合使用是该 API 的一个独特方面。通常,应用程序要么是响应式的,要么是命令式的,但不会同时使用两者。
在自定义模型的 HTTP 客户端交互时,必须同时配置
RestClient
和WebClient
。
由于 Spring Boot 3.4 中的一个错误,必须设置属性
"spring.http.client.factory=jdk"
。否则,它默认为"reactor"
,这会破坏某些 AI 工作流,如图像模型(ImageModel)。
流式传输仅通过响应式(Reactive)堆栈支持。因此,命令式应用程序必须包含响应式堆栈(例如
spring-boot-starter-webflux
)。非流式传输仅通过 Servlet 堆栈支持。因此,响应式应用程序必须包含 Servlet 堆栈(例如
spring-boot-starter-web
),并预期某些调用是阻塞的。工具调用(Tool calling)是命令式的,会导致阻塞工作流。这也会导致部分/中断的 Micrometer 观测(例如,ChatClient 的 spans 和工具调用的 spans 没有连接,第一个 span 因此保持不完整)。
内置的顾问(advisors)对标准调用执行阻塞操作,对流式调用执行非阻塞操作。用于顾问流式调用的 Reactor Scheduler 可以通过每个 Advisor 类上的 Builder 进行配置。
评论区