0.写在前面
因为Spring Ai版本迭代很快,并且迭代后会有接口的一些变化,所以本文讲解如何在全网都教你该怎么做的情况下,理解为什么要这么做?
1.引入Spring AI
1.1-选择正确的版本
为了给自己的汐落记账增加AI功能,遂探索Spring AI
首先搞明白,目前maven仓库存在两个Spring AI

实际上最新的版本是org.springframework.ai » spring-ai-core
虽然这个依赖的版本号是1.0.0开始的,但这确实是新的版本,我们开发应该使用这个
(实际开发选用的版本)
1.2-引入依赖
因为openai的接口已经成为行业规范,很多大模型都支持openai的接口规范,包括本次我要调用的deepseek官方大模型api也是支持openai规范的,所以这里我选择引入了spring-ai-starter-model-openai,spring ai提供了很多不同的组件可以使用,可以选择自己需要的
这里的版本号为什么是M7,因为我想学最新的,所以就使用了M7,值得注意的是,不同版本之间变化较大,所以本文最终的目的是在你知道怎么做的情况下,尽可能帮助你理解为什么要这样做,这样面对不同的版本,你都可以用深刻的理解快速适应接口的变更
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0-M7</version>
</dependency>
这里可以考虑使用BOM管理依赖的版本,点击下方文字跳转文档
Spring AI官方文档
1.3-配置讲解(重要)
这是一个很重要的板块,因为不同版本的Spring AI这里是有变化的,所以不能直接按照网上的教程写,需要根据自己的版本号判断如何进行配置
下面是yaml错误示范
一般来说网上很多教程会这样写,但实际上这样的写法在1.0.0M7的版本中是不能实现SpringBoot的自动装配的,如果这样写甚至还需要写一个配置类 - AiConfig
这个配置类是这样的
丢份代码在这里
@Configuration
public class AiConfig {
@Value("${ai.openai.api-key}")
private String API_KEY;
@Value("${ai.openai.base-url}")
private String BASE_URL;
@Value("${ai.openai.default-deepseek-model}")
private String DEFAULT_DEEPSEEK_MODEL;
@Bean
public OpenAiApi chatCompletionApi() {
return OpenAiApi.builder()
.apiKey(API_KEY) // 替换为你的API密钥
.baseUrl(BASE_URL) // 替换为你的API基础URL
.build();
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder().model(DEFAULT_DEEPSEEK_MODEL).build())
.build();
}
}
可以看到手动的进行了参数的注入
为什么会这样呢?
因为在这个M7版本的Spring AI中,参数的配置发生了一些变化,如果还按照旧版本来配置就会导致SpringBoot无法正确的将配置注入到需要的类
那么能不能通过正确的配置yaml实现SpringBoot的自动装配呢?
可以的!
正确的写法如下
spring:
ai:
openai:
api-key: ${ai.openai.api-key}
base-url: ${ai.openai.base-url}
chat:
options:
model: ${ai.openai.default-deepseek-model}
可以看到多了个 chat 和 options 的配置层级
api-key和base-url需要写到openai的层级下,chat-options里面需要指定model
这几个选项缺一不可,否则无法正常实现装配!
这样一来,即使不需要配置类也可以实现正确的SpringBoot自动装配!
不同版本Spring AI的配置
我怎么知道自己的版本应该怎么写?
一般来说,文档里会写的很清楚,但是如果你使用的版本较新,可能文档来不及更新,那么你可以在引入依赖后,输入spring.ai来查看配置推荐
2.调用Spring AI
2.1-Service层处理
注入OpenAiChatModel
这里需要使用构造函数注入,如果使用Lombok的注解@RequiredArgsConstructor
是无法顺利注入的,这里需要注意一下!(具体原因暂不了解,可能是还没有适配)
// 注入OpenAiChatModel
private final OpenAiChatModel openAiChatModel;
public ChatService(OpenAiChatModel openAiChatModel) {
this.openAiChatModel = openAiChatModel;
}

