Lzh on GitHub

聊天客户端 API

ChatClient 提供了一个流式 API(Fluent API),用于与 AI 模型进行交互。它同时支持 同步 和 流式 编程模型。

ChatClient 提供了一个流式 API(Fluent API),用于与 AI 模型进行交互。它同时支持 同步流式 编程模型。

有关在 ChatClient 中结合使用命令式(Imperative)和响应式(Reactive)编程模型的说明,请参见本文档底部的实现说明(Implementation Notes)。

流式 API 提供了一系列方法,用于构建传递给 AI 模型的 Prompt 的各个组成部分。Prompt 包含指导 AI 模型输出和行为的指令文本。从 API 的角度来看,Prompt 由一组消息(messages)组成。

AI 模型主要处理两类消息:

  • 用户消息(user messages):来自用户的直接输入
  • 系统消息(system messages):由系统生成,用于引导对话

这些消息通常包含占位符(placeholders),在运行时根据用户输入进行替换,从而定制 AI 模型对用户输入的响应。

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

创建 ChatClient

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

使用自动配置的 ChatClient.Builder

在最简单的使用场景中,Spring AI 提供了 Spring Boot 自动配置,为你创建一个原型(prototype)的 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() 方法则以字符串(String)形式返回 AI 模型的响应。

使用多个聊天模型

在某些场景下,你可能需要在单个应用中使用多个聊天模型,例如:

  • 为不同类型的任务使用不同的模型(例如:复杂推理使用更强大的模型,简单任务使用更快、更经济的模型)
  • 当某个模型服务不可用时,实现回退机制
  • 对不同模型或配置进行 A/B 测试
  • 根据用户偏好提供模型选择
  • 结合不同专业模型(如一个用于代码生成,另一个用于创意内容)

默认情况下,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);

// 或使用 Builder 进行更多自定义
ChatClient.Builder builder = ChatClient.builder(myChatModel);
ChatClient customChatClient = builder
    .defaultSystemPrompt("You are a helpful assistant.")
    .build();

使用不同模型类型的多个 ChatClient

当应用中需要使用多个 AI 模型时,可以为每个模型定义单独的 ChatClient Bean:

@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("\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");
            }

            // 使用选定的 ChatClient
            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 端点

OpenAiApiOpenAiChatModel 提供了 mutate() 方法,可以基于现有实例创建不同属性的变体。这在需要同时使用多个 OpenAI 兼容 API 时非常有用:

@Service
public class MultiModelService {

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

            // 为两个模型准备相同的 Prompt
            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

ChatClient 的流式 API 提供了三种不同方式来创建提示(Prompt),通过重载的 prompt 方法启动流式 API:

  • prompt():无参方法,用于启动流式 API,允许你逐步构建用户消息、系统消息及提示的其他部分。
  • prompt(Prompt prompt):接受一个 Prompt 对象作为参数,允许你传入通过 Prompt 的非流式 API 创建的实例。
  • 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();

返回实体(Entity)

通常情况下,你可能希望将返回的字符串(String)映射为一个实体类。entity() 方法提供了这种功能。

例如,给定如下 Java record:

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

你可以使用 entity() 方法轻松地将 AI 模型的输出映射到该 record:

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<ActorFilms>>() {});

原生结构化输出

随着越来越多的 AI 模型原生支持结构化输出,你可以通过在调用 ChatClient 时使用 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT 参数来利用这一功能。你可以在 ChatClient.Builder 上使用 defaultAdvisors() 方法为所有调用全局设置此参数,或者像下面这样针对单次调用设置:

ActorFilms actorFilms = chatClient.prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);
需要注意的是,一些 AI 模型(如 OpenAI)原生不支持对象数组。在这种情况下,你可以使用 Spring AI 提供的默认结构化输出转换来处理。

流式响应(Streaming Responses)

stream() 方法允许你以异步方式获取响应,如下所示:

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

你也可以使用 Flux<ChatResponse> chatResponse() 方法来流式获取 ChatResponse

未来,我们将提供一个便捷方法,使你可以在使用响应式 stream() 方法时直接返回 Java 实体类。在此之前,你需要使用 Structured Output Converter 来显式地转换聚合后的响应,如下示例所示。这也展示了在流式 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);

这里的流程是:先通过 stream() 异步获取文本内容,然后将所有流式内容聚合,再通过 BeanOutputConverter 转换为指定的实体列表。

Prompt 模板(Prompt Templates)

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

