聊天客户端API

ChatClient 提供了一个流畅的API,用于与AI模型进行通信。它支持同步和流式编程模型。

流畅的API具有用于构建作为输入传递给AI模型的提示词的组成部分的方法。Prompt包含指导AI模型输出和行为的指令文本。从API的角度来看,提示词由一系列消息组成。

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返回。

以编程方式创建ChatClient

您可以通过设置属性spring.ai.chat.client.enabled=false来禁用ChatClient.Builder自动配置。如果您同时使用多个聊天模型,这将非常有用。然后,为每个需要的ChatModel以编程方式创建一个ChatClient.Builder实例。

ChatModel myChatModel = ... // usually autowired

ChatClient.Builder builder = ChatClient.builder(this.myChatModel);

// or create a ChatClient with the default builder settings:

ChatClient chatClient = ChatClient.create(this.myChatModel);

ChatClient 流畅API

ChatClient流畅的API允许您使用重载的prompt方法以三种不同的方式创建提示词以启动流畅的API。

  • prompt():此方法不带任何参数,允许您开始使用流畅的API,从而构建提示词的用户、系统和其他部分。

  • prompt(Prompt prompt):此方法接受Prompt参数,允许您传入使用Prompt的非流畅API创建的Prompt实例。

  • prompt(String content):这是一个类似于前面重载的便捷方法。它采用用户的文本内容。

ChatClient 响应

ChatClient API提供了几种方法,可以使用流畅的API来格式化来自AI模型的响应。

返回ChatResponse

来自AI模型的响应是由类型ChatResponse定义的丰富结构。它包含有关响应生成方式的元数据,还可以包含多个响应(称为Generation),每个响应都有其自身的元数据。元数据包括用于创建响应的标记数量(每个标记大约是3/4个单词)。此信息非常重要,因为托管的AI模型根据每个请求使用的标记数量收费。

通过在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<ActorFilms> actorFilms = this.converter.convert(this.content);

call() 返回值

ChatClient上指定call()方法后,响应类型有以下几种不同的选择。

  • String content():返回响应的字符串内容。

  • ChatResponse chatResponse():返回包含多个生成以及有关响应的元数据的ChatResponse对象,例如创建响应使用了多少个标记。

  • entity() 返回Java类型

    • entity(ParameterizedTypeReference<T> type):用于返回实体类型的Collection

    • entity(Class<T> type):用于返回特定实体类型。

    • entity(StructuredOutputConverter<T> structuredOutputConverter):用于指定一个StructuredOutputConverter实例,用于将String转换为实体类型。

您也可以调用stream()方法代替call()方法。

stream() 返回值

ChatClient上指定stream()方法后,响应类型有以下几种选择:

  • Flux<String> content():返回一个包含AI模型生成的字符串的Flux

  • Flux<ChatResponse> chatResponse():返回一个包含ChatResponse对象的Flux,其中包含有关响应的附加元数据。

使用默认值

@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调用应用程序端点时,结果为:

❯ 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调用应用程序端点时,结果为:

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):顾问允许修改用于创建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)

顾问

顾问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(
        new MessageChatMemoryAdvisor(chatMemory),
        new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())
    )
    .user(userText)
    .call()
    .content();

在此配置中,MessageChatMemoryAdvisor将首先执行,并将对话历史记录添加到提示中。然后,QuestionAnswerAdvisor将根据用户的提问和添加的对话历史记录执行搜索,从而可能提供更相关的结果。

检索增强生成 (RAG)

向量数据库存储AI模型不知道的数据。当用户问题发送到AI模型时,QuestionAnswerAdvisor会查询向量数据库以查找与用户问题相关的文档。

向量数据库的响应将附加到用户文本中,为AI模型生成响应提供上下文。

假设您已将数据加载到VectorStore中,您可以通过向ChatClient提供QuestionAnswerAdvisor实例来执行检索增强生成 (RAG)。

ChatResponse response = ChatClient.builder(chatModel)
        .build().prompt()
        .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
        .user(userText)
        .call()
        .chatResponse();

在此示例中,SearchRequest.defaults()将在向量数据库中的所有文档上执行相似性搜索。为了限制搜索的文档类型,SearchRequest采用类似SQL的过滤器表达式,该表达式可在所有VectorStore中移植。

动态过滤器表达式

使用FILTER_EXPRESSION顾问上下文参数在运行时更新SearchRequest过滤器表达式。

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
    .build();

// Update filter expression at runtime
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表示聊天会话历史记录的存储。它提供将消息添加到对话、从对话中检索消息以及清除对话历史记录的方法。

目前有两个实现,InMemoryChatMemoryCassandraChatMemory,分别提供聊天会话历史记录的内存存储和具有time-to-live的持久化存储。

要创建具有time-to-liveCassandraChatMemory

CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build());

以下顾问实现使用ChatMemory接口以不同的方式将会话历史记录添加到提示中:

  • MessageChatMemoryAdvisor:检索记忆并将其作为消息集合添加到提示中。

  • PromptChatMemoryAdvisor:检索记忆并将其添加到提示的系统文本中。

  • VectorStoreChatMemoryAdvisor:构造函数VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order) 此构造函数允许您:

    1. 指定用于管理和查询文档的VectorStore实例。

    2. 设置默认会话ID(如果上下文中未提供则使用)。

    3. 根据标记大小定义聊天历史记录检索的窗口大小。

    4. 提供用于聊天顾问系统的系统文本建议。

    5. 设置此顾问在链中的优先级顺序。

VectorStoreChatMemoryAdvisor.builder()方法允许您指定默认会话ID、聊天历史记录窗口大小以及要检索的聊天历史记录的顺序。

下面显示了一个使用多个顾问的示例@Service实现。

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, SearchRequest.defaults()), // 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是一个顾问,它记录ChatClientrequestresponse数据。这对于调试和监控您的AI交互非常有用。

Spring AI支持LLM和向量存储交互的可观察性。有关更多信息,请参阅可观察性指南。

要启用日志记录,请在创建ChatClient时将SimpleLoggerAdvisor添加到顾问链中。建议将其添加到链的末尾。

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.propertiesapplication.yaml文件中。

您可以使用以下构造函数自定义记录自AdvisedRequestChatResponse中的哪些数据:

SimpleLoggerAdvisor(
    Function<AdvisedRequest, String> requestToString,
    Function<ChatResponse, String> responseToString
)

使用示例:

SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -> "Custom request: " + request.userText,
    response -> "Custom response: " + response.getResult()
);

这允许您根据您的特定需求定制记录的信息。

请注意,在生产环境中记录敏感信息。