Skip to content

SpringAI 统一门面接口 ChatClient

ChatClient 通过 Fluent API 模式来统一设置提示语 / 提示语模板 / 模型参数 / 工具与 MCP / 记忆 Memory / Advisor 等,同时提供了同步和流式调用模式,通过类型转换器转换为标准 Java 实体等能力,方便的构建自己的 AI 程序。

创建 ChatClient 的两种方式

java
@Configuration
public class SpringAIConfig {
    /**
     * 第一种创建 ChatClient 的方式,使用默认的 builder 参数
     *
     * @param chatModel 该对象会根据配置自动生成
     * @return ChatClient 对象。
     */
    @Bean
    public ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.create(chatModel);
    }

    /**
     * 第二种创建 ChatClient 的方式,使用 builder 来设置自定义参数
     *
     * @return ChatClient 对象。
     */
    @Bean
    public ChatClient customChatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .defaultSystem("You are a helpful assistant.")
                .build();
    }
}

说明:ChatModel 模型会根据配置自动注入。

启动 ChatClient Fluent API

ChatClient Fluent API 提供了以下三种 prompt 方法来启动 Fluent API:

方法定义:

java
ChatClientRequestSpec prompt();
ChatClientRequestSpec prompt(String content);
ChatClientRequestSpec prompt(Prompt prompt);

使用:

java
@RestController
public class Controller {
    @Resource
    private ChatClient chatClient;

    @RequestMapping("/1")
    public String execute1(@RequestParam("userRequest") String userRequest) {
        return chatClient.prompt().user(userRequest).call().content();
    }

    @RequestMapping("/2")
    public String execute2(@RequestParam("userRequest") String userRequest) {
        return chatClient.prompt(new Prompt(userRequest)).call().content();
    }

    @RequestMapping("/3")
    public String execute3(@RequestParam("userRequest") String userRequest) {
        return chatClient.prompt(userRequest).call().content();
    }
}

提示语模板 PromptTemplate

java
/**
 * 使用默认提示语模板 StTemplateRenderer
 */
@RequestMapping("/100")
public String execute100() {
    return chatClient.prompt()
            .user(u -> u.text("Tell me the names of 5 movies those act  by {actor}").param("actor", "周星驰"))
            .call().content();
}

/**
 * 定制提示语模板
 */
@RequestMapping("/101")
public String execute101() {
    return chatClient.prompt()
            .user(u -> u.text("Tell me the names of 5 movies those act  by <actor>").param("actor", "刘德华"))
            .templateRenderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
            .call().content();
}

说明:

  • ChatClient 提供了 user 提示语模板和 system 提示语模板, 其中的变量可以在运行时被替换
  • 默认使用 StTemplateRenderer 引擎,如果想要使用自定义的模板引擎,可以自己实现接口 TemplateRenderer
  • 默认使用 { 作为参数占位符起始标识,使用 } 作为参数占位符结束标识,当发现这两个字符会被提示语或者参数占用时,可以定制提示语模板参数占位符分隔符,如上 execute101() 方法所示

ChatClient 响应

从返回方式上分为:同步(call())和流式(stream())。

同步响应

call() 响应类型有几种不同的选项:

  • String content():返回响应的字符串内容
  • ChatResponse chatResponse()ChatResponse 包含响应的元数据的对象,例如,token 的消耗
  • ChatClientResponse chatClientResponse()ChatClientResponse 包含 ChatResponse 对象和 ChatClient 执行上下文的对象,使您能够访问 advisors 执行期间使用的附加数据(例如,在 RAG 流中检索到的相关文档)
  • ResponseEntity<?> responseEntity()ResponseEntity 包含完整 HTTP 响应(包括状态码、标头和正文)的响应
  • entity():返回 Java 类型
    • entity(ParameterizedTypeReference<T> type):返回 Collection 实体类型
    • entity(Class<T> type):返回特定的实体类型
    • entity(StructuredOutputConverter<T> structuredOutputConverter):指定 StructuredOutputConverter,将 StringT 类型。

说明:调用 call() 方法并不会真正触发 AI 模型的执行。它只是指示 Spring AI 是使用同步调用还是流式调用。实际的 AI 模型调用发生在调用 content()chatResponse()responseEntity() 等方法时。

java
/**
* 返回 String
*/
@RequestMapping("/1")
public String execute1(@RequestParam("userRequest") String userRequest) {
    return chatClient.prompt().user(userRequest).call().content();
}
    
/**
* 返回 ChatResponse
* 包含 token 信息
*/
@RequestMapping("/4")
public ChatResponse execute4(@RequestParam("userRequest") String userRequest) {
    return chatClient.prompt(userRequest).call().chatResponse();
}