通过 .templateRenderer()ChatClient 上直接配置的 TemplateRenderer 仅对在 ChatClient 构建链中定义的 prompt 内容生效(例如通过 .user().system()),不会影响 Advisors(如 QuestionAnswerAdvisor)内部使用的模板,这些 Advisors 有自己的模板定制机制(参见 自定义 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();

这样,模板引擎将在运行时用 composer 对应的值替换模板内容。

call() 返回值

在对 ChatClient 调用 call() 方法后,你可以选择多种不同的响应类型:

  • String content():返回响应的字符串内容。
  • ChatResponse chatResponse():返回 ChatResponse 对象,其中包含多条生成内容(Generations)及响应的元数据,例如生成响应所使用的 token 数量。
  • ChatClientResponse chatClientResponse():返回 ChatClientResponse 对象,其中包含 ChatResponse 对象和 ChatClient 执行上下文,让你在执行 Advisors 时访问额外数据(例如 RAG 流程中检索到的相关文档)。
  • entity():将响应转换为 Java 类型。
    • entity(ParameterizedTypeReference<T> type):用于返回实体类型的集合。
    • entity(Class<T> type):用于返回特定的实体类型。
    • entity(StructuredOutputConverter<T> structuredOutputConverter):指定一个 StructuredOutputConverter 实例,将字符串转换为实体类型。
  • responseEntity():同时返回完整的 AI 模型响应(含元数据和生成内容)和结构化输出实体。
    • responseEntity(Class<T> type):返回包含完整 ChatResponse 对象和特定实体类型的 ResponseEntity
    • responseEntity(ParameterizedTypeReference<T> type):返回包含完整 ChatResponse 对象和实体类型集合的 ResponseEntity
    • responseEntity(StructuredOutputConverter<T> structuredOutputConverter):返回包含完整 ChatResponse 对象及使用指定 StructuredOutputConverter 转换的实体类型的 ResponseEntity

你也可以调用 stream() 方法替代 call(),以获取异步流式响应。

需要注意的是,调用 call() 方法并不会立即触发 AI 模型执行。它只是指示 Spring AI 使用同步调用还是流式调用。实际的 AI 模型调用发生在你调用 content()chatResponse()responseEntity() 等方法时。

stream() 返回值

在对 ChatClient 调用 stream() 方法后,你可以选择几种不同的响应类型:

  • Flux<String> content():返回一个 Flux,流式输出 AI 模型生成的字符串内容。
  • Flux<ChatResponse> chatResponse():返回一个 Flux,流式输出 ChatResponse 对象,其中包含响应的附加元数据。
  • Flux<ChatClientResponse> chatClientResponse():返回一个 Flux,流式输出 ChatClientResponse 对象,其中包含 ChatResponse 对象和 ChatClient 执行上下文,让你在执行 Advisors 时访问额外数据(例如 RAG 流程中检索到的相关文档)。

消息元数据(Message Metadata)

ChatClient 支持向 用户消息系统消息 添加元数据。元数据为消息提供了额外的上下文信息,可供 AI 模型或后续处理使用。

为用户消息添加元数据

你可以使用 metadata() 方法向用户消息添加元数据:

// 添加单个元数据键值对
String response = chatClient.prompt()
    .user(u -> u.text("天气怎么样?")
        .metadata("messageId", "msg-123")
        .metadata("userId", "user-456")
        .metadata("priority", "high"))
    .call()
    .content();

// 一次性添加多个元数据条目
Map<String, Object> userMetadata = Map.of(
    "messageId", "msg-123",
    "userId", "user-456",
    "timestamp", System.currentTimeMillis()
);

String response = chatClient.prompt()
    .user(u -> u.text("天气怎么样?")
        .metadata(userMetadata))
    .call()
    .content();

以上示例展示了如何为用户消息附加单个或多个元数据,以便 AI 模型或后续处理使用这些额外的上下文信息。

为系统消息添加元数据

同样,你也可以向系统消息添加元数据:

// 向系统消息添加元数据
String response = chatClient.prompt()
    .system(s -> s.text("你是一个乐于助人的助手。")
        .metadata("version", "1.0")
        .metadata("model", "gpt-4"))
    .user("讲一个笑话给我听")
    .call()
    .content();

这样可以为系统消息附加额外的上下文信息,例如版本号或模型类型,以便 AI 模型在生成回复时使用。

默认元数据支持

你还可以在 ChatClient 构建器层面配置默认元数据:

@Configuration
class Config {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem(s -> s.text("你是一个乐于助人的助手")
                .metadata("assistantType", "general")
                .metadata("version", "1.0"))
            .defaultUser(u -> u.text("默认用户上下文")
                .metadata("sessionId", "default-session"))
            .build();
    }
}

