ChatClient 提供了一个流畅的API,用于与AI模型进行通信。它同时支持同步和流式编程模型。 该流畅的API提供了构建Prompt(提示词)各组成部分的方法,这些Prompt作为输入传递给AI模型。Prompt包含指导AI模型输出及行为的指令文本。从API视角来看,Prompt由一系列消息(Message)组成。

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

用户消息(User Messages) :用户的直接输入内容; 系统消息(System Messages) :由系统生成以引导对话流程的消息。 这些消息通常包含占位符(Placeholders),这些占位符会在运行时根据用户输入动态替换,从而实现AI模型对用户输入的定制化响应。

此外,Prompt还可指定以下选项: AI模型名称:指定使用的具体模型; 温度参数(Temperature):控制生成结果的随机性或创造性(值越高,输出越随机)

创建一个ChatClient

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

使用自动配置的ChatClient.Builder

在最简单的用例中,Spring AI通过Spring Boot自动配置机制,注入一个原型作用域(prototype)的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模型发送请求,而content()方法以字符串形式返回AI模型的响应

以编程方式创建 ChatClient

你可以通过设置属性 spring.ai.chat.client.enabled=false 禁用 ChatClient.Builder 的自动配置功能。当需要同时使用多个聊天模型时,此配置非常有用。随后,你可以为每个需要的 ChatModel 手动创建 ChatClient.Builder 实例:

ChatModel myChatModel = ... // 通常通过依赖注入(@Autowired)获取

// 通过模型创建自定义配置的Builder
ChatClient.Builder builder = ChatClient.builder(this.myChatModel);

// 或使用默认配置直接创建ChatClient实例
ChatClient chatClient = ChatClient.create(this.myChatModel);

ChatClient Fluent API

ChatClient Fluent API 通过重载的 prompt 方法提供了三种创建提示(Prompt)的方式,用于与 AI 模型交互:

1、prompt() 无参数方法,允许通过Fluent API 逐步构建用户消息(user)、系统消息(system)及其他提示内容。例如:

Prompt prompt = Prompt.builder()
    .user("用户输入内容")
    .system("系统指令")
    .build();

2、prompt(Prompt prompt) 接受一个 Prompt 实例作为参数,适用于通过非流畅 API(如直接构造 Prompt 对象)创建的提示。例如:

Prompt predefinedPrompt = new Prompt(...); // 预定义Prompt
ChatResponse response = chatClient.prompt(predefinedPrompt).call();

3、prompt(String content) 便捷方法,直接接收用户输入的文本内容(等效于 prompt(Prompt.builder().user(content).build()))。例如:

String response = chatClient.prompt("用户问题").call().content();

ChatClient 响应

ChatClient API 提供了多种通过fluent API格式化AI模型响应的方式。

返回一个ChatResponse(Returning a ChatResponse)

AI模型的响应通过ChatResponse对象返回,该对象是一个包含丰富结构的数据类型。它不仅包含生成响应的元数据(如使用的令牌数量),还可能包含多个生成结果(Generations),每个结果包含独立的内容和元数据,适用于需要对比或筛选的场景每个结果都有独立的元数据,元数据(Metadata) 包含生成过程的详细信息,例如使用的令牌数量(每个令牌约等于3/4个单词)。托管型AI模型通常按请求的令牌数量计费,因此此数据对成本控制至关重要。

以下示例通过chatResponse()方法获取包含元数据的完整响应对象:

ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();

返回一个实体类(Returning an Entity)

通常需要将AI模型返回的字符串映射到一个实体类(Entity Class)。entity()方法提供了此功能,支持将响应内容自动转换为指定的Java对象

示例:基础实体类映射 给定一个Java record类定义:

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

可通过entity()方法将AI生成的文本直接映射到该实体类:

ActorFilms actorFilms = chatClient.prompt()
    .user("为一位随机演员生成电影作品列表")
    .call()
    .entity(ActorFilms.class); // 映射到ActorFilms实体类

示例:泛型集合映射 若需返回泛型集合(如List),可使用ParameterizedTypeReference指定类型参数:

List<ActorFilms> actorFilms = chatClient.prompt()
    .user("生成Tom Hanks和Bill Murray各5部电影的作品列表")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {}); // 支持泛型类型 [[3]][[4]]

流式响应(Streaming Responses)

