|
如需最新的快照版本,请使用 Spring AI 1.1.3! |
聊天客户端API
ChatClient 提供了一种流畅的API来与AI模型通信。它支持同步和流式编程模型。
|
见本文件底部有关在 |
AI模型处理两种主要类型的消息:用户消息,这些是直接来自用户的输入;系统消息,这些是由系统生成的以引导对话的消息。
这些消息通常包含占位符,在运行时根据用户输入进行替换,以自定义AI模型对用户输入的响应。
也有可以指定的提示选项,例如要使用的AI模型名称以及控制生成输出随机性或创造力的温度设置。
创建 ChatClient
ChatClient 是使用一个 ChatClient.Builder 对象创建的。
你可以通过任何ChatModel Spring Boot 自动配置获取一个自动配置好的ChatClient.Builder 实例,或者程序化地创建一个。
使用自动配置的ChatClient.Builder
在最简单的使用案例中,Spring AI 提供了 Spring Boot 自动配置功能,为你创建一个原型 ChatClient.Builder bean,你可以将其注入到你的类中。
这里是一个简单的示例,展示如何检索一个针对简单用户请求的 String 响应。
@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。但是,您可能需要在应用程序中使用多个聊天模型。以下是处理这种情况的方法:
在所有情况下,您需要通过设置属性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);
}
}
然后可以使用 @0 注解将这些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
The ChatClient 流畅API允许您通过重载的 prompt 方法以三种不同的方式创建提示:
-
prompt(): 这个没有参数的方法让你能够开始使用流畅API,允许你构建用户、系统以及其他部分的提示。 -
prompt(Prompt prompt): 这个方法接受一个Prompt参数,让你可以使用 Prompt 的非流畅 API 创建的Prompt实例进行传递。 -
prompt(String content): 这是一个与上一个重载类似的便捷方法。它接收用户的文本内容。
ChatClient 响应
The ChatClient API 提供了几种使用流畅API格式化AI模型响应的方法。
返回聊天响应
The response from the AI model is a rich structure defined by the type ChatResponse.
It includes metadata about how the response was generated and can also contain multiple responses, known as 生成s, each with its own metadata.
The metadata includes the number of tokens (each token is approximately 3/4 of a word) used to create the response. This information is important because hosted AI models charge based on the number of tokens used per request.
以下是一个示例,通过调用call()方法后的chatResponse()方法,返回包含元数据的ChatResponse对象。
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
返回实体
您经常希望返回一个由返回的String映射的实体类。entity()方法提供了此功能。
例如,给定以下Java记录:
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(ParameterizedTypeReference<T> type)的重载entity方法,该方法允许您指定如泛型列表这样的类型:
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() 方法可以让您获取异步响应,如下所示:
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();
您也可以通过方法Flux<ChatResponse> chatResponse()流式处理ChatResponse。
在未来,我们将提供一个方便的方法,让您可以通过响应式的stream()方法返回一个Java实体。
在此期间,您应该使用结构化输出转换器显式地将聚合响应进行转换,如下所示。
这也展示了将在文档的后续部分中详细介绍的在流畅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);
提示模板
The 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())。
它不会影响由 Advisors(如 QuestionAnswerAdvisor)内部使用的模板,这些顾问拥有自己的模板自定义机制(请参阅 自定义顾问模板)。 |
如果希望使用不同的模板引擎,可以直接将自定义的TemplateRenderer接口实现提供给ChatClient。您也可以继续使用默认的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() 返回值
在指定 call() 方法于 ChatClient 后,响应类型有几种不同的选择。
-
String content(): 返回响应的内容字符串 -
ChatResponse chatResponse(): 返回包含多个生成结果和响应元数据的对象,例如创建响应时使用了多少个Tokens。 -
ChatClientResponse chatClientResponse(): 返回一个包含ChatResponse对象和 ChatClient 执行上下文的ChatClientResponse对象,让你可以访问执行顾问期间使用的附加数据(例如,在 RAG 流程中检索的相关文档)。 -
ResponseEntity<?> responseEntity(): 返回一个ResponseEntity,其中包含完整的 HTTP 响应,包括状态码、头部和正文。这在您需要访问响应的低级 HTTP 详细信息时非常有用。 -
entity()用于返回Java类型-
entity(ParameterizedTypeReference<T> type): 用于返回实体类型的Collection。 -
entity(Class<T> type): 用于返回特定实体类型。 -
entity(StructuredOutputConverter<T> structuredOutputConverter): 用于指定一个StructuredOutputConverter的实例,将其转换为一种String实体类型。
-
可以调用stream()方法而不是call()方法。
调用 call() 方法实际上并不会触发 AI 模型的执行。相反,它只是指示 Spring AI 是否使用同步或流式调用。实际的 AI 模型调用发生在调用 content()、chatResponse() 和 responseEntity() 等方法时。 |
stream() 返回值
在为ChatClient指定stream()方法后,响应类型有几种选择:
-
Flux<String> content(): 返回由AI模型生成的字符串的Flux。 -
Flux<ChatResponse> chatResponse(): 返回一个Flux的ChatResponse对象,该对象包含关于响应的附加元数据。 -
Flux<ChatClientResponse> chatClientResponse(): 返回包含ChatClientResponse对象和ChatResponse对象的Flux对象,为您提供在顾问执行过程中使用的附加数据访问权限(例如,在RAG流程中检索的相关文档)。
使用默认设置
创建一个ChatClient类中的默认系统文本的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 调用应用端点时,结果为:<br />
❯ 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) {
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 调用应用端点的结果是:<br/>
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。 -
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实现通过在与用户文本相关的上下文信息后附加提示词,实现了Retrieval Augmented Generation模式。 -
defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): 此方法允许您定义一个Consumer来使用AdvisorSpec配置多个顾问。顾问可以修改用于创建最终Prompt的数据。Consumer<AdvisorSpec>允许您指定一个 lambda 来添加顾问,例如QuestionAnswerAdvisor,它通过根据用户文本在提示中附加相关上下文信息来支持Retrieval Augmented Generation。
可以使用对应的方法在运行时覆盖这些默认值,而不需要使用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)
顾问
The 顾问API 提供了一种灵活且强大的方式,用于拦截、修改和增强您Spring应用程序中的AI驱动交互。
使用用户文本调用AI模型时,一个常见的模式是将上下文数据附加到提示词或扩充提示词。
此上下文数据可以是不同类型的。常见类型包括:
-
您自己的数据:这是AI模型未经训练的数据。即使模型见过类似数据,附加的上下文数据在生成响应时具有优先权。
-
对话历史:聊天模型的 API 是无状态的。如果你告诉 AI 模型你的名字,它在后续交互中不会记住。对话历史必须随每个请求一起发送,以确保在生成响应时考虑之前的交互。
ChatClient 中的 Advisor 配置
ChatClient 流式 API 提供了一个用于配置 advisor 的 AdvisorSpec 接口。该接口提供了添加参数、一次性设置多个参数以及向链中添加一个或多个 advisor 的方法。
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 将根据用户的问题和添加的对话历史执行其搜索,可能会提供更相关的结果。
检索增强生成
参见检索增强生成指南。
日志记录
The SimpleLoggerAdvisor 是一个顾问,记录 request 和 response 关于 ChatClient 的数据。这在调试和监控您的 AI 交互时可能会很有用。
| Spring AI 支持 LLM 和向量存储交互的可观测性。有关更多信息,请参阅 可观测性 指南。 |
要启用日志记录,请在创建ChatClient时将SimpleLoggerAdvisor添加到顾问链中。建议将其放在链的末尾:
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.user("Tell me a joke?")
.call()
.chatResponse();
要查看日志,请将advisor包的日志级别设置为DEBUG:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
在您的application.properties或application.yaml文件中添加此内容。
您可以使用以下构造函数来自定义从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
);
这使得您能够根据特定需求定制日志信息。
| 请谨慎在生产环境中记录敏感信息。 |
聊天记忆
接口 ChatMemory 表示聊天对话记忆的存储。它提供了向对话添加消息、从对话检索消息以及清除对话历史的方法。
当前有一个内置实现:MessageWindowChatMemory。
MessageWindowChatMemory 是一种聊天内存实现,它维护一个最多达到指定最大大小(默认为20条消息)的消息窗口。当消息数量超过此限制时,旧消息会被移除,但系统消息会被保留。如果添加新的系统消息,则所有先前的系统消息将从内存中移除。这确保了最新的上下文始终可用于对话,同时保持内存使用受限。
MessageWindowChatMemory 由 ChatMemoryRepository 抽象支持,该抽象为聊天对话记忆提供存储实现。有几种实现可用,包括 InMemoryChatMemoryRepository、JdbcChatMemoryRepository、CassandraChatMemoryRepository 和 Neo4jChatMemoryRepository。
对于更多详细信息和使用示例,请参阅聊天记忆文档。
实施说明
在框架ChatClient中结合使用命令式和响应式编程模型是一种独特的API特性。
通常,一个应用程序要么是响应式的,要么是命令式的,但不会两者兼具。
-
当自定义模型实现的HTTP客户端交互时,必须配置RestClient和WebClient。
|
由于 Spring Boot 3.4 中的一个 bug,必须设置 "spring.http.client.factory=jdk" 属性。否则,默认情况下它会被设置为 "reactor",这会破坏某些 AI 工作流程,例如 ImageModel。 |
-
流式处理仅通过响应式堆栈支持。因此,命令式应用程序必须包含响应式堆栈(例如 spring-boot-starter-webflux)。
-
非流式仅通过 Servlet 栈支持。因此,响应式应用程序必须包含 Servlet 栈(例如 spring-boot-starter-web)并预期某些调用可能会阻塞。
-
工具调用是命令式的,导致阻塞的工作流。这还会导致 Micrometer 观测结果部分/中断(例如,ChatClient 跨度和工具调用跨度未连接,因此前者保持不完整状态)。
-
内置的顾问对标准调用执行阻塞操作,对流调用执行非阻塞操作。顾问流调用使用的 Reactor 调度器可以通过每个顾问类上的 Builder 进行配置。