写在前面

本篇开始学习学习Spring Ai提供的Chat Client Api,通过它可以很轻松的与大模型进行交互。

ChatClient API

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

流畅的API具有构建提示词组成部分的方法,该提示词会作为输入传递给AI模型。Prompt包含用于指导AI模型输出和行为的指令性文本。从API的角度来看,提示词由一系列消息组成。

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

  1. 用户消息,即用户的直接输入。
  2. 系统消息,由系统生成以指导对话。

这些消息通常包含占位符,这些占位符在运行时会根据用户输入进行替换,以自定义AI模型对用户输入的响应。

还可以指定一些提示选项,如:

  • AI模型的名称,即要使用的AI模型的名称。
  • 温度设置,控制生成输出的随机性或创造性。

这些功能使得ChatClient成为一个强大的工具,允许开发者以灵活的方式与AI模型进行交互,并通过定制化的提示和消息来优化AI模型的响应。

创建ChatClient

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

使用自动配置的ChatClient.Builder

Spring AI提供Spring Boot自动配置,会创建一个原型<font style="color:rgb(25, 30, 30);">ChatClient.Builder</font>bean,供开发者注入到自己的类中。

以下是一个简单的示例,用于检索对简单用户请求的字符串响应。新建一个名为controller的包,并在里面定义一个名为MyController的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class MyController {
private ChatClient chatClient;

public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}

@GetMapping("/ai")
public String generation(@RequestParam String userInput) {
return this.chatClient.prompt()
.user(userInput)
.call()
.content();
}
}

运行项目,访问对应地址,返回信息如下:

用户传入了消息,之后调用call()方法向 AI 模型发送请求,content()方法将AI模型的响应作为字符串返回。

手动创建ChatClient

默认情况下,Spring AI会自动配置一个ChatClient.Builder bean。不过,当你的应用程序中需要使用多个聊天模型时,就需要手动创建ChatClient。首先必须通过设置属性spring.ai.chat.client.enabled=false来禁用ChatClient.Builder的自动配置,之后才允许开发者手动创建多个ChatClient实例。

单一模型类型的多个****ChatClient

一个常见的使用场景,比如开发者需要创建多个ChatClient实例,但这些实例都使用相同的基础模型类型,只是配置不同:

1
2
3
4
5
6
7
8
ChatModel myChatModel = ... // 通常通过自动装配获得
ChatClient chatClient = ChatClient.create(myChatModel);

//或者使用默认构建器设置创建一个ChatClient
ChatClient.Builder builder = ChatClient.builder(myChatModel);
ChatClient customChatClient = builder
.defaultSystemPrompt("一些默认的系统提示词")
.build();

不同模型类型的****ChatClient

当你需要使用多个AI模型时,可以为每个模型定义单独的<font style="color:rgb(25, 30, 30);">ChatClient</font> bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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注入到自己的应用组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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;

// Model selection
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");
}

// Use the selected chat client
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时,这一点特别有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
@Slf4j
public class MultiModelService {
@Autowired
private OpenAiChatModel baseChatModel;

@Autowired
private OpenAiApi baseOpenAiApi;

public void multiClientFlow() {
try {
// Derive a new OpenAiApi for Groq (Llama3)
OpenAiApi groqApi = baseOpenAiApi.mutate()
.baseUrl("https://api.groq.com/openai")
.apiKey(System.getenv("GROQ_API_KEY"))
.build();

// Derive a new OpenAiApi for OpenAI GPT-4
OpenAiApi gpt4Api = baseOpenAiApi.mutate()
.baseUrl("https://api.openai.com")
.apiKey(System.getenv("OPENAI_API_KEY"))
.build();

// Derive a new OpenAiChatModel for Groq
OpenAiChatModel groqModel = baseChatModel.mutate()
.openAiApi(groqApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("llama3-70b-8192").temperature(0.5).build())
.build();

// Derive a new OpenAiChatModel for GPT-4
OpenAiChatModel gpt4Model = baseChatModel.mutate()
.openAiApi(gpt4Api)
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4").temperature(0.7).build())
.build();

// Simple prompt for both models
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();

log.info("Groq (Llama3) response: {}", groqResponse);
log.info("OpenAI GPT-4 response: {}", gpt4Response);
}
catch (Exception e) {
log.error("Error in multi-client flow", e);
}
}
}

ChatClient流畅API(ChatClient Fluent API)