通过 stream() 方法可获取异步响应流,示例如下:

Flux<String> output = chatClient.prompt()
    .user("讲个笑话")
    .stream()
    .content(); // 返回包含字符串的响应流

你也可以通过 Flux chatResponse() 方法流式传输完整的 ChatResponse 对象,包含元数据和生成结果。

未来版本将提供便捷方法,支持直接通过响应式 stream() 方法返回Java实体类。当前可通过结构化输出转换器(Structured Output Converter) 显式转换聚合后的响应,示例如下:

// 定义转换器(支持泛型类型)
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorFilms>>() {});

// 流式请求:通过参数动态替换占位符
Flux<String> flux = chatClient.prompt()
    .user(u -> u.text("""
                        为随机演员生成电影作品列表
                        {format}  // 动态替换格式参数
                      """)
            .param("format", converter.getFormat())) // 注入格式参数 [[3]][[5]]
    .stream()
    .content();

// 聚合流式响应并转换为实体类
String content = flux.collectList().block().stream().collect(Collectors.joining());
List<ActorFilms> actorFilms = converter.convert(content); // 转换为List<ActorFilms> [[1]][[6]]

call() 返回值

在 ChatClient 上指定 call() 方法后,有以下几种不同的响应类型选项 1、String content() 返回响应的字符串内容。例如:

String response = chatClient.prompt("问题").call().content();

2、ChatResponse chatResponse() 返回包含多个生成结果(Generations)及元数据(如生成响应所用的令牌数量)的 ChatResponse 对象。例如:

ChatResponse response = chatClient.prompt("问题").call().chatResponse();

3、entity() 方法(返回 Java 类型)

  • entity(Class type) 用于返回特定实体类型。例如:
ActorFilms films = chatClient.prompt("生成演员作品").call().entity(ActorFilms.class);
  • entity(ParameterizedTypeReference type) 用于返回泛型集合(如 List)。例如:
List<ActorFilms> films = chatClient.prompt("生成多个演员作品")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
  • entity(StructuredOutputConverter converter) 通过自定义转换器将字符串响应映射到实体类。例如:
var converter = new BeanOutputConverter<>(ActorFilms.class);
ActorFilms films = chatClient.prompt("生成演员作品").call().entity(converter);

4、stream() 方法(替代 call()) 通过 stream() 方法可获取异步响应流(如 Flux 或 Flux)。例如:

Flux<String> stream = chatClient.prompt("问题").stream().content();

stream() 返回值

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

1、Flux content() 返回由AI模型生成的字符串的响应流(Flux),适用于逐字接收生成内容。例如:

Flux<String> stream = chatClient.prompt("问题").stream().content();

2、Flux chatResponse() 返回包含元数据(如令牌使用量、生成配置)的 ChatResponse 对象流,适用于需要监控生成过程或调试的场景。例如:

Flux<ChatResponse> responseStream = chatClient.prompt("问题").stream().chatResponse();

使用默认值(Using Defaults)

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

默认系统文本(Default System Text)

以下示例将配置系统文本,使AI始终以海盗的口吻回答。为避免在运行时代码中重复设置系统文本,我们通过 @Configuration 类创建预配置的 ChatClient 实例

1、配置类(@Configuration)

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你是一个友好的聊天机器人,以海盗的口吻回答问题")
                .build();
    }
}

作用 :通过 defaultSystem() 方法设置全局系统提示,后续调用时无需重复指定。

2、控制器(@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", 
            chatClient.prompt().user(message).call().content());
    }
}

简化调用 :仅需传递用户输入(message),系统文本自动应用。

3、调用示例 通过curl请求端点时,响应会遵循预设的海盗风格:

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

带参数的默认系统文本

以下示例通过占位符({voice})在系统文本中动态指定回复风格,实现运行时配置而非设计时固定。

1、配置类(@Configuration)

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个友好的聊天机器人,以{voice}的口吻回答问题")
                .build();
    }
}

占位符机制:{voice} 在运行时通过参数替换,实现动态风格控制。

2、 控制器(@RestController)

@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,
        @RequestParam("voice") String voice) { // 接收风格参数
        return Map.of("completion",
            chatClient.prompt()
                .system(sp -> sp.param("voice", voice)) // 替换占位符
                .user(message)
                .call()
                .content());
    }
}

动态替换:通过 system().param() 方法将请求参数 voice 绑定到占位符。