简单来说
stream是流式输出,ai输出到哪里,spring ai就返回给前端到哪里
call是等待ai输出所有内容后一并返回给前端
他们分别对应两种支持的传参,Message 和 Prompt
观察call内部代码
这里跟进去看一下代码是怎么实现的
以call为例
可以看到如果直接使用 message 询问,spring ai会将message封装到prompt里面,并且将得到的ChatResponse类型进行解析,取出里面的String类型的ai回复
观察stream内部代码
我们再跟进一下stream
可以看到,在调用stream或者是call的方法时都可以直接返回String类型的响应,转换的过程已经在内部帮我们封装好了,相较于call方法,stream方法的返回值用Flux<>进行的封装,这就是流式输出的精髓
所以我们如果没有特别需要创建新的Prompt的情况,可以直接发送问题给方法,直接输出返回的String数据
观察Prompt内部
这个Prompt是什么呢?跟进去看一下
可以看到Prompt里面可以接受几种属性,其中眼熟的就是Message
和List<Message>
,如果是列表结构的信息,那么Prompt会将里面的信息解析出来用不同的方式处理
不同的Message种类
我们跟进Message看看
Message接口继承自Content,提供了一个getter方法,我们跟进去看看
可以得到这段代码
package org.springframework.ai.chat.messages;
/**
* Enumeration representing types of {@link Message Messages} in a chat application. It
* can be one of the following: USER, ASSISTANT, SYSTEM, FUNCTION. */public enum MessageType {
/**
* A {@link Message} of type {@literal user}, having the user role and originating
* from an end-user or developer. * @see UserMessage
*/
USER("user"),
/**
* A {@link Message} of type {@literal assistant} passed in subsequent input
* {@link Message Messages} as the {@link Message} generated in response to the user.
* @see AssistantMessage
*/
ASSISTANT("assistant"),
/**
* A {@link Message} of type {@literal system} passed as input {@link Message
* Messages} containing high-level instructions for the conversation, such as behave
* like a certain character or provide answers in a specific format. * @see SystemMessage
*/
SYSTEM("system"),
/**
* A {@link Message} of type {@literal function} passed as input {@link Message
* Messages} with function content in a chat application.
* @see ToolResponseMessage
*/
TOOL("tool");
private final String value;
MessageType(String value) {
this.value = value;
}
public static MessageType fromValue(String value) {
for (MessageType messageType : MessageType.values()) {
if (messageType.getValue().equals(value)) {
return messageType;
}
}
throw new IllegalArgumentException("Invalid MessageType value: " + value);
}
public String getValue() {
return this.value;
}
}
可以看到MessageType有很多枚举类型,其中USER是用来指代用户提问的信息,而SYSTEM则是给大模型系统的提示词,这里我们了解即可
分析ChatOptions
再回到Prompt,可以看到有个私有内部类 ChatOptions,让我们跟进去看看
/**
* {@link ModelOptions} representing the common options that are portable across different
* chat models. */public interface ChatOptions extends ModelOptions {
/**
* Returns the model to use for the chat. * @return the model to use for the chat */ @Nullable
String getModel();
/**
* Returns the frequency penalty to use for the chat. * @return the frequency penalty to use for the chat */ @Nullable
Double getFrequencyPenalty();
/**
* Returns the maximum number of tokens to use for the chat. * @return the maximum number of tokens to use for the chat */ @Nullable
Integer getMaxTokens();
/**
* Returns the presence penalty to use for the chat. * @return the presence penalty to use for the chat */ @Nullable
Double getPresencePenalty();
/**
* Returns the stop sequences to use for the chat. * @return the stop sequences to use for the chat */ @Nullable
List<String> getStopSequences();
/**
* Returns the temperature to use for the chat. * @return the temperature to use for the chat */ @Nullable
Double getTemperature();
/**
* Returns the top K to use for the chat. * @return the top K to use for the chat */ @Nullable
Integer getTopK();
/**
* Returns the top P to use for the chat. * @return the top P to use for the chat */ @Nullable
Double getTopP();
/**
* Returns a copy of this {@link ChatOptions}.
* @return a copy of this {@link ChatOptions}
*/ <T extends ChatOptions> T copy();
/**
* Creates a new {@link Builder} to create the default {@link ChatOptions}.
* @return Returns a new {@link Builder}.
*/ static Builder builder() {
return new DefaultChatOptionsBuilder();
}
/**
* Builder for creating {@link ChatOptions} instance.
*/ interface Builder {
/**
* Builds with the model to use for the chat. * @param model
* @return the builder
*/ Builder model(String model);
/**
* Builds with the frequency penalty to use for the chat. * @param frequencyPenalty
* @return the builder.
*/ Builder frequencyPenalty(Double frequencyPenalty);
/**
* Builds with the maximum number of tokens to use for the chat. * @param maxTokens
* @return the builder.
*/ Builder maxTokens(Integer maxTokens);
/**
* Builds with the presence penalty to use for the chat. * @param presencePenalty
* @return the builder.
*/ Builder presencePenalty(Double presencePenalty);
/**
* Builds with the stop sequences to use for the chat. * @param stopSequences
* @return the builder.
*/ Builder stopSequences(List<String> stopSequences);
/**
* Builds with the temperature to use for the chat. * @param temperature
* @return the builder.
*/ Builder temperature(Double temperature);
/**
* Builds with the top K to use for the chat. * @param topK
* @return the builder.
*/ Builder topK(Integer topK);
/**
* Builds with the top P to use for the chat. * @param topP
* @return the builder.
*/ Builder topP(Double topP);
/**
* Build the {@link ChatOptions}.
* @return the Chat options. */ ChatOptions build();
}
}
可以看到这里面有很多getter和setter方法,用于定义各种大模型的参数,比如topK,topP,temperature等等
这些参数是在AI模型生成文本时控制输出特性的重要参数,这里简单讲几个:
-
Temperature(温度):控制生成文本的随机性。较高的温度值(如0.8-1.0)会产生更多样化、创造性的响应,但可能不太准确。较低的温度值(如0.2-0.3)会使输出更加确定和一致,但创造性可能较低。温度为0时,模型总是选择最可能的下一个词。
-
Top-K:限制模型在每一步只考虑概率最高的K个词。例如,如果设置top-k=50,模型只会从概率最高的50个词中选择下一个词,忽略其他所有可能性。
-
Top-P(也称为nucleus sampling):模型只考虑累积概率达到P值的最小词集合。例如,如果top-p=0.9,模型会选择概率总和达到90%的最少数量的词,忽略剩余的低概率词。
这些参数就是我们自定义Prompt的意义
通过自定义Prompt调用ai
那么我们怎么通过创建Prompt传参呢?
ChatResponse response = openAiChatModel.call(new Prompt(
message,
ChatOptions.builder()
.temperature(0.7)
.topK(40)
.build()
));
这是一段代码示例,通过链式编程的方式传入参数,是建造者模式(Builder Pattern)的实际使用
通过yaml注入Prompt属性
其实我们几乎不会去创建Prompt注入,而是使用修改配置的方式
还记得application.yaml这个文件吗?