<font style="color:rgb(25, 30, 30);">ChatClient</font>流畅 PI允许开发者通过重载的<font style="color:rgb(25, 30, 30);">prompt</font>方法来启动流畅 API,从而以三种不同的方式创建提示词:

  • <font style="color:rgb(25, 30, 30);">prompt()</font>:此无参数方法可让您开始使用流式 API,从而构建提示词的用户部分、系统部分和其他部分。
  • <font style="color:rgb(25, 30, 30);">prompt(Prompt prompt)</font>:此方法接受一个<font style="color:rgb(25, 30, 30);">Prompt</font>参数,允许你传入一个使用Prompt的非流式API创建的<font style="color:rgb(25, 30, 30);">Prompt</font>实例。
  • <font style="color:rgb(25, 30, 30);">prompt(String content)</font>:这是一个与之前的重载类似的便捷方法。它接收用户的文本内容。

ChatClient响应

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

返回ChatResponse

AI模型的响应是一种由ChatResponse类型定义的丰富结构。它包含关于响应生成方式的元数据,还可以包含多个响应(称为Generation),每个响应都有自己的元数据。元数据包括用于生成响应的令牌数量(每个令牌大约相当于一个单词的3/4)。这一信息很重要,因为托管的人工智能模型会根据每个请求所使用的令牌数量来收费。

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

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

返回实体

开发者如果希望返回一个从返回的String映射而来的实体类。entity()方法就提供了这一功能。

举个例子,给定以下Java记录:

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

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

1
2
3
4
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);

还有一个重载的entity方法,其签名为entity(ParameterizedTypeReference<T> type),可让你指定诸如泛型列表之类的类型:

1
2
3
4
List<ActorFilms> actorFilms = chatClient.prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});

流式响应

<font style="color:rgb(25, 30, 30);">strea</font>m()方法可让你获取异步响应,如下所示:

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

您也可以使用<font style="color:rgb(25, 30, 30);">Flux<ChatResponse> chatResponse()</font>方法来流式传输<font style="color:rgb(25, 30, 30);">ChatResponse</font>

未来,我们将提供一种便捷方法,让您能够使用响应式的<font style="color:rgb(25, 30, 30);">stream()</font>方法返回Java实体。在此期间,您应该像下面所示的那样,使用结构化输出转换器来显式转换聚合响应。如下所示。这也展示了流畅 API 中参数的使用,我们将在文档的后续部分详细讨论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

提示模板(Prompt Templates)

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

1
2
3
4
5
6
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,用于不需要模板处理的情况。

直接在<font style="color:rgb(25, 30, 30);">ChatClient</font>上配置(通过<font style="color:rgb(25, 30, 30);">.templateRenderer()</font>)的<font style="color:rgb(25, 30, 30);">TemplateRenderer</font>仅适用于在<font style="color:rgb(25, 30, 30);">ChatClient</font>构建器链中直接定义的提示内容(例如,通过<font style="color:rgb(25, 30, 30);">.user()</font><font style="color:rgb(25, 30, 30);">.system()</font>)。它不会影响Advisors(如<font style="color:rgb(25, 30, 30);">QuestionAnswerAdvisor</font>)内部使用的模板,这些模板有自己的模板自定义机制(参见 Custom Advisor Templates)。

如果您更愿意使用不同的模板引擎,可以直接向ChatClient提供<font style="color:rgb(25, 30, 30);">TemplateRenderer</font>接口的自定义实现。您也可以继续使用默认的<font style="color:rgb(25, 30, 30);">StTemplateRenderer</font>,但需采用自定义配置。

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

1
2
3
4
5
6
7
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()返回值

