文章目录
ChatClient
提供了与AI模型交互的fluent API。它同时支持同步和流式编程模型。
ChatClient
fluent API拥有构建传递给AI模型的提示(Prompt)的组成部分的方法。提示包含指导AI模型输出和行为的指令文本。从API的角度来看,提示由一系列消息组成。
AI模型处理两种主要类型的消息:
- 用户消息,即用户的直接输入。
- 系统消息,由系统生成以指导对话。
这些消息通常包含占位符,这些占位符在运行时会根据用户输入进行替换,以自定义AI模型对用户输入的响应。
还可以指定一些提示选项,例如:
- AI模型的名称,即要使用的AI模型的名称。
- 温度设置,控制生成输出的随机性或创造性。
这些功能使得ChatClient成为一个强大的工具,允许开发者以灵活的方式与AI模型进行交互,并通过定制化的提示和消息来优化AI模型的响应。
创建 ChatClient
使用 ChatClient.Builder
对象创建 ChatClient
。你可以为任何 ChatModel SpringBoot 自动配置获取自动配置的 ChatClient.Builder
实例,或者手动创建一个。
使用自动配置的 ChatClient.Builder
在最简单的用例中,Spring AI 通过 SpringBoot 自动配置创建了一个 ChatClient.Builder
实例原型,使用时可以将其注入到类中。以下是一个简单的示例,用于检索对简单用户请求的字符串响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@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 模型的响应作为字符串返回。
手动创建 ChatClient
可以通过设置属性 spring.ai.chat.client.enabled=false
来禁用 ChatClient.Builder
自动配置。当同时使用多个聊天模型时,这个配置会很有用。然后,为每个需要的 ChatModel
手动创建一个 ChatClient.Builder
实例:
1 2 3 4 5 6 7 |
ChatModel myChatModel = ... // 通常通过自动装配获得 ChatClient.Builder builder = ChatClient.builder(this.myChatModel); // 或者使用默认构建器创建一个 ChatClient 实例: ChatClient chatClient = ChatClient.create(this.myChatModel); |
ChatClient
fluent API 允许使用重载的 prompt()
方法以三种不同的方式创建提示(Prompt),以启动fluent API:
prompt()
: 此方法不接受任何参数就可以开始使用fluent API,允许您构建用户、系统和其它提示(prompt)部分。prompt(Prompt prompt)
: 此方法接受一个Prompt
实例作为参数,这个参数可以是一个非fluent API 创建的Prompt
实例。prompt(String content)
: 这是一个快捷方法,类似于之前重载的方法,它接受用户文本内容作为参数。
ChatClient 响应
ChatClient
API 提供了几种格式化 AI 模型响应内容的方法。
返回 ChatResponse
AI 模型的响应是一个由类型 ChatResponse
定义的复杂结构。ChatResponse
中包含关于生成响应的元数据,并且还可以包含多个响应,称为 Generations,每个Generation
都有自己的元数据。元数据还包括用于创建响应的token数量(每个token大概是 3/4 个单词)。这个信息很重要,因为托管的 AI 模型会根据每个请求中使用的token数量收费。
以下示例展示了如何获取包含元数据的 ChatResponse
对象的过程,ChatResponse
实例是在 call()
方法后执行 chatResponse()
方法获得:
1 2 3 4 |
ChatResponse chatResponse = chatClient.prompt() .user("Tell me a joke") .call() .chatResponse(); |
返回Entity
有时会希望将返回的字符串映射为某个特定的实体类的对象。entity()
方法提供了这个功能。
比如下面的 Java record类:
1 |
record ActorFilms(String actor, List<String> movies) {} |
可以使用 entity()
方法轻松地将 AI 模型的输出结果映射为这个record类,如下所示:
1 2 3 4 |
ActorFilms actorFilms = chatClient.prompt() .user("Generate the filmography for a random actor.") .call() .entity(ActorFilms.class); |
还有一个重载的 entity()
方法,方法签名为 entity(ParameterizedTypeReference type)
,让您可以指定更复杂的类型,如List泛型:
1 2 3 4 |
List<ActorFilms> actorFilms = chatClient.prompt() .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.") .call() .entity(new ParameterizedTypeReference<List<ActorFilms>>() {}); |
流式响应
使用 stream()
方法可以像下面这样获得异步响应:
1 2 3 4 |
Flux<String> output = chatClient.prompt() .user("Tell me a joke") .stream() .content(); |
也可以使用 Flux<ChatResponse> chatResponse()
方法流式传输 ChatResponse
对象结果。
Spring AI将提供一个更便捷的方法,让开发者能够使用反应式 stream()
方法返回 Java Entity结果。与此同时,还可以使用结构化输出转换器( Structured Output Converter )来显式转换聚合的响应结果,就跟下面的例子一样。这个例子里也展示了fluent API 中参数的使用,具体将在文档的后续部分详细讨论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); |
call()
方法返回值
在 ChatClient
上指定 call()
方法后,有如下几种不同的响应类型选项。
String content()
: 返回字符串格式的响应结果ChatResponse chatResponse()
: 返回包含多个Generation
和响应元数据的ChatResponse
对象,例如用于创建响应的token数量。entity()
返回指定 Java 类型的返回结果entity(ParameterizedTypeReference<T> type)
: 用于返回集合类型的结果(支持泛型)。entity(Class type)
: 用于返回特定类型的结果。entity(StructuredOutputConverter structuredOutputConverter)
: 可以指定StructuredOutputConverter
实例,将字符串转换为需要的类型。
也可以使用 stream()
方法来替换 call()
方法。
stream() 方法返回值
在 ChatClient
上指定 stream()
方法后,有几种响应类型选项:
Flux<String> content()
: 返回 AI 模型生成的字符串Flux
对象。Flux chatResponse()
: 返回包含响应的额外元数据的ChatResponse
对象的Flux
对象。
使用默认值
使用默认系统文本在 @Configuration
类中创建ChatClient
可以 简化运行时代码。通过设置默认值,在调用 ChatClient
时只需要指定用户文本,这样在运行时代码路径中就不需要再为每个请求设置系统文本了。
默认系统文本
以下面的例子中,我们将系统文本配置为始终以海盗的声音回复。为了避免在运行时代码中重复系统文本,我们将在@Configuration
类中创建一个ChatClient
实例:
1 2 3 4 5 6 7 8 9 10 |
@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(); } } |
以及一个调用这个ChatClient
实例的 @RestController
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@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 命令来调用这个接口,收到的返回结果是:
1 2 |
❯ curl localhost:8080/ai/simple {"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"} |
带有参数的默认系统文本
以下示例中,我们将在系统文本中通过占位符在运行时指定声音类型而不是设计时指定声音类型。
1 2 3 4 5 6 7 8 9 10 |
@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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@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 调用应用程序接口的结果如下:
1 2 3 4 |
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
级别,可以指定默认的提示(prompt)配置:
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)
: 此方法支持使用AdvisorSpec
配置多个Advisor
。Advisor
可以修改用于创建最终**提示(Prompt)**的数据。Consumer<AdvisorSpec>
通过指定一个 lambda 来添加顾问,如QuestionAnswerAdvisor
,它通过将提示与基于用户文本的相关上下文信息附加在一起来支持检索增强生成(RAG)。
可以使用没有 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 userSpecConsumer)
advisors(Advisor… advisor)
advisors(Consumer advisorSpecConsumer)
Advisor
Advisor
API 提供了一种灵活而强大的方式,用于拦截、修改和增强 Spring 应用程序中的AI驱动交互。
使用用户文本调用 AI 模型时的通用模式是用上下文数据追加或增强提示。
这些上下文数据可以是不同类型的。常见类型包括:
- 您自己的数据: 这是 AI 模型未经训练的数据。即使模型已经看到过类似的数据,追加的上下文数据在生成响应时仍会被优先考虑。
- 对话历史记录: 聊天模型的 API 是无状态的。如果你告诉 AI 模型你的名字,它在后续交互中也不会记住它。必须将对话历史记录与每个请求一起发送,以确保在生成响应时考虑之前的交互。
ChatClient 中的 Advisor
配置
ChatClient
的 fluent API 提供了一个AdvisorSpec
接口来配置Advisor。这个接口提供了添加参数、一次设置多个参数和向调用链中添加一个或多个Advisor的方法。
1 2 3 4 5 6 |
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以某种方式修改提示或上下文,一个Advisor所做的更改会传递给链中的下一个。
1 2 3 4 5 6 7 8 9 10 |
ChatClient.builder(chatModel) .build() .prompt() .advisors( new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(vectorStore) ) .user(userText) .call() .content(); |
在此配置中,MessageChatMemoryAdvisor
将首先执行,将对话历史记录添加到提示中。然后QuestionAnswerAdvisor
将根据用户的问题和添加的对话历史记录执行搜索,这样就可能会提供更相关的结果。
检索增强生成(RAG)
向量数据库存储了 AI 模型不知道的数据。当用户问题发送到 AI 模型时,QuestionAnswerAdvisor
会为与用户问题相关的文档查询向量数据库。
向量数据库的响应被追加到用户文本中,为 AI 模型生成响应提供上下文。
假设已经将数据加载到 VectorStore
中,就可以通过向 ChatClient
提供 QuestionAnswerAdvisor
实例来执行检索增强生成(RAG)。
1 2 3 4 5 6 |
ChatResponse response = ChatClient.builder(chatModel) .build().prompt() .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())) .user(userText) .call() .chatResponse(); |
在此示例中,SearchRequest.defaults()
将在向量数据库中对所有文档执行相似性搜索。要限制搜索的文档类型,SearchRequest
接受一个 SQL 样式的过滤表达式,该表达式在所有 VectorStores
中都是可移植的。
动态过滤表达式
使用 FILTER_EXPRESSION Advisor上下文参数在运行时更新 SearchRequest
过滤表达式:
1 2 3 4 5 6 7 8 9 10 |
ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())) .build(); // 在运行时更新过滤表达式 String content = this.chatClient.prompt() .user("Please answer my question XYZ") .advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Spring'")) .call() .content(); |
FILTER_EXPRESSION 参数允许根据提供的表达式动态过滤搜索结果。
聊天记忆
ChatMemory
接口表示对聊天对话历史的存储。它提供将消息添加到对话、从对话中检索消息和清除对话历史记录的方法。
目前有两种实现,InMemoryChatMemory
和 CassandraChatMemory
,分别提供基于内存和TTL持久存储的聊天对话历史记录存储支持。
创建TTL的 CassandraChatMemory
:
1 |
CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build()); |
以下Advisor的实现使用 ChatMemory
接口与对话历史记录来建议提示,它们在如何将内存添加到提示的细节上有所不同:
MessageChatMemoryAdvisor
:内存被检索并作为一系列消息添加到提示中PromptChatMemoryAdvisor
:内存被检索并添加到提示的系统文本中。VectorStoreChatMemoryAdvisor
:构造函数VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order)
允许您:- 指定用于管理和查询文档的
VectorStore
实例。 - 设置如果在上下文中未提供,则使用的默认对话 ID。
- 定义以token数量为单位的聊天历史记录检索窗口规格。
- 提供用于聊天Advisor系统的系统文本建议。
- 设置Advisor在链中的优先级顺序。
- 指定用于管理和查询文档的
VectorStoreChatMemoryAdvisor.builder()
方法用于指定默认对话 ID、聊天历史记录窗口大小和要检索的聊天历史的顺序。
以下是一个使用多个Advisor的 @Service
实现示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY; import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY; @Service public class CustomerSupportAssistant { private final ChatClient chatClient; public CustomerSupportAssistant(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) { this.chatClient = builder .defaultSystem(""" You are a customer chat support agent of an airline named "Funnair". Respond in a friendly, helpful, and joyful manner. Before providing information about a booking or cancelling a booking, you MUST always get the following information from the user: booking number, customer first name and last name. Before changing a booking you MUST ensure it is permitted by the terms. If there is a charge for the change, you MUST ask the user to consent before proceeding. """) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORY new QuestionAnswerAdvisor(vectorStore), // RAG new SimpleLoggerAdvisor()) .defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // FUNCTION CALLING .build(); } public Flux<String> chat(String chatId, String userMessageContent) { return this.chatClient.prompt() .user(userMessageContent) .advisors(a -> a .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) .stream().content(); } } |
日志记录
SimpleLoggerAdvisor
是一个记录 ChatClient
请求和响应数据的Advisor。这对于调试和监控 AI 交互很有用。
Spring AI 支持对 LLM 和向量存储交互的可观察性。有关更多信息,请参考可观测性指南。
要启用日志记录,在创建 ChatClient
实例时需要将 SimpleLoggerAdvisor
添加到Advisor链中。建议将其添加到链的末尾:
1 2 3 4 5 |
ChatResponse response = ChatClient.create(chatModel).prompt() .advisors(new SimpleLoggerAdvisor()) .user("Tell me a joke?") .call() .chatResponse(); |
要查看日志,将顾问包的日志级别设置为 DEBUG
:
1 |
logging.level.org.springframework.ai.chat.client.advisor=DEBUG |
将这行配置添加到 application.properties
或 application.yam
文件中。
可以使用以下构造函数自定义记录的 AdvisedRequest
和 ChatResponse
数据:
1 2 3 4 |
SimpleLoggerAdvisor( Function<AdvisedRequest, String> requestToString, Function<ChatResponse, String> responseToString ) |
示例用法:
1 2 3 4 |
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor( request -> "Custom request: " + request.userText, response -> "Custom response: " + response.getResult() ); |
这允许根据特定需求定制日志信息。
在生产环境中记录敏感信息时要小心。
END!!
发表评论