Service层代码示例
最终跨域进行流式聊天的最简代码实现如下所示
/**
* 流式聊天
*
* @param message 消息
* @return 响应
* Author: Anson
*/
public Flux<String> fluxchat(String message) {
Message usermessage = new UserMessage(message);
Message systemMessage = new SystemMessage(systemResource);
List<Message> messages = new ArrayList<>();
messages.add(systemMessage);
messages.add(usermessage);
// 处理聊天逻辑
return openAiChatModel.stream(String.valueOf(messages));
}
完整的Service层级代码
@Service
public class ChatService {
// 注入OpenAiChatModel
private final OpenAiChatModel openAiChatModel;
@Value("classpath:/prompts/alice-system-message.st")
private Resource systemResource;
public ChatService(OpenAiChatModel openAiChatModel) {
this.openAiChatModel = openAiChatModel;
}
/**
* 流式聊天
*
* @param message 消息
* @return 响应
* Author: Anson
*/
public Flux<String> fluxchat(String message) {
Message usermessage = new UserMessage(message);
Message systemMessage = new SystemMessage(systemResource);
List<Message> messages = new ArrayList<>();
messages.add(systemMessage);
messages.add(usermessage);
// 处理聊天逻辑
return openAiChatModel.stream(String.valueOf(messages));
}
}
我们提供了一个可以接受String数据类型的方法 fluxchat
在方法内部我们创建了两个不同类型的Message,一个是user,一个是system,分别指代用户的提问和大模型的提示词
之后我们使用了一个ArrayList将信息收集起来,一起传给了stream方法,通过String.valueOf(messages) 解析里面的内容,这样就可以把系统提示词和用户问题一起输入进去
这样我们的一个简单的Service就写好了
提示词传递
值得一提的是这里的提示词使用了一个systemResource
@Value("classpath:/prompts/alice-system-message.st")
private Resource systemResource;
这是我创建的一个用于获取提示词的变量
通过@Value注解,systemResource可以获取st(StringTemplate)文件,这样就可以作为提示词使用
2.2-Controller层处理
现在我们需要在Controller层级调用方法
@GetMapping(value = "/flux-alice", produces = "text/html;charset=UTF-8")
@Operation(summary = "聊天接口")
public Flux<String> fluxchat(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {
System.out.println();
Flux<String> fluxchat = chatService.fluxchat(message);
return fluxchat;
}
简单测试一下
可以看到现在已经顺利流式输出了!
(好冷的烂笑话)
3.最终代码
3.1-Service层级
@Service
public class ChatService {
// 注入OpenAiChatModel
private final OpenAiChatModel openAiChatModel;
@Value("classpath:/prompts/alice-system-message.st")
private Resource systemResource;
public ChatService(OpenAiChatModel openAiChatModel) {
this.openAiChatModel = openAiChatModel;
}
/**
* 流式聊天
*
* @param message 消息
* @return 响应
* Author: Anson
*/
public Flux<String> fluxchat(String message) {
Message usermessage = new UserMessage(message);
Message systemMessage = new SystemMessage(systemResource);
List<Message> messages = new ArrayList<>();
messages.add(systemMessage);
messages.add(usermessage);
// 处理聊天逻辑
return openAiChatModel.stream(String.valueOf(messages));
}
}
3.2-Controller层级
@RestController
@RequestMapping("/ai")
@Tag(name = "ai聊天控制器")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping(value = "/flux-alice", produces = "text/html;charset=UTF-8")
@Operation(summary = "聊天接口")
public Flux<String> fluxchat(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {
System.out.println();
Flux<String> fluxchat = chatService.fluxchat(message);
return fluxchat;
}
}
4.ChatClient
这里介绍一下ChatClient
因为ai模型的提供厂商很多,也有不同的规范,为了方便调用,统一接口,Spring AI为我们提供了ChatClient,在ChatClient里面提供了一些统一的写法
-
ChatModel (例如 OpenAiChatModel):
这是与底层 AI 模型(如 OpenAI 的 GPT 模型)进行交互的直接、低级别接口。
它负责将 Prompt 对象发送给 AI 服务,并接收原始的 ChatResponse。
你可以把它看作是与特定 AI 提供商 API 通信的核心组件。 -
ChatClient:
这是一个更高级别、更流畅的 API,构建在 ChatModel 之上。
它旨在简化与 AI 模型的交互,提供更方便的链式调用方法来构建请求(例如 .prompt(), .user(), .system(), .stream(), .call())。
-
封装了常见的模式: 例如,更容易地管理对话历史、设置系统消息、处理用户消息、添加工具/函数调用等。
-
提供附加功能: 如请求/响应的拦截器(Advisors)、默认提示设置、结构化输出转换(将 AI 的响应直接映射到 Java 对象)以及与可观测性(如 Micrometer)的集成。
总结:
OpenAiChatModel 是基础,负责与 OpenAI API 的直接通信。
ChatClient 是一个便利层,它使用 OpenAiChatModel,但提供了更简单、更强大的 API 来构建 AI 交互逻辑,减少了样板代码,并集成了更多高级功能。
下面讲讲怎么用他
4.1-创建需要的Client
要使用Client,需要我们创建一个Client(存在水字数的嫌疑( •̀ ω •́ )
我们需要像使用Model一样配置参数
在进行yaml的正确配置之后,按照上面的章节我们其实可以直接注入Model到service并且使用了
private final OpenAiChatModel openAiChatModel;
public ChatClientService(OpenAiChatModel openAiChatModel) {
this.openAiChatModel = openAiChatModel;
}
而使用Client只需要把这个Model注入到Client里面就可以了(套娃)
ChatClient momoi = ChatClient.create(openAiChatModel);


4.2-使用momoi得到midori(流式输出)
// 直接使用链式编程传入参数
Flux<String> midori = momoi.prompt()
.system(systemResource)
.user(message)
.options(options)
.stream()
.content();
return midori;

你可能会留意到在链式编程的过程中存在一些不同
比如我们没有传入用数组列表实现的messages,而是使用.system
和.user
来传入我们需要的系统提示词和用户信息,而且有个没有见过的选项 .options
这里我们直接跟进去,看看这是个什么东西
<T extends ChatOptions> ChatClientRequestSpec options(T options);
可以看到需要一个继承了ChatOptions类型的参数,我们看看这个ChatOptions是何方神圣
public interface ChatOptions extends ModelOptions {
static Builder builder() {
return new DefaultChatOptionsBuilder();
}
String getModel();
Double getFrequencyPenalty();
Integer getMaxTokens();
Double getPresencePenalty();
List<String> getStopSequences();
Double getTemperature();
Integer getTopK();
Double getTopP();
<T extends ChatOptions> T copy();
interface Builder {
Builder model(String model);
Builder frequencyPenalty(Double frequencyPenalty);
Builder maxTokens(Integer maxTokens);
Builder presencePenalty(Double presencePenalty);
Builder stopSequences(List<String> stopSequences);
Builder temperature(Double temperature);
Builder topK(Integer topK);
Builder topP(Double topP);
ChatOptions build();
}
}
我手动删除了很多注释内容,现在留下了比较干净的类,可以看到这些参数都非常眼熟,实际上这就是我们用来微调模型的一些参数,ChatClient允许我们通过传入options来对模型进行微调
那么我们怎么创建options呢
相当的简单
OpenAiChatOptions options = OpenAiChatOptions.builder()
.temperature(1.0)
.build();
我们只需要使用链式编程构建一个option就可以了
4.3-Service层级示例
现在看一下我们使用Client返回流式输出的代码
@Service
public class ChatClientService {
private final OpenAiChatModel openAiChatModel;
@Value("classpath:/prompts/alice-system-message.st")
private Resource systemResource;
public ChatClientService(OpenAiChatModel openAiChatModel) {
this.openAiChatModel = openAiChatModel;
}
/**
* 使用Momoi进行聊天(直接使用链式编程传入参数)
*
* @param message
* @return
*/ public Flux<String> chatWithOpenAiByMomoi(String message) {
ChatClient momoi = ChatClient.create(openAiChatModel);
OpenAiChatOptions options = OpenAiChatOptions.builder()
.temperature(1.0)
.build();
// 直接使用链式编程传入参数
Flux<String> midori = momoi.prompt()
.system(systemResource)
.user(message)
.options(options)
.stream()
.content();
return midori;
}
}
4.4-Controller层级示例
/**
* 链式编程ChatClient聊天接口
*
* @param message 用户消息
* @return 返回流式聊天结果
*/
@GetMapping(value = "/flux-ChatClient/OpenAi-momoi", produces = "text/html;charset=UTF-8")
@Operation(summary = "链式实现ChatClient聊天接口")
public Flux<String> chatWithOpenAiByChatClientBymomoi(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {
log.info("链式构成ChatClient/OpenAi聊天接口被调用,消息内容:{}", message);
Flux<String> fluxchat = chatClientService.chatWithOpenAiByMomoi(message);
log.info("Momoi调用完成");
return fluxchat;
}
现在我们就已经掌握了配置不同AI的Model,使用SpringBoot自动装配Model需要的参数,使用ChatClient的统一Api发送请求,至于ChatClient的其他好处,接下来我们讲一个Advisor的用法
5.使用Advisor提供临时会话记忆
先看效果

5.1-创建配置类
我们需要创建一个配置类,这里我命名为AiConfig
5.2-第一种写法(create与mutate)
这个配置类的代码如下
@Configuration
public class AiConfig {
// 依然使用注解导入系统提示词
@Value("classpath:/prompts/alice-system-message.st")
private Resource systemResource;
// 创建yuukaChat的同时注入两个参数,OpenAiChatModel和ChatMemory
@Bean
public ChatClient yuukaChat(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {
// mutate不修改调用它的client,而是创建一个新的client
// 所以这里是写到链式表达式里面并且直接返回
return ChatClient.create(openAiChatModel).mutate().defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录
).build();
}
// 创建一个内存类型的记忆
// 官方还提供了CassandraChatMemory,Neo4jChatMemory和JdbcChatMemory
@Bean
public ChatMemory memory() {
return new InMemoryChatMemory();
}
}
Service层级需要注入并调用yuukaChat
注入yuukaChat
public ChatClientService(ChatClient yuukaChat) {
this.yuukaChat = yuukaChat;
}

调用yuukaChat得到noa
public Flux<String> chatWithOpenAiByYuuka(String message) {
// 直接使用链式编程传入参数
Flux<String> noa = yuukaChat.prompt()
.user(message)
.system(systemResource)
.stream()
.content();
return noa;
}

之后调用controller
@GetMapping(value = "/flux-ChatClient/OpenAi-yuuka", produces = "text/html;charset=UTF-8")
@Operation(summary = "链式实现ChatClient聊天接口")
public Flux<String> chatWithOpenAiByYuuka(@RequestParam(value = "message", defaultValue = "给我讲个笑话?") String message) {
log.info("链式构成ChatClient/OpenAi聊天接口被调用,消息内容:{}", message);
Flux<String> fluxchat = chatClientService.chatWithOpenAiByYuuka(message);
log.info("Yuuka调用完成");
return fluxchat;
}
效果实现


但是目前的记忆实现是临时的,我们之后再说持久化的事情,先把临时的讲清楚
5.3-第二种写法(builder构建)
第二种写法的区别主要是在Config类中初始化ChatClient上,调用都是一样的
@Bean
public ChatClient Kei(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem(systemResource)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录
)
.build();
}

可以看到这里调用了builder方法,那么和第一种ChatClient配置时先用了create,后用了mutate有什么区别呢?
5.4-两种方法的教会了我们什么
我们直接跟到create里面看看
也就是说create本质还是builder,那为什么要有mutate呢?
我们注意到mutate可以用来修改已经存在的实例,对其微调并返回一个新的实例
也就是说
return ChatClient.create(openAiChatModel).mutate().defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录
).build();
可以拆分为两部分
如下
@Bean
public ChatClient yuukaChat(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {
// 先用create生成新的client
ChatClient originClient = ChatClient.create(openAiChatModel);
// 再用mutate进行微调并返回新的client
ChatClient finalClient = originClient.mutate().defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录
).build();
最后我们直接返回就可以了
return finalClient;
}
这里就展现了mutate可以微调并生成一个新的client并返回
那么下次如果你希望生成一个经过微调的client就可以使用mutate了!
一般来说,我们还是推荐直接使用builder
毕竟生成一次就能直接用,不需要多生成一个实例
@Bean
public ChatClient Kei(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {
return ChatClient.builder(openAiChatModel)
.defaultSystem(systemResource)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 使用内存聊天记录
)
.build();
}
5.5-简单看一下Memory实现
在这里我们可以看到memory是一个ChatMemory类型的类
我们直接ctrl + 左键跟进去看看
发现ChatMemory是一个接口,提供了增加,获取,清除的方法
咦,看看怎么实现的这些方法,我们直接跳到Impl类里面
因为跳进来阅读是只读代码,所以我复制一份代码出来给大家加个注释
public class InMemoryChatMemory implements ChatMemory {
// 使用ConcurrentHashMap,将对话id作为key,消息列表作为value
Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
// 这里自定义实现了一个put方法 putIfAbsent
/**
如果 Map 中已经存在指定的 key 并且其值不是 null,则此方法不执行任何操作,并返回该 key 当前关联的值。
如果 Map 中不存在指定的 key,或者该 key 存在但其关联的值是 null,则此方法会将指定的 key 和 value 添加到 Map 中,并返回 null(因为之前没有值或值为 null)。
*/
@Override
public void add(String conversationId, List<Message> messages) {
this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
this.conversationHistory.get(conversationId).addAll(messages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> all = this.conversationHistory.get(conversationId);
return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
}
@Override
public void clear(String conversationId) {
this.conversationHistory.remove(conversationId);
}
}
这里补充一下
ConcurrentHashMap:
- 优点:线程安全,采用了分段锁或细粒度锁的设计,允许多个线程同时访问map的不同部分,并发性能好
- 缺点:迭代时不反映最新状态,弱一致性,略微牺牲单线程性能
- 适用场景:需要线程安全的高并发场景
所以简单的来说,Memory通过Map的方式实现了对话id和内容的存储,每次对话都会将上一次的对话一起发送给大模型,以此实现记忆上下文的功能,但是大模型的输入Token终究有限,所以有些公司会使用自己研发的注意力集中机制,专注于最近的消息
6.配置记忆持久化
TODO
评论