<font style="color:rgb(25, 30, 30);">ChatClient</font>上指定<font style="color:rgb(25, 30, 30);">call()</font>方法后,响应类型有几种不同的选项:

  • <font style="color:rgb(25, 30, 30);">String content()</font>:返回响应的字符串内容。
  • <font style="color:rgb(25, 30, 30);">ChatResponse chatResponse()</font>:返回包含多个生成内容以及响应元数据(例如创建响应所用的令牌数量)的<font style="color:rgb(25, 30, 30);">ChatResponse</font>对象。
  • <font style="color:rgb(25, 30, 30);">ChatClientResponse chatClientResponse()</font>:返回一个<font style="color:rgb(25, 30, 30);">ChatClientResponse</font>对象,该对象包含<font style="color:rgb(25, 30, 30);">ChatResponse</font>对象和ChatClient的执行上下文,使您能够访问顾问执行期间使用的额外数据(例如,在RAG流程中检索到的相关文档)。
  • <font style="color:rgb(25, 30, 30);">ResponseEntity<?> responseEntity()</font>:返回一个包含完整HTTP响应的<font style="color:rgb(25, 30, 30);">ResponseEntity</font>,包括状态码、头部和正文。当你需要访问响应的低级HTTP细节时,这很有用。
  • <font style="color:rgb(25, 30, 30);">entity()</font> 返回一个Java类型。
    • <font style="color:rgb(25, 30, 30);">entity(ParameterizedTypeReference<T> type)</font>:用于返回实体类型的<font style="color:rgb(25, 30, 30);">Collection</font>
    • <font style="color:rgb(25, 30, 30);">entity(Class<T> type)</font>:用于返回特定的实体类型。
    • <font style="color:rgb(25, 30, 30);">entity(StructuredOutputConverter<T> structuredOutputConverter)</font>:用于指定一个<font style="color:rgb(25, 30, 30);">StructuredOutputConverter</font>实例,将<font style="color:rgb(25, 30, 30);">String</font>转换为实体类型。

开发者也可以调用<font style="color:rgb(25, 30, 30);">stream()</font>方法来替代<font style="color:rgb(25, 30, 30);">call()</font>方法。

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

stream()返回值

<font style="color:rgb(25, 30, 30);">ChatClient</font>上指定<font style="color:rgb(25, 30, 30);">stream()</font>方法后,响应类型有几个选项:

  • <font style="color:rgb(25, 30, 30);">Flux<String> content()</font>:返回由AI模型生成的字符串的<font style="color:rgb(25, 30, 30);">Flux</font>
  • <font style="color:rgb(25, 30, 30);">Flux<ChatResponse> chatResponse()</font>:返回<font style="color:rgb(25, 30, 30);">Flux</font>类型的<font style="color:rgb(25, 30, 30);">ChatResponse</font>对象,该对象包含有关响应的附加元数据。
  • <font style="color:rgb(25, 30, 30);">Flux<ChatClientResponse> chatClientResponse()</font>:返回一个<font style="color:rgb(25, 30, 30);">Flux</font>类型的<font style="color:rgb(25, 30, 30);">ChatClientResponse</font>对象,该对象包含<font style="color:rgb(25, 30, 30);">ChatResponse</font>对象和ChatClient的执行上下文,使你能够访问在顾问执行过程中使用的额外数据(例如,在RAG流程中检索到的相关文档)。

使用默认值(using defaults)

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

默认系统文本

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

1
2
3
4
5
6
7
8
9
@Configuration
class Config {

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

一个@RestController来调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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调用应用程序端点时,结果如下:

1
2
❯ curl localhost:8080/ai/simple
{"completion":"海盗为什么去喜剧俱乐部?为了听一些 “海盗级” 笑话呀!哈哈,伙计!"}

带参数的默认系统文本

在下面的示例中,我们将在系统文本中使用占位符,以在运行时而非设计时指定补全内容的语气:

1
2
3
4
5
6
7
8
9
10
@Configuration
class Config {

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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调用应用程序端点时,结果如下:

1
2
3
4
http localhost:8080/ai voice=='海盗'
{
"completion": "你在跟我说话吗?好,给你讲个笑话:为什么自行车自己站不起来?因为它 “太疲惫” 了!这可是经典笑话,对吧?"
}

其他默认值

<font style="color:rgb(25, 30, 30);">ChatClient.Builder</font>级别,您可以指定默认的提示配置。

