SpringAI 统一门面接口 ChatClient
ChatClient
通过 Fluent API 模式来统一设置提示语 / 提示语模板 / 模型参数 / 工具与 MCP / 记忆 Memory / Advisor 等,同时提供了同步和流式调用模式,通过类型转换器转换为标准 Java 实体等能力,方便的构建自己的 AI 程序。
创建 ChatClient 的两种方式
@Configuration
public class SpringAIConfig {
/**
* 第一种创建 ChatClient 的方式,使用默认的 builder 参数
*
* @param chatModel 该对象会根据配置自动生成
* @return ChatClient 对象。
*/
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.create(chatModel);
}
/**
* 第二种创建 ChatClient 的方式,使用 builder 来设置自定义参数
*
* @return ChatClient 对象。
*/
@Bean
public ChatClient customChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("You are a helpful assistant.")
.build();
}
}
说明:ChatModel
模型会根据配置自动注入。
启动 ChatClient Fluent API
ChatClient Fluent API
提供了以下三种 prompt 方法来启动 Fluent API:
方法定义:
ChatClientRequestSpec prompt();
ChatClientRequestSpec prompt(String content);
ChatClientRequestSpec prompt(Prompt prompt);
使用:
@RestController
public class Controller {
@Resource
private ChatClient chatClient;
@RequestMapping("/1")
public String execute1(@RequestParam("userRequest") String userRequest) {
return chatClient.prompt().user(userRequest).call().content();
}
@RequestMapping("/2")
public String execute2(@RequestParam("userRequest") String userRequest) {
return chatClient.prompt(new Prompt(userRequest)).call().content();
}
@RequestMapping("/3")
public String execute3(@RequestParam("userRequest") String userRequest) {
return chatClient.prompt(userRequest).call().content();
}
}
提示语模板 PromptTemplate
/**
* 使用默认提示语模板 StTemplateRenderer
*/
@RequestMapping("/100")
public String execute100() {
return chatClient.prompt()
.user(u -> u.text("Tell me the names of 5 movies those act by {actor}").param("actor", "周星驰"))
.call().content();
}
/**
* 定制提示语模板
*/
@RequestMapping("/101")
public String execute101() {
return chatClient.prompt()
.user(u -> u.text("Tell me the names of 5 movies those act by <actor>").param("actor", "刘德华"))
.templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.call().content();
}
说明:
- ChatClient 提供了 user 提示语模板和 system 提示语模板, 其中的变量可以在运行时被替换
- 默认使用 StTemplateRenderer 引擎,如果想要使用自定义的模板引擎,可以自己实现接口
TemplateRenderer
- 默认使用
{
作为参数占位符起始标识,使用}
作为参数占位符结束标识,当发现这两个字符会被提示语或者参数占用时,可以定制提示语模板参数占位符分隔符,如上execute101()
方法所示
ChatClient 响应
从返回方式上分为:同步(call()
)和流式(stream()
)。
同步响应
call()
响应类型有几种不同的选项:
String content()
:返回响应的字符串内容ChatResponse chatResponse()
:ChatResponse
包含响应的元数据的对象,例如,token 的消耗ChatClientResponse chatClientResponse()
:ChatClientResponse
包含ChatResponse
对象和ChatClient
执行上下文的对象,使您能够访问advisors
执行期间使用的附加数据(例如,在RAG
流中检索到的相关文档)ResponseEntity<?> responseEntity()
:ResponseEntity
包含完整 HTTP 响应(包括状态码、标头和正文)的响应entity()
:返回 Java 类型entity(ParameterizedTypeReference<T> type)
:返回Collection
实体类型entity(Class<T> type)
:返回特定的实体类型entity(StructuredOutputConverter<T> structuredOutputConverter)
:指定StructuredOutputConverter
,将String
为T
类型。
说明:调用 call()
方法并不会真正触发 AI 模型的执行。它只是指示 Spring AI
是使用同步调用还是流式调用。实际的 AI 模型调用发生在调用 content()
、chatResponse()
和 responseEntity()
等方法时。
/**
* 返回 String
*/
@RequestMapping("/1")
public String execute1(@RequestParam("userRequest") String userRequest) {
return chatClient.prompt().user(userRequest).call().content();
}
/**
* 返回 ChatResponse
* 包含 token 信息
*/
@RequestMapping("/4")
public ChatResponse execute4(@RequestParam("userRequest") String userRequest) {
return chatClient.prompt(userRequest).call().chatResponse();
}
/**
* 返回 ResponseEntity
*/
@RequestMapping("/40")
public org.springframework.ai.chat.client.ResponseEntity<ChatResponse, String> execute40() {
return chatClient.prompt("Tell me a joke").call().responseEntity(String.class);
}
/**
* 返回 Java 类
*/
@RequestMapping("/5")
public ActorFilms execute5() {
return chatClient.prompt("Generate the filmography for a random actor.").call().entity(ActorFilms.class);
}
/**
* 返回 Java 列表
*/
@RequestMapping("/6")
public List<ActorFilms> execute6() {
return chatClient.prompt("Generate the filmography of 5 movies for 周星驰 and 刘德华.").call().entity(new ParameterizedTypeReference<>() {
});
Java 类如下:
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@Accessors(chain = true)
public class ActorFilms {
private String actor;
private List<String> films;
}
流式响应
stream()
响应类型有几种不同的选项:
Flux<String> content()
:返回响应的字符串内容Flux<ChatResponse> chatResponse()
:ChatResponse
包含响应的元数据的对象Flux<ChatClientResponse> chatClientResponse()
:ChatClientResponse
包含ChatResponse
对象和ChatClient
执行上下文的对象
说明:stream()
调用方式下,无法直接返回 Java 对象,需要借助类型转换器进行转换,见如下的代码操作。
/**
* 流式返回 String
* 包含 token 信息
*/
@RequestMapping("/7")
public Flux<String> execute7() {
return chatClient.prompt("Tell me a joke").stream().content();
}
/**
* 流式返回 ChatResponse
* 包含 token 信息
*/
@RequestMapping("/8")
public Flux<ChatResponse> execute8() {
return chatClient.prompt("Tell me a joke").stream().chatResponse();
}
/**
* 流式返回信息,转换为 Java 类
*/
@RequestMapping("/9")
public ActorFilms execute9() {
/*
* 创建 Converter
*/
BeanOutputConverter<ActorFilms> converter = new BeanOutputConverter<>(ActorFilms.class);
/*
* 流式调用
*/
Flux<String> flux = chatClient.prompt().user(
u -> u.text("""
Generate the filmography for a random actor.
{format}
""")
.param("format", converter.getFormat())
).stream().content();
String content = flux.collectList().block().stream().collect(Collectors.joining());
/*
* 转换
*/
return converter.convert(content);
}
/**
* 流式返回信息,转换为 Java 列表
*/
@RequestMapping("/10")
public List<ActorFilms> execute10() {
/*
* 创建 Converter
*/
BeanOutputConverter<List<ActorFilms>> converter = new BeanOutputConverter<>(new ParameterizedTypeReference<>() {
});
/*
* 流式调用
*/
Flux<String> flux = chatClient.prompt().user(
u -> u.text("""
Generate the filmography for a random actor.
{format}
""")
.param("format", converter.getFormat())
).stream().content();
String content = flux.collectList().block().stream().collect(Collectors.joining());
/*
* 转换
*/
return converter.convert(content);
}
类型转换器会根据返回的不同的 Java 模型制定不同的 format 提示语。例如对于返回 ActorFilms
,format 提示语如下(注意观察 schema 部分):
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"actor" : {
"type" : "string"
},
"films" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
},
"additionalProperties" : false
}```
对于返回 List<ActorFilms>
,format 提示语如下(注意观察 schema 部分):
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"actor" : {
"type" : "string"
},
"films" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
},
"additionalProperties" : false
}
}```
ChatClient 默认选项设置
使用 ChatClient.Builder
创建 ChatClient
实例时,可以全局指定以下的默认配置项来设置默认配置:
defaultOptions
:核心是模型参数配置,例如温度,在模型介绍的章节会详细介绍defaultSystem
:system 提示语,可以包含参数defaultUser
:user 提示语,可以包含参数defaultTemplateRenderer
:提示语模板defaultToolCallbacks
/defaultTools
/defaultToolContext
/defaultToolNames
:工具与 mcpdefaultAdvisors
:advisors
可以在运行时使用不带前缀的相应方法覆盖这些默认值。
options
system
user
templateRenderer
toolCallbacks
/tools
/toolContext
/toolNames
advisors
在运行时给带参数占位符的默认配置项设置参数值。
@Configuration
public class SpringAIConfig {
@Bean
public ChatClient customChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("You are a helpful assistant.{actor}")
.build();
}
}
@RequestMapping("/1000")
public String execute1000() {
return chatClient.prompt()
.system(s -> s.param("actor", "周星驰")) // 直接设置参数
.user("tell me a joke").call().content();
}
Advisors
Advisors
提供了一种灵活而强大的方法来拦截、修改和增强 Spring AI
应用程序。
例如,使用 user 提示语调用 AI 模型时的一个常见模式是 使用上下文数据扩充提示
。 这些上下文数据可以是不同类型的。常见的类型包括:
- 企业私有数据:这是 AI 模型尚未训练过的数据
- 对话历史记录:聊天模型的 API 是无状态的。如果您告诉 AI 模型您的姓名,它不会在后续交互中记住它。每次请求都必须发送对话历史记录,以确保在生成响应时考虑到之前的交互
ChatClient
提供了一个 AdvisorSpec
接口用于配置 Advisor
的接口。该接口提供了添加参数、一次性设置多个参数以及将一个或多个 Advisors
添加到链中的方法。
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}
关于 Advisors
更多细节,我们在后续的章节继续深入,这里我们以一个简单的日志记录为例,来了解其基础用法。
SimpleLoggerAdvisor
一个用于记录请求模型的 request 和模型返回的 response 数据的 Advisor
。可以用于调试和监控 AI 交互。
application.properties 文件种新增如下配置,开启日志:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
/**
* 使用默认的日志方式
*/
@RequestMapping("/10000")
public String execute10000() {
return chatClient.prompt()
.advisors(new SimpleLoggerAdvisor())
.user("tell me a joke")
.call().content();
}
/**
* 定制日志
*/
@RequestMapping("/10001")
public String execute10001() {
return chatClient.prompt()
.advisors(new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(),
response -> "Custom response: " + response.getResult(),
0
))
.user("tell me a joke")
.call().content();
}
注意:假设有多个 Advisors
,每个 Advisor
添加到链中的顺序至关重要,因为它决定了它们的执行顺序。每个 Advisor
都会以某种方式修改提示或上下文,并且一个 Advisor
所做的更改会传递给链中的下一个 Advisor
。
ChatMemory
ChatMemory
接口表示聊天对话内存存储。它提供了向对话添加消息、从对话中检索消息以及清除对话历史记录的方法;对话需要设置 conversationId
,用于不同的对话区分。
接口定义如下:
public interface ChatMemory {
String DEFAULT_CONVERSATION_ID = "default";
/**
* The key to retrieve the chat memory conversation id from the context.
*/
String CONVERSATION_ID = "chat_memory_conversation_id";
/**
* Save the specified message in the chat memory for the specified conversation.
*/
default void add(String conversationId, Message message) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(message, "message cannot be null");
this.add(conversationId, List.of(message));
}
/**
* Save the specified messages in the chat memory for the specified conversation.
*/
void add(String conversationId, List<Message> messages);
/**
* Get the messages in the chat memory for the specified conversation.
*/
List<Message> get(String conversationId);
/**
* Clear the chat memory for the specified conversation.
*/
void clear(String conversationId);
}
ChatMemory
有一个内置实现:MessageWindowChatMemory
维护一个消息窗口,窗口大小不超过指定的最大限制(默认值:20 条消息)。当消息数量超过此限制时,较旧的消息将被移除,但 system 消息将被保留。如果添加了新的 system 消息,所有先前的 system 消息都将从内存中删除。这确保了对话始终可以使用最新的上下文,同时保持内存使用量有限。
MessageWindowChatMemory
内部通过 ChatMemoryRepository
接口提供的内存实现来实现消息存储,ChatMemoryRepository
的实现包括 InMemoryChatMemoryRepository
/ JdbcChatMemoryRepository
/ CassandraChatMemoryRepository
/ Neo4jChatMemoryRepository
等,在 memory 章节我们会详细介绍。
ChatClient
是通过 Advisors
机制来使用 ChatMemory
的。
@Bean
public ChatMemory messageWindowChatMemory() {
return MessageWindowChatMemory.builder().maxMessages(10).build();
}
@Resource
private ChatMemory messageWindowChatMemory;
@RequestMapping("/20000")
public String execute20000() {
return chatClient.prompt()
.advisors(
MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build(),
new SimpleLoggerAdvisor())
.user("tell me a joke")
.call().content();
}
文章的最后,如果您觉得本文对您有用,请打赏一杯咖啡!感谢!
