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

请参阅本文档底部的实现说明,了解 ChatClient 中命令式和响应式编程模型结合使用的相关信息。

流畅的 API 提供了构建 Prompt 各个组成部分的方法,这些部分作为输入传递给 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 返回。

使用多个聊天模型

在单个应用程序中使用多个聊天模型有几种场景:

默认情况下,Spring AI 自动配置单个 ChatClient.Builder bean。但是,您可能需要在应用程序中使用多个聊天模型。以下是处理这种情况的方法:

在所有情况下,您都需要通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置。

这允许您手动创建多个 ChatClient 实例。

使用单个模型类型的多个 ChatClient

本节介绍一个常见用例,您需要创建多个使用相同底层模型类型但具有不同配置的 ChatClient 实例。

// 以编程方式创建 ChatClient 实例
ChatModel myChatModel = ... // 已由 Spring Boot 自动配置
ChatClient chatClient = ChatClient.create(myChatModel);

// 或使用构建器以获得更多控制
ChatClient.Builder builder = ChatClient.builder(myChatModel);
ChatClient customChatClient = builder
    .defaultSystemPrompt("你是一个乐于助人的助手。")
    .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);
    }
}

然后,您可以使用 @Qualifier 注解将这些 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;
            
            // 模型选择
            System.out.println("\n选择您的 AI 模型:");
            System.out.println("1. OpenAI");
            System.out.println("2. Anthropic");
            System.out.print("输入您的选择(1 或 2):");
            
            String choice = scanner.nextLine().trim();
            
            if (choice.equals("1")) {
                chat = openAiChatClient;
                System.out.println("使用 OpenAI 模型");
            } else {
                chat = anthropicChatClient;
                System.out.println("使用 Anthropic 模型");
            }
            
            // 使用选定的聊天客户端
            System.out.print("\n输入您的问题:");
            String input = scanner.nextLine();
            String response = chat.prompt(input).call().content();
            System.out.println("助手:" + response);
            
            scanner.close();
        };
    }
}

多个 OpenAI 兼容的 API 端点

OpenAiApiOpenAiChatModel 类提供了 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 {
            // 为 Groq (Llama3) 派生新的 OpenAiApi
            OpenAiApi groqApi = baseOpenAiApi.mutate()
                .baseUrl("https://api.groq.com/openai")
                .apiKey(System.getenv("GROQ_API_KEY"))
                .build();
            
            // 为 OpenAI GPT-4 派生新的 OpenAiApi
            OpenAiApi gpt4Api = baseOpenAiApi.mutate()
                .baseUrl("https://api.openai.com")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .build();
            
            // 为 Groq 派生新的 OpenAiChatModel
            OpenAiChatModel groqModel = baseChatModel.mutate()
                .openAiApi(groqApi)
                .defaultOptions(OpenAiChatOptions.builder().model("llama3-70b-8192").temperature(0.5).build())
                .build();
            
            // 为 GPT-4 派生新的 OpenAiChatModel
            OpenAiChatModel gpt4Model = baseChatModel.mutate()
                .openAiApi(gpt4Api)
                .defaultOptions(OpenAiChatOptions.builder().model("gpt-4").temperature(0.7).build())
                .build();
            
            // 两个模型的简单提示
            String prompt = "法国的首都是什么?";
            
            String groqResponse = ChatClient.builder(groqModel).build().prompt(prompt).call().content();
            String gpt4Response = ChatClient.builder(gpt4Model).build().prompt(prompt).call().content();
            
            logger.info("Groq (Llama3) 响应:{}", groqResponse);
            logger.info("OpenAI GPT-4 响应:{}", gpt4Response);
        }
        catch (Exception e) {
            logger.error("多客户端流程中的错误", e);
        }
    }
}

ChatClient 流畅 API

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

prompt()

这个无参数方法让您开始使用流畅 API,允许您构建用户、系统和其他提示部分。

prompt(Prompt prompt)

这个方法接受 Prompt 参数,让您传入使用 Prompt 的非流畅 API 创建的 Prompt 实例。

prompt(String content)

这是一个类似于前一个重载的便捷方法。它接受用户的文本内容。

ChatClient 响应

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

返回 ChatResponse

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

下面通过调用 call() 方法后的 chatResponse() 方法展示了返回包含元数据的 ChatResponse 对象的示例:

ChatResponse chatResponse = chatClient.prompt()
    .user("给我讲个笑话")
    .call()
    .chatResponse();

返回实体

您通常希望返回一个从返回的 String 映射的实体类。 entity() 方法提供了这个功能。

例如,给定 Java 记录:

record ActorFilms(String actor, List<String> movies) {}

您可以使用 entity() 方法轻松地将 AI 模型的输出映射到这个记录,如下所示:

ActorFilms actorFilms = chatClient.prompt()
    .user("生成一个随机演员的电影作品。")
    .call()
    .entity(ActorFilms.class);

还有一个重载的 entity 方法,签名为 entity(ParameterizedTypeReference<T> type),允许您指定泛型列表等类型:

List<ActorFilms> actorFilms = chatClient.prompt()
    .user("生成汤姆·汉克斯和比尔·默瑞的 5 部电影作品。")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});