  • <font style="color:rgb(25, 30, 30);">defaultOptions(ChatOptions chatOptions)</font>:传入ChatOptions类中定义的可移植选项,或特定于模型的选项(如OpenAiChatOptions中的选项)。
  • <font style="color:rgb(25, 30, 30);">defaultFunction(String name, String description, java.util.function.Function<I, O> function)</font><font style="color:rgb(25, 30, 30);">name</font>用于在用户文本中引用该函数。<font style="color:rgb(25, 30, 30);">description</font>解释函数的用途,并帮助AI模型选择正确的函数以获得准确的响应。<font style="color:rgb(25, 30, 30);">function</font>参数是模型在必要时将执行的Java函数实例。
  • <font style="color:rgb(25, 30, 30);">defaultFunctions(String… functionNames)</font>:在应用上下文中定义的java.util.Function的Bean名称。
  • <font style="color:rgb(25, 30, 30);">defaultUser(String text)</font><font style="color:rgb(25, 30, 30);">defaultUser(Resource text)</font><font style="color:rgb(25, 30, 30);">defaultUser(Consumer<UserSpec> userSpecConsumer)</font>:这些方法允许你定义用户文本。<font style="color:rgb(25, 30, 30);">Consumer<UserSpec></font>支持你使用lambda来指定用户文本和任何默认参数。
  • <font style="color:rgb(25, 30, 30);">defaultAdvisors(Advisor… advisor)</font>:Advisor程序允许修改用于创建<font style="color:rgb(25, 30, 30);">Prompt</font>的数据。<font style="color:rgb(25, 30, 30);">QuestionAnswerAdvisor</font>实现通过在提示词后附加与用户文本相关的上下文信息,支持<font style="color:rgb(25, 30, 30);">Retrieval Augmented Generation</font>模式。
  • <font style="color:rgb(25, 30, 30);">defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer)</font>:此方法允许您定义一个<font style="color:rgb(25, 30, 30);">Consumer</font>,以使用<font style="color:rgb(25, 30, 30);">AdvisorSpec</font>配置多个Advisor。Advisor可以修改用于创建最终<font style="color:rgb(25, 30, 30);">Prompt</font>的数据。<font style="color:rgb(25, 30, 30);">Consumer<AdvisorSpec></font>允许您指定一个lambda来添加Advisor,例如QuestionAnswerAdvisor,它通过根据用户文本附加相关上下文信息来支持<font style="color:rgb(25, 30, 30);">Retrieval Augmented Generation</font>

你也可以在运行时使用相应的方法(不带<font style="color:rgb(25, 30, 30);">default</font>前缀)来覆盖这些默认值:

  • <font style="color:rgb(25, 30, 30);">options(ChatOptions chatOptions)</font>
  • <font style="color:rgb(25, 30, 30);">function(String name, String description, java.util.function.Function<I, O> function)</font>
  • <font style="color:rgb(25, 30, 30);">functions(String… functionNames)</font>
  • <font style="color:rgb(25, 30, 30);">user(String text)</font><font style="color:rgb(25, 30, 30);">user(Resource text)</font><font style="color:rgb(25, 30, 30);">user(Consumer<UserSpec> userSpecConsumer)</font>
  • <font style="color:rgb(25, 30, 30);">advisors(Advisor… advisor)</font>
  • <font style="color:rgb(25, 30, 30);">advisors(Consumer<AdvisorSpec> advisorSpecConsumer)</font>

顾问(Advisors)

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

在使用用户文本调用人工智能模型时,一种常见模式是在提示词中添加或补充上下文数据。

这些上下文数据可以有不同的类型。常见类型包括:

  • 您自己的数据:这是AI模型未经过训练的数据。即便模型见过类似的数据,附加的上下文数据在生成响应时也具有优先性。
  • 对话历史记录:聊天模型的API是无状态的。如果你告诉AI模型你的名字,它在后续交互中不会记住这个名字。每次请求都必须发送对话历史,以确保生成响应时会考虑之前的交互。

ChatClient中的顾问配置

ChatClient流畅API 提供了一个 AdvisorSpec 接口,用于配置顾问。该接口提供了添加参数、一次性设置多个参数以及向链中添加一个或多个顾问的方法。

1
2
3
4
5
6
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}

向链中添加顾问的顺序至关重要,因为它决定了顾问的执行顺序。每个顾问以某种方式修改提示或上下文,一个顾问所做的更改会传递给链中的下一个。

1
2
3
4
5
6
7
8
9
10
ChatClient.builder(chatModel)
.build()
.prompt()
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.user(userText)
.call()
.content();

在上述配置下,MessageChatMemoryAdvisor将首先执行,把对话历史添加到提示词中。然后,QuestionAnswerAdvisor会根据用户的问题和添加的对话历史进行搜索,有可能提供更相关的结果。

检索增强生成

向量数据库存储了 AI 模型不知道的数据。当用户问题发送到 AI 模型时,QuestionAnswerAdvisor 会为与用户问题相关的文档查询向量数据库。向量数据库的响应被追加到用户文本中,为 AI 模型生成响应提供上下文。

假设您已经将数据加载到 VectorStore 中,您可以通过向ChatClient提供QuestionAnswerAdvisor实例来执行检索增强生成(RAG):

