聊天客户端 API

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

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

AI 模型处理两种主要类型的消息:用户消息,即来自用户的直接输入;系统消息,即由系统生成以指导对话。

这些消息通常包含占位符,这些占位符在运行时根据用户输入进行替换,以根据用户输入自定义 AI 模型的响应。

还可以指定提示选项,例如要使用的 AI 模型的名称以及控制生成的输出的随机性或创造性的温度设置。

创建 ChatClient

ChatClient 是使用 ChatClient.Builder 对象创建的。您可以为任何 ChatModel Spring Boot 自动配置获取自动配置的 ChatClient.Builder 实例,或者以编程方式创建一个。

使用自动配置的 ChatClient.Builder

在最简单的用例中,Spring AI 提供 Spring Boot 自动配置,为您创建原型 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 模型发送请求,context 方法将 AI 模型的响应作为字符串返回。

以编程方式创建 ChatClient

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

ChatModel myChatModel = ... // usually autowired

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

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

ChatClient chatClient = ChatClient.create(myChatModel);

ChatClient 响应

ChatClient 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 方法,其签名为 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<ActorsFilms>>() {
    });

流式响应

stream 允许您获得异步响应,如下所示

Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();

您还可以使用 Flux<ChatResponse> chatResponse() 方法流式传输 ChatResponse

在 1.0.0 M2 中,我们将提供一个方便的方法,允许您使用响应式 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", converter.getFormat()))
        .stream()
        .content();

    String content = flux.collectList().block().stream().collect(Collectors.joining());

    List<ActorFilms> actorFilms = converter.convert(content);

call() 返回值

ChatClient 上指定 call 方法后,响应类型有几个不同的选项。

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

  • ChatResponse chatResponse():返回包含多个生成以及有关响应的元数据的 ChatResponse 对象,例如用于创建响应的令牌数量。

  • entity 用于返回 Java 类型。

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

    • 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", chatClient.prompt().user(message).call().content());
	}
}

通过 curl 调用它,结果是

❯ curl localhost:8080/ai/simple
{"generation":"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",
				chatClient.prompt()
						.system(sp -> sp.param("voice", voice))
						.user(message)
						.call()
						.content());
	}
}

响应是

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(RequestResponseAdvisor…​ 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(RequestResponseAdvisor…​ advisor)

  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

顾问

在使用用户文本调用 AI 模型时,一种常见的模式是在提示中附加或增强上下文数据。

这种上下文数据可以是不同类型的。常见的类型包括

  • 您自己的数据:这是 AI 模型未经训练的数据。即使模型已经见过类似的数据,附加的上下文数据在生成响应时优先考虑。

  • 对话历史:聊天模型的 API 是无状态的。如果您告诉 AI 模型您的姓名,它不会在后续交互中记住它。对话历史必须与每个请求一起发送,以确保在生成响应时考虑之前的交互。

检索增强生成

向量数据库存储 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 的过滤器表达式,该表达式在所有 VectorStores 中都是可移植的。

聊天记忆

接口 ChatMemory 代表聊天对话历史的存储。它提供方法将消息添加到对话中,从对话中检索消息以及清除对话历史。

有一个实现 InMemoryChatMemory 提供聊天对话历史的内存存储。

两个顾问实现使用 ChatMemory 接口来建议使用对话历史的提示,它们在将内存添加到提示的细节方面有所不同

  • MessageChatMemoryAdvisor:内存被检索并作为消息集合添加到提示中

  • PromptChatMemoryAdvisor:内存被检索并添加到提示的系统文本中。

  • VectorStoreChatMemoryAdvisor : 构造函数 ` VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize)` 允许您指定用于检索聊天历史记录的 VectorStore、唯一的对话 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 PromptChatMemoryAdvisor(chatMemory),
                    // new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORY
                    new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()),
                    new LoggingAdvisor()) // RAG
            .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();
    }
}