3、调用示例 通过HTTP请求传递 voice 参数,响应会根据参数值调整风格:

http localhost:8080/ai voice=='Robert DeNiro'
{
    "completion": "你在跟我说话吗?好吧,给你讲个笑话:为什么自行车不能自己站起来?因为它太累了!经典吧?"
}

其他默认配置

在 ChatClient.Builder 级别,您可以指定默认的提示(Prompt)配置。

1、defaultOptions(ChatOptions chatOptions)传入在 ChatOptions 类中定义的可移植选项,或特定于模型的选项(如 OpenAiChatOptions)。有关特定模型的 ChatOptions 实现,请参考 JavaDocs。

2、defaultFunction(String name, String description, java.util.function.Function<I, O> function)

  • name :用户文本中引用该函数的名称。
  • description :描述函数用途,帮助AI模型选择正确的函数以生成准确响应。
  • function :当需要执行时,AI模型调用的Java函数实例。

3、defaultFunctions(String… functionNames)指定应用上下文中定义的 java.util.Function Bean名称列表。

4、defaultUser 方法族定义用户文本的默认值:

  • defaultUser(String text):直接传入文本。
  • defaultUser(Resource text):从资源文件加载文本。
  • defaultUser(Consumer userSpecConsumer):通过Lambda表达式定义用户文本及默认参数。

5、defaultAdvisors 方法族

  • defaultAdvisors(Advisor… advisor) :添加默认顾问(Advisor),用于修改生成Prompt的数据。例如,QuestionAnswerAdvisor 通过附加与用户文本相关的上下文信息,实现检索增强生成(RAG)模式。
  • defaultAdvisors(Consumer advisorSpecConsumer) :通过 AdvisorSpec 配置多个顾问。例如,使用Lambda表达式添加 QuestionAnswerAdvisor,基于用户文本动态附加上下文。

覆盖默认值

运行时可通过以下不带 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 userSpecConsumer)
  • advisors(Advisor… advisor)
  • advisors(Consumer advisorSpecConsumer)

Advisors

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

当通过用户文本调用 AI 模型时,一种常见模式是通过附加或增强提示(Prompt)来引入上下文数据 。此类上下文数据可包含以下类型:

1、自有数据(Your own data) 这是 AI 模型未接受过训练的数据。即使模型接触过类似数据,附加的上下文数据在生成响应时仍会优先生效 。

2、对话历史(Conversational history) 聊天模型的 API 是无状态的 。例如,若用户告知模型自己的姓名,模型不会在后续交互中记住。为确保生成响应时考虑之前的交互,必须在每次请求中附带对话历史

ChatClient中的Advisor配置

ChatClient的fluent API提供了一个AdvisorSpec接口用于配置Advisor。该接口提供了添加参数、批量设置参数以及向链中添加一个或多个顾问的方法

interface AdvisorSpec {
    AdvisorSpec param(String k, Object v); // 添加单个参数
    AdvisorSpec params(Map<String, Object> p); // 批量添加参数
    AdvisorSpec advisors(Advisor... advisors); // 添加可变参数Advisor
    AdvisorSpec advisors(List<Advisor> advisors); // 添加Advisor列表
}

Advisor链中添加的顺序至关重要,因为这决定了它们的执行顺序。每个Advisor会以某种方式修改提示(Prompt)或上下文,前一个Advisor的修改结果会传递给下一个Advisor

ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        new MessageChatMemoryAdvisor(chatMemory), // 对话历史Advisor
        new QuestionAnswerAdvisor(vectorStore)    // 问答增强Advisor
    )
    .user(userText)
    .call()
    .content();

在此配置中: MessageChatMemoryAdvisor 会首先执行,将对话历史附加到提示中。 QuestionAnswerAdvisor 随后基于用户问题和对话历史执行检索,提供更相关的上下文信息。

Advisor执行顺序与作用

1、顺序敏感性:Advisor链的执行顺序由添加顺序决定,类似Spring AOP的拦截链。 2、上下文增强:

  • MessageChatMemoryAdvisor通过附加历史对话记录,确保模型具备上下文感知能力。
  • QuestionAnswerAdvisor结合外部数据源(如向量数据库),实现检索增强生成(RAG)。