1
2
3
4
5
6
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.user(userText)
.call()
.chatResponse();

在此示例中,SearchRequest.defaults() 将在 Vector Database 中对所有文档执行相似性搜索。要限制搜索的文档类型,SearchRequest 接受一个 SQL 样式的过滤表达式,该表达式在所有 VectorStores 中都是可移植的。

动态过滤表达式

使用 FILTER_EXPRESSION 顾问上下文参数在运行时更新 SearchRequest 过滤表达式:

1
2
3
4
5
6
7
8
9
10
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.build();

// 在运行时更新过滤表达式
String content = this.chatClient.prompt()
.user("Please answer my question XYZ")
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Spring'"))
.call()
.content();

FILTER_EXPRESSION 参数允许开发者根据提供的表达式动态过滤搜索结果。

日志记录

SimpleLoggerAdvisor是一个记录器顾问,它会记录ChatClientrequestresponse数据。这对于调试和监控开发者的人工智能交互很有用。

Spring AI支持对大语言模型和向量存储交互的可观测性。

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

1
2
3
4
5
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.user("Tell me a joke?")
.call()
.chatResponse();

要查看日志,请将advisor包的日志级别设置为<font style="color:rgb(25, 30, 30);">DEBUG</font>

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

将此添加到您的application.propertiesapplication.yaml文件中。

开发者可以使用以下构造函数来自定义要记录的来自AdvisedRequestChatResponse的数据:

1
2
3
4
5
SimpleLoggerAdvisor(
Function<ChatClientRequest, String> requestToString,
Function<ChatResponse, String> responseToString,
int order
)

举个例子,如下所示:

1
2
3
4
5
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(),
response -> "Custom response: " + response.getResult(),
0
);

这样开发者就能够根据自己的特定需求定制记录的信息。

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

聊天记忆(ChatMemory)

<font style="color:rgb(25, 30, 30);">ChatMemory</font>接口代表聊天对话记忆的存储。它提供了向对话添加消息、从对话检索消息以及清除对话历史的方法。

目前有一个内置实现:<font style="color:rgb(25, 30, 30);">MessageWindowChatMemory</font><font style="color:rgb(25, 30, 30);">MessageWindowChatMemory</font>是一种聊天记忆实现方式,它会维持一个消息窗口,消息数量最多为指定的最大规模(默认值:20条消息)。当消息数量超过这一限制时,较早的消息会被移除,但系统消息会被保留。如果添加了新的系统消息,所有之前的系统消息都会从记忆中删除。这确保了对话中始终能获取最新的上下文,同时控制了记忆的使用量。

<font style="color:rgb(25, 30, 30);">MessageWindowChatMemory</font><font style="color:rgb(25, 30, 30);">ChatMemoryRepository</font>抽象提供支持,该抽象为聊天对话记忆提供了存储实现。目前有多种可用的实现,包括<font style="color:rgb(25, 30, 30);">InMemoryChatMemoryRepository</font><font style="color:rgb(25, 30, 30);">JdbcChatMemoryRepository</font><font style="color:rgb(25, 30, 30);">CassandraChatMemoryRepository</font><font style="color:rgb(25, 30, 30);">Neo4jChatMemoryRepository</font>

实现说明

<font style="color:rgb(25, 30, 30);">ChatClient</font>中,命令式编程模型和响应式编程模型的结合使用是该API的一个独特之处。通常,一个应用程序要么采用响应式编程,要么采用命令式编程,而不会两者兼而有之。

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

由于Spring Boot3.4中存在一个漏洞,必须设置“spring.http.client.factory=jdk”属性。否则,该属性会默认设为“reactor”,这会破坏某些AI工作流,如ImageModel。

  • 流处理仅通过响应式栈支持。因此,命令式应用程序必须包含响应式栈(例如,spring-boot-starter-webflux)。
  • 非流式处理仅通过Servlet栈提供支持。因此,响应式应用程序必须包含Servlet栈(例如spring-boot-starter-web),并需接受一些调用会是阻塞式的。
  • 工具调用是必要的,但会导致工作流阻塞。这还会造成Micrometer观测的部分中断(例如,ChatClient跨度和工具调用跨度未连接,因此前者仍不完整)。
  • 内置的顾问对标准调用执行阻塞操作,对流式调用执行非阻塞操作。可通过每个顾问类上的构建器配置用于顾问流式调用的Reactor调度器。