这样配置后,每条系统消息和用户消息都会自动附加这些默认元数据,无需每次手动设置。

元数据验证

ChatClient 会对元数据进行验证以确保数据完整性:

  • 元数据键(key)不能为空或 null
  • 元数据值(value)不能为 null
  • 当传入 Map 时,键和值都不能包含 null 元素

示例:

// 这会抛出 IllegalArgumentException
chatClient.prompt()
    .user(u -> u.text("Hello")
        .metadata(null, "value"))  // 无效:key 为 null
    .call()
    .content();

// 这也会抛出 IllegalArgumentException
chatClient.prompt()
    .user(u -> u.text("Hello")
        .metadata("key", null))    // 无效:value 为 null
    .call()
    .content();

这样可以防止无效的元数据被发送给 AI 模型,从而保证数据的可靠性。

访问元数据

元数据会包含在生成的 UserMessageSystemMessage 对象中,并且可以通过消息的 getMetadata() 方法访问。这在处理 Advisors 中的消息或检查对话历史时尤其有用。

使用默认值

@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,
                                  @RequestParam 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 实现支持 Retrieval Augmented Generation(RAG) 模式,通过在 Prompt 中追加与用户文本相关的上下文信息。
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):此方法允许你定义一个 Consumer 来使用 AdvisorSpec 配置多个 advisor。Advisor 可以修改用于创建最终 Prompt 的数据。Consumer<AdvisorSpec> 允许你通过 lambda 添加各种 advisor,例如 QuestionAnswerAdvisor,它通过基于用户输入追加相关上下文信息来支持检索增强生成(RAG)。

你可以在运行时使用对应的方法覆盖这些默认值(无需 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 中的顾问配置

ChatClient 的 fluent 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 的修改会传递给链中的下一个 Advisor。
ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(),
        QuestionAnswerAdvisor.builder(vectorStore).build()
    )
    .user(userText)
    .call()
    .content();

在此配置中,MessageChatMemoryAdvisor 会首先执行,将对话历史添加到 Prompt 中。随后,QuestionAnswerAdvisor 会基于用户的问题以及添加的对话历史执行搜索,从而提供更相关的结果。

了解更多关于 Question Answer Advisor 的内容。

检索增强生成(Retrieval Augmented Generation, RAG)

请参考《增强检索生成(Retrieval Augmented Generation)》指南。

日志记录(Logging)

SimpleLoggerAdvisor 是一个顾问(Advisor),用于记录 ChatClient 的请求和响应数据,这对于调试和监控 AI 交互非常有用。

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

要启用日志记录,请在创建 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<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
);

这使你可以根据具体需求定制日志记录的信息。

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

聊天记忆(Chat Memory)

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

目前内置的实现有一个:MessageWindowChatMemory

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

MessageWindowChatMemory 基于 ChatMemoryRepository 抽象,该抽象提供了聊天记忆的存储实现。可用的实现包括:InMemoryChatMemoryRepositoryJdbcChatMemoryRepositoryCassandraChatMemoryRepositoryNeo4jChatMemoryRepository

更多详情和使用示例,请参见《Chat Memory》文档。

实现说明

ChatClient 中命令式(imperative)和响应式(reactive)编程模型的结合使用是 API 的一个独特之处。通常应用程序会选择其中一种,但不同时使用。

  • 自定义模型实现的 HTTP 客户端交互时,需要同时配置 RestClientWebClient
由于 Spring Boot 3.4 的一个 bug,必须设置 spring.http.client.factory=jdk。否则默认值为 reactor,会导致某些 AI 工作流(如 ImageModel)出错。
  • 流式(streaming)仅支持响应式栈(Reactive stack)。因此,命令式应用必须引入响应式栈(如 spring-boot-starter-webflux)。
  • 非流式(non-streaming)仅支持 Servlet 栈。响应式应用必须引入 Servlet 栈(如 spring-boot-starter-web),并且某些调用可能会阻塞。
  • 工具调用(tool calling)是命令式的,因此会导致阻塞的工作流。这也会造成 Micrometer 监控观测的不完整或中断(例如 ChatClient 的跨度和工具调用的跨度未连接,第一个跨度会因此保持不完整)。
  • 内置顾问(advisors)在标准调用中执行阻塞操作,而在流式调用中执行非阻塞操作。用于顾问流式调用的 Reactor Scheduler 可以通过每个顾问类的 Builder 进行配置。