3.2 Spring AI Chat Client API

34 阅读15分钟

Spring AI Chat Client API

概述

ChatClient提供了与AI模型通信的流式API(Fluent API)。它支持同步和流式编程模型。

注意: 请参阅本文档底部关于ChatClient中命令式和响应式编程模型组合使用的实现说明

流式API包含构建作为输入传递给AI模型的Prompt的组成部分的方法。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()方法以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:

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

            // 使用选定的聊天客户端
            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 {

    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 = "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 Fluent API

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

  • prompt(): 这个无参数方法让您开始使用流式API,允许您构建用户、系统和其他提示部分。
  • prompt(Prompt prompt): 这个方法接受Prompt参数,让您传入使用提示的非流式API创建的Prompt实例。
  • prompt(String content): 这是一个类似于前一个重载的便利方法。它接受用户的文本内容。

ChatClient Responses

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

返回ChatResponse

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

通过在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<ActorsFilms> actorFilms = this.converter.convert(this.content);

提示模板

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

注意: 直接在ChatClient上配置的TemplateRenderer(通过.templateRenderer())仅适用于直接在ChatClient构建器链中定义的提示内容(例如通过.user().system())。它不会影响Advisors等内部使用的模板,如QuestionAnswerAdvisor,它们有自己的模板自定义机制(参见自定义Advisor模板)。

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

例如,默认情况下,模板变量由{}语法标识。如果您计划在提示中包含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对象,例如创建响应使用了多少令牌。
  • ChatClientResponse chatClientResponse(): 返回一个包含ChatResponse对象和ChatClient执行上下文的ChatClientResponse对象,让您访问advisor执行期间使用的附加数据(例如在RAG流程中检索的相关文档)。
  • ResponseEntity<?> responseEntity(): 返回包含完整HTTP响应的ResponseEntity,包括状态代码、标头和正文。当您需要访问响应的低级HTTP详细信息时,这很有用。
  • entity()返回Java类型
    • entity(ParameterizedTypeReference<T> type): 用于返回实体类型的Collection
    • entity(Class<T> type): 用于返回特定的实体类型。
    • entity(StructuredOutputConverter<T> structuredOutputConverter): 用于指定StructuredOutputConverter实例以将String转换为实体类型。

您也可以调用stream()方法而不是call()

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

stream()返回值

ChatClient上指定stream()方法后,响应类型有几个选项:

  • Flux<String> content(): 返回AI模型生成的字符串的Flux
  • Flux<ChatResponse> chatResponse(): 返回ChatResponse对象的Flux,其中包含响应的附加元数据。
  • Flux<ChatClientResponse> chatClientResponse(): 返回包含ChatResponse对象和ChatClient执行上下文的ChatClientResponse对象的Flux,让您访问advisor执行期间使用的附加数据(例如在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级别,您可以指定默认提示配置。

  • 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实现通过向提示附加与用户文本相关的上下文信息来启用检索增强生成模式。
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): 此方法允许您定义一个Consumer来使用AdvisorSpec配置多个advisors。Advisors可以修改用于创建最终Prompt的数据。Consumer<AdvisorSpec>让您指定一个lambda来添加advisors,如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<UserSpec> userSpecConsumer)
  • advisors(Advisor… advisor)
  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

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将基于用户的问题和添加的对话历史执行搜索,可能会提供更相关的结果。

了解问题回答Advisor

检索增强生成

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

日志记录

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

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

提示: 在生产环境中记录敏感信息时要谨慎。

聊天记忆

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

目前有一个内置实现:MessageWindowChatMemory

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

MessageWindowChatMemoryChatMemoryRepository抽象支持,该抽象为聊天对话记忆提供存储实现。有几种实现可用,包括InMemoryChatMemoryRepositoryJdbcChatMemoryRepositoryCassandraChatMemoryRepositoryNeo4jChatMemoryRepository

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

实现说明

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

  • 在自定义Model实现的HTTP客户端交互时,必须配置RestClient和WebClient。

重要: 由于Spring Boot 3.4中的一个错误,必须设置"spring.http.client.factory=jdk"属性。否则,默认情况下设置为"reactor",这会破坏某些AI工作流程,如ImageModel。

  • 流式传输仅通过响应式堆栈支持。因此,命令式应用程序必须包含响应式堆栈(例如spring-boot-starter-webflux)。
  • 非流式传输仅通过Servlet堆栈支持。因此,响应式应用程序必须包含Servlet堆栈(例如spring-boot-starter-web)并预期某些调用是阻塞的。
  • 工具调用是命令式的,导致阻塞工作流程。这也会导致部分/中断的Micrometer观察(例如,ChatClient span和工具调用span未连接,第一个因此保持不完整)。
  • 内置advisors对标准调用执行阻塞操作,对流式调用执行非阻塞操作。用于advisor流式调用的Reactor Scheduler可以通过每个Advisor类上的Builder进行配置。