/**
* 返回 ResponseEntity
*/
@RequestMapping("/40")
public org.springframework.ai.chat.client.ResponseEntity<ChatResponse, String> execute40() {
    return chatClient.prompt("Tell me a joke").call().responseEntity(String.class);
}

/**
* 返回 Java 类
*/
@RequestMapping("/5")
public ActorFilms execute5() {
    return chatClient.prompt("Generate the filmography for a random actor.").call().entity(ActorFilms.class);
}

/**
* 返回 Java 列表
*/
@RequestMapping("/6")
public List<ActorFilms> execute6() {
    return chatClient.prompt("Generate the filmography of 5 movies for 周星驰 and 刘德华.").call().entity(new ParameterizedTypeReference<>() {
});

Java 类如下:

java
import lombok.Data;
import lombok.experimental.Accessors;

import java.util.List;

@Data
@Accessors(chain = true)
public class ActorFilms {
    private String actor;
    private List<String> films;
}

流式响应

stream() 响应类型有几种不同的选项:

  • Flux<String> content():返回响应的字符串内容
  • Flux<ChatResponse> chatResponse()ChatResponse 包含响应的元数据的对象
  • Flux<ChatClientResponse> chatClientResponse()ChatClientResponse 包含 ChatResponse 对象和 ChatClient 执行上下文的对象

说明:stream() 调用方式下,无法直接返回 Java 对象,需要借助类型转换器进行转换,见如下的代码操作。

java
/**
 * 流式返回 String
 * 包含 token 信息
 */
@RequestMapping("/7")
public Flux<String> execute7() {
    return chatClient.prompt("Tell me a joke").stream().content();
}

/**
 * 流式返回 ChatResponse
 * 包含 token 信息
 */
@RequestMapping("/8")
public Flux<ChatResponse> execute8() {
    return chatClient.prompt("Tell me a joke").stream().chatResponse();
}

/**
 * 流式返回信息,转换为 Java 类
 */
@RequestMapping("/9")
public ActorFilms execute9() {
    /*
     * 创建 Converter
     */
    BeanOutputConverter<ActorFilms> converter = new BeanOutputConverter<>(ActorFilms.class);

    /*
     * 流式调用
     */
    Flux<String> flux = 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());

    /*
     * 转换
     */
    return converter.convert(content);
}

/**
 * 流式返回信息,转换为 Java 列表
 */
@RequestMapping("/10")
public List<ActorFilms> execute10() {
    /*
     * 创建 Converter
     */
    BeanOutputConverter<List<ActorFilms>> converter = new BeanOutputConverter<>(new ParameterizedTypeReference<>() {
    });

    /*
     * 流式调用
     */
    Flux<String> flux = 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());

    /*
     * 转换
     */
    return converter.convert(content);
}

类型转换器会根据返回的不同的 Java 模型制定不同的 format 提示语。例如对于返回 ActorFilms,format 提示语如下(注意观察 schema 部分):

text
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "actor" : {
      "type" : "string"
    },
    "films" : {
      "type" : "array",
      "items" : {
        "type" : "string"
      }
    }
  },
  "additionalProperties" : false
}```

对于返回 List<ActorFilms>,format 提示语如下(注意观察 schema 部分):

text
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "array",
  "items" : {
    "type" : "object",
    "properties" : {
      "actor" : {
        "type" : "string"
      },
      "films" : {
        "type" : "array",
        "items" : {
          "type" : "string"
        }
      }
    },
    "additionalProperties" : false
  }
}```

ChatClient 默认选项设置

使用 ChatClient.Builder 创建 ChatClient 实例时,可以全局指定以下的默认配置项来设置默认配置:

  • defaultOptions:核心是模型参数配置,例如温度,在模型介绍的章节会详细介绍
  • defaultSystem:system 提示语,可以包含参数
  • defaultUser:user 提示语,可以包含参数
  • defaultTemplateRenderer:提示语模板
  • defaultToolCallbacks/defaultTools/defaultToolContext/defaultToolNames:工具与 mcp
  • defaultAdvisorsadvisors

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

  • options
  • system
  • user
  • templateRenderer
  • toolCallbacks/tools/toolContext/toolNames
  • advisors

在运行时给带参数占位符的默认配置项设置参数值。

java
@Configuration
public class SpringAIConfig {
    @Bean
    public ChatClient customChatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .defaultSystem("You are a helpful assistant.{actor}")
                .build();
    }
}
java
@RequestMapping("/1000")
public String execute1000() {
    return chatClient.prompt()
            .system(s -> s.param("actor", "周星驰")) // 直接设置参数
            .user("tell me a joke").call().content();
}