流式响应

stream() 方法让您获得异步响应,如下所示:

Flux<String> output = chatClient.prompt()
    .user("给我讲个笑话")
    .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("""
                        生成一个随机演员的电影作品。
                        {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);

提示模板

ChatClient 流畅 API 允许您提供带有变量的用户和系统文本作为模板,这些变量在运行时被替换。

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("告诉我 5 部由 {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,它们有自己的模板自定义机制(参见自定义 Advisor 模板)。

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

例如,默认情况下,模板变量由 {} 语法标识。如果您计划在提示中包含 JSON,您可能想使用不同的语法以避免与 JSON 语法冲突。例如,您可以使用 <> 分隔符。

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("告诉我 5 部由 <composer> 作曲的电影原声带")
            .param("composer", "John Williams"))
    .templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
    .call()
    .content();

call() 返回值

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

content()

返回响应的 String 内容

chatResponse()

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

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() 方法后,响应类型有几种选项:

content()

返回 AI 模型生成的字符串的 Flux<String>

chatResponse()

返回 Flux<ChatResponse> 对象,其中包含有关响应的其他元数据

chatClientResponse()

返回 Flux<ChatClientResponse> 对象,其中包含 ChatResponse 对象和 ChatClient 执行上下文,让您可以访问在执行 advisors 期间使用的其他数据(例如,在 RAG 流程中检索的相关文档)

使用默认值

@Configuration 类中创建带有默认系统文本的 ChatClient 可以简化运行时代码。 通过设置默认值,您只需要在调用 ChatClient 时指定用户文本,无需在运行时代码路径中为每个请求设置系统文本。

默认系统文本

在以下示例中,我们将配置系统文本始终以海盗的声音回复。 为了避免在运行时代码中重复系统文本,我们将在 @Configuration 类中创建一个 ChatClient 实例。

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个友好的聊天机器人,用海盗的声音回答问题")
                .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 = "给我讲个笑话") String message) {
        return Map.of("completion", this.chatClient.prompt().user(message).call().content());
    }
}

当通过 curl 调用应用程序端点时,结果是:

curl localhost:8080/ai/simple
{"completion":"为什么海盗去喜剧俱乐部?为了听一些 arrr-rated 笑话!啊,伙计!"}

带参数的默认系统文本

在以下示例中,我们将在系统文本中使用占位符,以便在运行时而不是设计时指定完成的声音。

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个友好的聊天机器人,用 {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 = "给我讲个笑话") 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": "你在跟我说话吗?好吧,给你讲个笑话:为什么自行车不能自己站起来?因为它太累了!经典,对吧?"
}

其他默认值

ChatClient.Builder 级别,您可以指定默认提示配置。

您可以使用不带 default 前缀的相应方法在运行时覆盖这些默认值。

options(ChatOptions chatOptions)

覆盖默认选项

function(...)

覆盖默认函数

functions(String... functionNames)

覆盖默认函数

user(...)

覆盖默认用户

advisors(Advisor... advisor)

覆盖默认 advisors

advisors(Consumer<AdvisorSpec>...)

使用 consumer 覆盖默认 advisors

Advisors

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

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

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

您自己的数据

这是 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 都以某种方式修改提示或上下文,一个 advisor 所做的更改会传递给链中的下一个。

ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(),
        QuestionAnswerAdvisor.builder(vectorStore).build()
    )
    .user(userText)
    .call()
    .content();

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

了解 Question Answer Advisor

检索增强生成

请参阅检索增强生成指南。

日志记录

SimpleLoggerAdvisor 是一个记录 ChatClientrequestresponse 数据的 advisor。 这对于调试和监控您的 AI 交互很有用。

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

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

ChatResponse response = ChatClient.create(chatModel).prompt()
        .advisors(new SimpleLoggerAdvisor())
        .user("给我讲个笑话?")
        .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 -> "自定义请求:" + request.userText,
    response -> "自定义响应:" + response.getResult()
);

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

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

聊天内存

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

目前有一个内置实现:MessageWindowChatMemory

MessageWindowChatMemory 是一个聊天内存实现,它维护一个最多指定最大大小(默认:20 条消息)的消息窗口。当消息数量超过此限制时,较旧的消息会被逐出,但系统消息会被保留。如果添加了新的系统消息,所有先前的系统消息都会从内存中删除。这确保了对话始终可以使用最新的上下文,同时保持内存使用有界。

MessageWindowChatMemoryChatMemoryRepository 抽象支持,它提供了聊天对话内存的存储实现。有几种实现可用,包括 InMemoryChatMemoryRepositoryJdbcChatMemoryRepositoryCassandraChatMemoryRepositoryNeo4jChatMemoryRepository

有关更多详细信息和用法示例,请参阅聊天内存文档。

实现说明

ChatClient 中命令式和响应式编程模型的结合使用是 API 的一个独特方面。 通常,应用程序要么是响应式的,要么是命令式的,但不是两者都是。

文档有误?请协助编辑

发现文档问题?点击此处直接在 GitHub 上编辑并提交 PR,帮助我们改进文档!