关于 QuestionAnswerAdvisor QuestionAnswerAdvisor 是一种特殊Advisor,用于在生成响应前从外部数据源(如向量库)检索与用户问题相关的上下文信息,并将其附加到提示中。其核心流程如下:

  • 分析用户输入,提取关键词或嵌入向量。
  • 从向量库中检索最相关的文档片段。
  • 将检索到的上下文与原始用户输入合并,形成增强后的提示。

Retrieval Augmented Generation

Refer to the Retrieval Augmented Generation guide.

聊天记忆(Chat Memory)

ChatMemory 接口表示聊天对话历史记录的存储,提供以下方法:

  • 向对话中添加消息
  • 从对话中检索消息
  • 清除对话历史记录

目前有两种实现:

  • InMemoryChatMemory :基于内存的存储,适合临时会话。
  • CassandraChatMemory :支持持久化存储并可通过 time-to-live 设置数据存活时间。

创建带存活时间的 CassandraChatMemory

CassandraChatMemory.create(
    CassandraChatMemoryConfig.builder()
        .withTimeToLive(Duration.ofDays(1)) // 设置存活时间为1天
        .build()
);

使用ChatMemory的Advisors

以下顾问实现通过 ChatMemory 接口将对话历史附加到提示(Prompt)中,区别在于附加方式:

1、MessageChatMemoryAdvisor 将历史记录作为消息集合直接附加到提示中。

2、PromptChatMemoryAdvisor 将历史记录合并到提示的系统文本(System Text)中。

3、VectorStoreChatMemoryAdvisor 通过向量存储(Vector Store)管理对话历史,支持更复杂的检索逻辑。构造函数参数包括:

  • VectorStore:向量存储实例(如数据库)
  • defaultConversationId:默认对话ID
  • chatHistoryWindowSize:历史记录窗口大小(以令牌数计)
  • order:Advisor链中的执行顺序。

示例:客服助手服务 以下 @Service 类结合了多种顾问实现:

@Service
public class CustomerSupportAssistant {

    private final ChatClient chatClient;

    public CustomerSupportAssistant(
        ChatClient.Builder builder, 
        VectorStore vectorStore, 
        ChatMemory chatMemory) {

        this.chatClient = builder
            .defaultSystem("""
                你是名为"Funnair"的航空公司的客服代理,需以友好、乐于助人且愉快的方式回应。
                在提供预订信息或取消预订前,必须获取用户的预订号、姓名。
                修改预订前需确认是否符合条款,若涉及费用需先获得用户同意。
                """)
            .defaultAdvisors(
                new MessageChatMemoryAdvisor(chatMemory), // 聊天记忆 [[9]]
                new QuestionAnswerAdvisor(vectorStore),   // 检索增强生成(RAG)[[3]][[8]]
                new SimpleLoggerAdvisor()                 // 日志记录
            )
            .defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // 函数调用 [[7]]
            .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) // 绑定对话ID [[4]]
                .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))      // 历史记录窗口大小 [[8]]
            .stream()
            .content();
    }
}

关键点说明

  • 对话历史管理 :通过 MessageChatMemoryAdvisor 附加历史记录,确保上下文连贯性。
  • 检索增强生成(RAG) :QuestionAnswerAdvisor 结合向量存储检索外部数据,提升响应准确性。
  • 函数调用 :defaultFunctions 注册的函数(如 getBookingDetails)允许AI执行业务逻辑。

日志记录(Logging)

SimpleLoggerAdvisor 是一个用于记录 ChatClient 请求与响应数据的顾问,可用于调试和监控AI交互。 Spring AI 支持对大语言模型(LLM)和向量存储交互的可观测性。更多细节请参考《可观测性指南》。

1、启用日志记录

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

ChatResponse response = ChatClient.create(chatModel).prompt()
        .advisors(new SimpleLoggerAdvisor()) // 添加日志顾问 [[9]]
        .user("讲个笑话?")
        .call()
        .chatResponse();

在 application.properties 或 application.yml 中设置日志级别为 DEBUG 以查看日志:

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

2、自定义日志内容
通过构造函数可自定义记录的请求(AdvisedRequest)和响应(ChatResponse)数据:

SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -> "自定义请求: " + request.getUserText(), // 自定义请求日志格式
    response -> "自定义响应: " + response.getResult()  // 自定义响应日志格式
);

此功能允许根据需求调整日志内容,例如仅记录关键字段或添加标记。