Advisors

Advisors 提供了一种灵活而强大的方法来拦截、修改和增强 Spring AI 应用程序。

例如,使用 user 提示语调用 AI 模型时的一个常见模式是 使用上下文数据扩充提示。 这些上下文数据可以是不同类型的。常见的类型包括:

  • 企业私有数据:这是 AI 模型尚未训练过的数据
  • 对话历史记录:聊天模型的 API 是无状态的。如果您告诉 AI 模型您的姓名,它不会在后续交互中记住它。每次请求都必须发送对话历史记录,以确保在生成响应时考虑到之前的交互

ChatClient 提供了一个 AdvisorSpec 接口用于配置 Advisor 的接口。该接口提供了添加参数、一次性设置多个参数以及将一个或多个 Advisors 添加到链中的方法。

java
interface AdvisorSpec {
    AdvisorSpec param(String k, Object v);
    AdvisorSpec params(Map<String, Object> p);
    AdvisorSpec advisors(Advisor... advisors);
    AdvisorSpec advisors(List<Advisor> advisors);
}

关于 Advisors 更多细节,我们在后续的章节继续深入,这里我们以一个简单的日志记录为例,来了解其基础用法。

SimpleLoggerAdvisor 一个用于记录请求模型的 request 和模型返回的 response 数据的 Advisor。可以用于调试和监控 AI 交互。

application.properties 文件种新增如下配置,开启日志:

text
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
java
/**
 * 使用默认的日志方式
 */
@RequestMapping("/10000")
public String execute10000() {
    return chatClient.prompt()
            .advisors(new SimpleLoggerAdvisor())
            .user("tell me a joke")
            .call().content();
}

/**
 * 定制日志
 */
@RequestMapping("/10001")
public String execute10001() {
    return chatClient.prompt()
            .advisors(new SimpleLoggerAdvisor(
                    request -> "Custom request: " + request.prompt().getUserMessage(),
                    response -> "Custom response: " + response.getResult(),
                    0
            ))
            .user("tell me a joke")
            .call().content();
}

注意:假设有多个 Advisors,每个 Advisor 添加到链中的顺序至关重要,因为它决定了它们的执行顺序。每个 Advisor 都会以某种方式修改提示或上下文,并且一个 Advisor 所做的更改会传递给链中的下一个 Advisor

ChatMemory

ChatMemory 接口表示聊天对话内存存储。它提供了向对话添加消息、从对话中检索消息以及清除对话历史记录的方法;对话需要设置 conversationId,用于不同的对话区分。

接口定义如下:

java
public interface ChatMemory {
	String DEFAULT_CONVERSATION_ID = "default";

	/**
	 * The key to retrieve the chat memory conversation id from the context.
	 */
	String CONVERSATION_ID = "chat_memory_conversation_id";

	/**
	 * Save the specified message in the chat memory for the specified conversation.
	 */
	default void add(String conversationId, Message message) {
		Assert.hasText(conversationId, "conversationId cannot be null or empty");
		Assert.notNull(message, "message cannot be null");
		this.add(conversationId, List.of(message));
	}

	/**
	 * Save the specified messages in the chat memory for the specified conversation.
	 */
	void add(String conversationId, List<Message> messages);

	/**
	 * Get the messages in the chat memory for the specified conversation.
	 */
	List<Message> get(String conversationId);

	/**
	 * Clear the chat memory for the specified conversation.
	 */
	void clear(String conversationId);
}

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

MessageWindowChatMemory 内部通过 ChatMemoryRepository 接口提供的内存实现来实现消息存储,ChatMemoryRepository 的实现包括 InMemoryChatMemoryRepository / JdbcChatMemoryRepository / CassandraChatMemoryRepository / Neo4jChatMemoryRepository 等,在 memory 章节我们会详细介绍。

ChatClient 是通过 Advisors 机制来使用 ChatMemory 的。

java
@Bean
public ChatMemory messageWindowChatMemory() {
    return MessageWindowChatMemory.builder().maxMessages(10).build();
}
java
@Resource
private ChatMemory messageWindowChatMemory;

@RequestMapping("/20000")
public String execute20000() {
    return chatClient.prompt()
            .advisors(
                    MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build(),
                    new SimpleLoggerAdvisor())
            .user("tell me a joke")
            .call().content();
}

文章的最后,如果您觉得本文对您有用,请打赏一杯咖啡!感谢!