聊天客户端 API

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

该流畅 API 提供了用于构建 Prompt 各个部分的方法,该 Prompt 作为输入传递给 AI 模型。Prompt 包含指导 AI 模型输出和行为的指令文本。从 API 的角度来看,Prompt 由消息集合组成。

AI 模型处理两种主要类型的消息:用户消息(来自用户的直接输入)和系统消息(系统生成用于引导对话)。

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

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

创建 ChatClient

ChatClient 是使用 ChatClient.Builder 对象创建的。您可以获取任何 聊天模型 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 方法以三种不同的方式创建 Prompt,从而启动流畅 API

  • prompt(): 这个无参数方法让您开始使用流畅 API,允许您构建用户、系统和 Prompt 的其他部分。

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

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

ChatClient 响应

ChatClient API 提供了几种使用流畅 API 格式化 AI 模型响应的方式。

返回 ChatResponse

AI 模型的响应是由类型 ChatResponse 定义的丰富结构。它包含有关响应如何生成的元数据,并且还可以包含多个响应,称为 Generations,每个响应都有自己的元数据。元数据包括用于创建响应的 token(每个 token 大约是 3/4 个单词)数量。此信息很重要,因为托管的 AI 模型根据每个请求使用的 token 数量收费。

下面通过在 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 等类型

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);

Prompt 模板

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,适用于不需要模板处理的情况。

Spring AI 也提供了一个 NoOpTemplateRenderer

直接在 ChatClient 上配置的 TemplateRenderer (通过 .templateRenderer()) 仅应用于直接在 ChatClient 构建器链中定义的 Prompt 内容(例如,通过 .user(), .system())。它影响 Advisors 内部使用的模板,例如 QuestionAnswerAdvisor,它们有自己的模板自定义机制(请参阅 自定义 Advisor 模板)。

如果您更喜欢使用不同的模板引擎,可以直接向 ChatClient 提供 TemplateRenderer 接口的自定义实现。您也可以继续使用默认的 StTemplateRenderer,但进行自定义配置。

例如,默认情况下,模板变量由 {} 语法标识。如果您计划在 Prompt 中包含 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() 返回值

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

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

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

  • ChatClientResponse chatClientResponse(): 返回一个 ChatClientResponse 对象,该对象包含 ChatResponse 对象和 ChatClient 执行上下文,使您能够访问在执行 advisors 期间使用的额外数据(例如,在 RAG 流程中检索到的相关文档)。

  • 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,该对象包含有关响应的额外元数据。

  • Flux<ChatClientResponse> chatClientResponse(): 返回 ChatClientResponse 对象的 Flux,该对象包含 ChatResponse 对象和 ChatClient 执行上下文,使您能够访问在执行 advisors 期间使用的额外数据(例如,在 RAG 流程中检索到的相关文档)。

使用默认值

@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 级别,您可以指定默认的 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 实现通过将与用户文本相关的上下文信息附加到 Prompt 来启用 检索增强生成 模式。

  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): 此方法允许您定义一个 Consumer,以使用 AdvisorSpec 配置多个 advisors。Advisors 可以修改用于创建最终 Prompt 的数据。Consumer<AdvisorSpec> 允许您指定 lambda 表达式来添加 advisors,例如 QuestionAnswerAdvisor,它通过根据用户文本将相关上下文信息附加到 Prompt 来支持 检索增强生成

您可以在运行时使用不带 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)

Advisors

Advisors API 提供了一种灵活而强大的方式来拦截、修改和增强 Spring 应用程序中的 AI 驱动交互。

使用用户文本调用 AI 模型时的一个常见模式是使用上下文数据附加或增强 Prompt。

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

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

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

ChatClient 中的 Advisor 配置

ChatClient 流畅 API 提供了 AdvisorSpec 接口用于配置 advisors。该接口提供了添加参数、一次设置多个参数以及向链中添加一个或多个 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 添加到链中的顺序至关重要,因为它决定了它们的执行顺序。每个 advisor 都以某种方式修改 Prompt 或上下文,并且一个 advisor 所做的更改会传递给链中的下一个。
ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        new MessageChatMemoryAdvisor(chatMemory),
        new QuestionAnswerAdvisor(vectorStore)
    )
    .user(userText)
    .call()
    .content();

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

有关如何结合 advisors 使用 ChatMemory 接口管理对话历史的更多信息,请参阅 聊天记忆 文档。

以下 advisor 实现使用 ChatMemory 接口为 Prompt 提供对话历史,它们在内存如何添加到 Prompt 的细节上有所不同

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

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

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

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

    2. 设置在上下文中未提供默认对话 ID 时使用的默认对话 ID。

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

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

    5. 设置此 advisor 在链中的优先顺序。

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

下面显示了一个使用多个 advisors 的 @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), // 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 数据的 advisor。这对于调试和监控您的 AI 交互非常有用。

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

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

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.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()
);

这使您可以根据您的特定需求定制记录的信息。

在生产环境中记录敏感信息时要小心。

聊天记忆 (已弃用)

有关当前特性和功能的更多信息,请参阅新的 聊天记忆 文档。

接口 ChatMemory 表示聊天对话历史的存储。它提供了向对话添加消息、从对话检索消息以及清除对话历史的方法。

目前有四种实现:InMemoryChatMemoryCassandraChatMemoryNeo4jChatMemoryJdbcChatMemory,它们分别提供了内存中、在 Cassandra 中带 time-to-live 持久化以及在 Neo4j 和 Jdbc 中不带 time-to-live 持久化的聊天对话历史存储。

CassandraChatMemory

创建带 time-to-liveCassandraChatMemory

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

Neo4jChatMemory

Neo4j 聊天记忆支持以下配置参数

属性 描述 默认值

spring.ai.chat.memory.neo4j.messageLabel

存储消息的节点标签

Message

spring.ai.chat.memory.neo4j.sessionLabel

存储对话会话的节点标签

Session

spring.ai.chat.memory.neo4j.toolCallLabel

存储工具调用的节点标签,例如在 Assistant Messages 中

ToolCall

spring.ai.chat.memory.neo4j.metadataLabel

存储消息元数据的节点标签

Metadata

spring.ai.chat.memory.neo4j.toolResponseLabel

存储工具响应的节点标签

ToolResponse

spring.ai.chat.memory.neo4j.mediaLabel

存储与消息相关的媒体的节点标签

ToolResponse

JdbcChatMemory

创建 JdbcChatMemory

JdbcChatMemory.create(JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build());

JdbcChatMemory 也可以通过将以下依赖项添加到您的项目来自动配置(前提是您有 JdbcTemplate bean)

到您的 Maven pom.xml 文件

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
</dependency>

或到您的 Gradle build.gradle 文件

dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-jdbc'
}

自动配置将默认根据 JDBC 驱动程序自动创建 ai_chat_memory 表。目前,仅支持 postgresqlmariadb

要禁用模式初始化,请将属性 spring.ai.chat.memory.jdbc.initialize-schema 设置为 false

在某些情况下,您使用 Liquibase 或 Flyway 等数据库迁移工具来管理数据库模式。在这种情况下,您可以禁用模式初始化,只需参考 这些 sql 文件 并将它们添加到您的迁移脚本中。