使用Java语言来开发的绝大多数技术都是要和SpringBoot做整合的,Langchain4j也不例外,SpringBoot本质上是自动管理应用程序运行过程中所需要的实例
ChatModel

创建SpringBoot应用
使用idea新建Project,先创建一个Project

创建完Project之后然后开始创建我们的应用module

选择spring-web和lombok即可

引入starter依赖
starter依赖是SpringBoot的核心,我们引入langchain4j-open-ai-spring-boot-starter,通过它
- 引入langchain4j的核心依赖
- 自动完成组件的管理
- 可以通过配置文件提供核心信息
引入以下依赖
<!--Langchain4j支持-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.8.0-beta15</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.8.0-beta15</version>
</dependency>
application-yml配置文件
提供配置信息,基于ChatModel的配置使用的前缀是langchain4j.open-ai.chat-model
- 配置的属性值
base-url:API请求基础地址(此处为阿里百炼平台的OpenAI兼容接口地址,用于对接通义千问等模型)api-key:访问API的密钥(通过环境变量${API-KEY}注入,避免硬编码,增强安全性)model-name:要使用的具体模型名称(此处为阿里通义千问的”qwen3-max”模型,需与base-url对应的平台支持的模型匹配)log-requests/log-responses:是否打印日志
对应的配置项如下
# LangChain4j 框架核心配置
langchain4j:
# OpenAI兼容的聊天模型配置(支持所有遵循OpenAI API规范的模型,如阿里通义千问、智谱AI等)
open-ai:
chat-model:
# API请求基础地址(此处为阿里百炼平台的OpenAI兼容接口地址,用于对接通义千问等模型)
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
# 访问API的密钥(通过环境变量${API-KEY}注入,避免硬编码,增强安全性)
api-key: ${API-KEY}
# 要使用的具体模型名称(此处为阿里通义千问的"qwen3-max"模型,需与base-url对应的平台支持的模型匹配)
model-name: qwen3-max
log-requests: true
log-responses: true
Controller开发
开发Controller,并且在其中引入ChatModel
- 先来提供响应的JSON字符串封装对象CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private Object data;
// ok方法(静态方法)
public static CommonResult ok() {
CommonResult commonResult = new CommonResult();
commonResult.code = 200;
commonResult.message = "success";
return commonResult;
}
// ok方法(静态方法) 重载
public static CommonResult ok(Object data) {
CommonResult commonResult = ok();
commonResult.data = data;
return commonResult;
}
}
- 接下来在Controller中注入ChatModel对象(容器会自动注册ChatModel,可以直接建立ChatModel和Controller之间的依赖关系)
@RestController
public class ChatController {
@Autowired
private ChatModel chatModel;
}
- 提供映射以及响应处理
@RestController
public class ChatController {
@Autowired
private ChatModel chatModel;
@RequestMapping("/api/chat/send")
public CommonResult chat(@RequestBody Map bodyMap) {
String userMessage = (String) bodyMap.get("message");
String aiMessage = chatModel.chat(userMessage);
return CommonResult.ok(aiMessage);
}
}
通过Apifox构造请求,发送请求接收响应如下

对应的控制台中的日志如下
2025-10-30T15:55:41.630+08:00 INFO 27768 --- [langchain4j-chatmodel] [nio-8080-exec-1] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen3-max",
"messages" : [ {
"role" : "user",
"content" : "你是谁"
} ],
"stream" : false
}
2025-10-30T15:55:43.758+08:00 INFO 27768 --- [langchain4j-chatmodel] [nio-8080-exec-1] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding], [x-request-id: d3767179-0f5c-4a7a-80ae-430b2ffc5770], [x-dashscope-call-gateway: true], [content-type: application/json], [content-length: 669], [req-cost-time: 1855], [req-arrive-time: 1761810944740], [resp-start-time: 1761810946595], [x-envoy-upstream-service-time: 1854], [set-cookie: acw_tc=d3767179-0f5c-4a7a-80ae-430b2ffc57706d3d84fc490ad3a142c238d5d50edfd9;path=/;HttpOnly;Max-Age=1800], [date: Thu, 30 Oct 2025 07:55:46 GMT], [server: istio-envoy]
- body: {"choices":[{"message":{"content":"我是通义千问,阿里巴巴集团旗下的超大规模语言模型。我能够回答问题、创作文字,比如写故事、写公文、写邮件、写剧本、逻辑推理、编程等等,还能表达观点,玩游戏等。如果你有任何问题或需要帮助,欢迎随时告诉我!","role":"assistant"},"finish_reason":"stop","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":10,"completion_tokens":60,"total_tokens":70,"prompt_tokens_details":{"cached_tokens":0}},"created":1761810947,"system_fingerprint":null,"model":"qwen3-max","id":"chatcmpl-d3767179-0f5c-4a7a-80ae-430b2ffc5770"}
其实也是发起Http请求和处理Http响应
AiServices
回顾一下我们之前使用的AiServices,我们需要提供接口
回顾
代理接口:接口中定义方法名为chat,返回值为String的方法,通过@SystemMessage和@UserMessage提供信息,通过@V提供消息中的值和方法参数之间的对应关系ChatModel:通过ChatModel来创建代理实例
OpenAiChatModel model = OpenAiChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("API-KEY"))
.modelName("qwen3-max")
.logRequests(true)
//.logResponses(true)
.build();
// 调用聊天模型的方法,传入系统消息和用户消息
UserJavaCoder coder = AiServices.create(UserJavaCoder.class, model);
String response = coder.chat("写一个斐波那契数列");
代理接口的代码
public interface UserJavaCoder {
// @V中的值在@UserMessage中使用{{}}的方式来引用
@UserMessage("你是一个专业的Java开发,你只能使用Java语言来写代码,不能使用其他语言。{{message}}")
String chat(@V("message") String userMessage);
// @V中的值在@SystemMessage和@UserMessage中使用{{}}的方式来引用
@SystemMessage("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。")
@UserMessage("{{task}}")
String chat(@V("language") String language, @V("task") String task);
}
SpringBoot整合
SpringBoot的核心就是自动管理组件,前面我们发现SpringBoot已经自动注册了ChatModel实例,那么这时候就有一个问题,能够自动注册AiServices创建的代理实例
// 创建代理实例
UserJavaCoder coder = AiServices.create(UserJavaCoder.class, model);
从以上方法中可以看出来需要两个参数:接口,ChatModel
接下来我们要围绕着接口来做些事情,以下是示意图
在接口上增加@AiService注解对应的就是上面的:2. 获取接口和ChatModel的对应关系
说明:其中并没有直接指定ChatModel,SpringBoot自动获取了容器中的ChatModel实例
@AiService
public interface UserJavaCoder {
// @V中的值在@UserMessage中使用{{}}的方式来引用
@UserMessage("你是一个专业的Java开发,你只能使用Java语言来写代码,不能使用其他语言。{{message}}")
String chat(@V("message") String userMessage);
// @V中的值在@SystemMessage和@UserMessage中使用{{}}的方式来引用
@SystemMessage("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。")
@UserMessage("{{task}}")
String chat(@V("language") String language, @V("task") String task);
}
Controller开发
直接获取容器中的UserJavaCoder组件实例,然后调用chat方法来进行交互

@RestController
public class AiServiceChatController {
@Autowired
private UserJavaCoder userJavaCoder;
@RequestMapping("/api/aiservice/send")
public CommonResult chat(@RequestBody Map bodyMap) {
String userMessage = (String) bodyMap.get("message");
String aiMessage = userJavaCoder.chat(userMessage);
return CommonResult.ok(aiMessage);
}
}
通过Apifox构造请求,发送请求接收响应如下

对应的控制台日志如下
2025-10-30T17:47:11.095+08:00 INFO 28832 --- [langchain4j-chatmodel] [nio-8080-exec-1] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen3-max",
"messages" : [ {
"role" : "user",
"content" : "你是一个专业的Java开发,你只能使用Java语言来写代码,不能使用其他语言。实现一个斐波那契数列"
} ],
"stream" : false
}
2025-10-30T17:47:30.384+08:00 INFO 28832 --- [langchain4j-chatmodel] [nio-8080-exec-1] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding], [x-request-id: 365b8a26-47d7-4f3c-8efc-904d6fdf6b58], [x-dashscope-call-gateway: true], [content-type: application/json], [content-length: 3536], [req-cost-time: 19100], [req-arrive-time: 1761817634017], [resp-start-time: 1761817653117], [x-envoy-upstream-service-time: 19099], [set-cookie: acw_tc=365b8a26-47d7-4f3c-8efc-904d6fdf6b58b1b590af29d69d56c7ab7853e1d3eaa5;path=/;HttpOnly;Max-Age=1800], [date: Thu, 30 Oct 2025 09:47:32 GMT], [server: istio-envoy]
- body: {"choices":[{"message":{"content":"```java\npublic class Fibonacci {\n \n /**\n * 使用递归方式计算斐波那契数列的第n项\n * 时间复杂度: O(2^n)\n * 空间复杂度: O(n)\n */\n public static long fibonacciRecursive(int n) {\n if (n < 0) {\n throw new IllegalArgumentException(\"n must be non-negative\");\n }\n if (n <= 1) {\n return n;\n }\n return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);\n }\n \n /**\n * 使用动态规划(迭代)方式计算斐波那契数列的第n项\n * 时间复杂度: O(n)\n * 空间复杂度: O(1)\n */\n public static long fibonacciIterative(int n) {\n if (n < 0) {\n throw new IllegalArgumentException(\"n must be non-negative\");\n }\n if (n <= 1) {\n return n;\n }\n \n long prev2 = 0; // F(0)\n long prev1 = 1; // F(1)\n long current = 0;\n \n for (int i = 2; i <= n; i++) {\n current = prev1 + prev2;\n prev2 = prev1;\n prev1 = current;\n }\n \n return current;\n }\n \n /**\n * 使用数组存储方式计算斐波那契数列的前n+1项\n * 时间复杂度: O(n)\n * 空间复杂度: O(n)\n */\n public static long[] fibonacciArray(int n) {\n if (n < 0) {\n throw new IllegalArgumentException(\"n must be non-negative\");\n }\n \n long[] fib = new long[n + 1];\n if (n >= 0) {\n fib[0] = 0;\n }\n if (n >= 1) {\n fib[1] = 1;\n }\n \n for (int i = 2; i <= n; i++) {\n fib[i] = fib[i - 1] + fib[i - 2];\n }\n \n return fib;\n }\n \n /**\n * 打印斐波那契数列的前n项\n */\n public static void printFibonacci(int n) {\n if (n <= 0) {\n System.out.println(\"No terms to print\");\n return;\n }\n \n System.out.print(\"Fibonacci sequence (first \" + n + \" terms): \");\n for (int i = 0; i < n; i++) {\n System.out.print(fibonacciIterative(i));\n if (i < n - 1) {\n System.out.print(\", \");\n }\n }\n System.out.println();\n }\n \n // 测试方法\n public static void main(String[] args) {\n int n = 10;\n \n // 测试递归方法\n System.out.println(\"Recursive approach:\");\n for (int i = 0; i <= n; i++) {\n System.out.println(\"F(\" + i + \") = \" + fibonacciRecursive(i));\n }\n \n System.out.println(\"\\nIterative approach:\");\n for (int i = 0; i <= n; i++) {\n System.out.println(\"F(\" + i + \") = \" + fibonacciIterative(i));\n }\n \n System.out.println(\"\\nArray approach:\");\n long[] fibArray = fibonacciArray(n);\n for (int i = 0; i <= n; i++) {\n System.out.println(\"F(\" + i + \") = \" + fibArray[i]);\n }\n \n System.out.println();\n printFibonacci(15);\n }\n}\n```","role":"assistant"},"finish_reason":"stop","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":36,"completion_tokens":806,"total_tokens":842,"prompt_tokens_details":{"cached_tokens":0}},"created":1761817653,"system_fingerprint":null,"model":"qwen3-max","id":"chatcmpl-365b8a26-47d7-4f3c-8efc-904d6fdf6b58"}
指定ChatModel(了解)
@AiService注解也可以指定使用的ChatModel,方便进行灵活的切换
wiringMode: 注入方式。- 默认值为AiServiceWiringMode.AUTOMATIC,代表自动注入;
- AiServiceWiringMode.EXPLICIT,代表手动指定
chatModel:提供容器中的chatModel的组件名称。显式指定chatModel,当前这个值为SpringBoot自动注册的ChatModel的组件名称
@AiService(
// 默认值为AiServiceWiringMode.AUTOMATIC,代表自动注入
wiringMode = AiServiceWiringMode.EXPLICIT,
// 显式指定chatModel,当前这个值为SpringBoot自动注册的ChatModel的组件名称
chatModel = "openAiChatModel"
)
public interface UserJavaCoder {
// @V中的值在@UserMessage中使用{{}}的方式来引用
@UserMessage("你是一个专业的Java开发,你只能使用Java语言来写代码,不能使用其他语言。{{message}}")
String chat(@V("message") String userMessage);
// @V中的值在@SystemMessage和@UserMessage中使用{{}}的方式来引用
@SystemMessage("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。")
@UserMessage("{{task}}")
String chat(@V("language") String language, @V("task") String task);
}
了解:自动注册的组件来源于其自动配置类的这里
@Bean @ConditionalOnProperty(PREFIX + ".chat-model.api-key") OpenAiChatModel openAiChatModel( // 其中这个方法名就是默认的组件id(名称) @Qualifier(CHAT_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder, Properties properties, ObjectProvider<ChatModelListener> listeners ) { ChatModelProperties chatModelProperties = properties.chatModel(); return OpenAiChatModel.builder() ... .build(); }
流式(Stream)调用
我们前面的其实是阻塞式调用,结果是一次性响应,接下来我们来进行流式调用,结果是分次来进行相应的。
比如大家访问DeepSeek – 探索未至之境,通义 – 你的超级个人助理等过程时,回复的消息是类似于打字机效果的,这里的结果其实就是流式响应的。

引入依赖
在pom.xml中引入依赖
<!--langchain4j-流式调用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.8.0-beta15</version>
</dependency>
application-yml配置文件
其实就是把前面的配置文件中的chat-model改为streaming-chat-model
# LangChain4j 框架核心配置
langchain4j:
# OpenAI兼容的聊天模型配置(支持所有遵循OpenAI API规范的模型,如阿里通义千问、智谱AI等)
open-ai:
# chat-model改为stream-chat-model
streaming-chat-model:
# API请求基础地址(此处为阿里百炼平台的OpenAI兼容接口地址,用于对接通义千问等模型)
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
# 访问API的密钥(通过环境变量${API-KEY}注入,避免硬编码,增强安全性)
api-key: ${API-KEY}
# 要使用的具体模型名称(此处为阿里通义千问的"qwen3-max"模型,需与base-url对应的平台支持的模型匹配)
model-name: qwen3-max
log-requests: true
log-responses: true
引入配置文件中的对应配置,SpringBoot会自动向容器中注册组件openaiStreamChatModel
了解:自动注册的组件来源于其自动配置类的这里
@Bean @ConditionalOnProperty(PREFIX + ".streaming-chat-model.api-key") OpenAiStreamingChatModel openAiStreamingChatModel( // 该方法名为默认的组件id @Qualifier(STREAMING_CHAT_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder, Properties properties, ObjectProvider<ChatModelListener> listeners ) { ChatModelProperties chatModelProperties = properties.streamingChatModel(); return OpenAiStreamingChatModel.builder() ... .build(); }
代理接口
对应提供的接口变为如下的接口
@AiService(
// 默认值为AiServiceWiringMode.AUTOMATIC,代表自动注入
wiringMode = AiServiceWiringMode.EXPLICIT,
// 显式指定openAiStreamingChatModel,当前这个值为SpringBoot自动注册的ChatModel的组件名称
streamingChatModel = "openAiStreamingChatModel"
)
public interface StreamUserJavaCoder {
// @V中的值在@UserMessage中使用{{}}的方式来引用
@UserMessage("你是一个专业的Java开发,你只能使用Java语言来写代码,不能使用其他语言。{{message}}")
Flux<String> chat(@V("message") String userMessage);
// @V中的值在@SystemMessage和@UserMessage中使用{{}}的方式来引用
@SystemMessage("你是一个专业的{{language}}开发,你只能使用{{language}}语言来写代码,不能使用其他语言。")
@UserMessage("{{task}}")
Flux<String> chat(@V("language") String language, @V("task") String task);
}
其中主要修改了如下内容
- @AiService中的
chatModel变更为streamingChatModel属性,对应的值也做了修改 - chat方法的返回值变更为
Flux<String>
Controller
直接获取容器中的UserJavaCoder组件实例,然后调用chat方法来进行交互
@RestController
public class StreamingChatController {
@Autowired
private StreamUserJavaCoder streamUserJavaCoder;
// produces应该写"text/event-stream;charset=UTF-8"
// 返回值为Flux<String>,代表流式返回
@RequestMapping(value = "/api/streamingchat/send",produces = "text/event-stream;charset=utf-8")
public Flux<String> chat(@RequestBody Map bodyMap) {
String userMessage = (String) bodyMap.get("message");
Flux<String> aiMessage = streamUserJavaCoder.chat("Java",userMessage);
return aiMessage;
}
}
这里来解释一下这个方法的参数,因为我们解析的是请求体中的JSON字符串,所以我们在这里使用的是@RequestBody注解
{"message":"你是谁","streaming":true}
会话记忆
接下来我们来完成会话记忆,在前面的会话记忆案例中,我们需要在创建接口代理对象的过程中提供ChatMemory实例,就是如下代码
// 通过AiServices创建代理对象
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemory(chatMemory)
.build();
在SpringBoot整合会话记忆的过程中,我们仍然要做如下的工作
以下是配置示意图(这里我们把代理实例改了名字,开始改为了ExamPreparationAssistant,也就是考试助手)

创建配置类
首先创建一个配置类,配置类上有@Configuration注解(注意要在Application启动类的包目录下或其子包下)
比如启动类的全限定类名为com.cskaoyan.langchain4j.Application,对应的包名就为com.cskaoyan.langchain4j,那么启动类就要在这个包目录或其子包下
package com.cskaoyan.langchain4j.config;
@Configuration
public class Langchain4jConfiguration {
}
上面这段代码的包目录为com.cskaoyan.langchain4j.config,就是其子包,满足我们的要求
注册ChatMemory组件
在配置类中使用@Bean对应的方法来注册组件
@Configuration
public class Langchain4jConfiguration {
// 组件的名称默认为方法名,后面创建的代理组件中的chatMemory属性使用的值和这里保持一致
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20)//最大保存的会话记录数量
.build();
}
}
这里对应的就是上图中的这个部分

代理接口配置
在代理接口中的@AiServices中增加chatMemory属性,对应的值要和前面组件注册中的方法名一致
这里我们提供的代理不再是编程助手了,而是备考助手了
@AiService(
// 默认值为AiServiceWiringMode.AUTOMATIC,代表自动注入
wiringMode = AiServiceWiringMode.EXPLICIT,
// 显式指定openAiStreamingChatModel,当前这个值为SpringBoot自动注册的ChatModel的组件名称
streamingChatModel = "openAiStreamingChatModel",
chatMemory = "chatMemory"
)
public interface ExamPreparationAssistant {
@SystemMessage("你是一个专业的408考研备考助手,用来帮助考生备考408。")
@UserMessage("{{task}}")
Flux<String> chat(@V("task") String task);
}
以上代码的第6行就是指定了chatMemory,也就是上图中的这个部分

Controller
在Controller中引入ExamPreparationAssistant,并且执行其chat方法
@RestController
public class MemoryChatController {
@Autowired
private ExamPreparationAssistant examPreparationAssistant;
// produces应该写"text/event-stream;charset=UTF-8"
// 返回值为Flux<String>,代表流式返回
@RequestMapping(value = "/api/streamingchat/send",produces = "text/event-stream;charset=utf-8")
public Flux<String> chat(@RequestBody Map bodyMap) {
String userMessage = (String) bodyMap.get("message");
Flux<String> aiMessage = examPreparationAssistant.chat(userMessage);
return aiMessage;
}
}
当我们发送信息,我要考北京大学这样的一段信息后,对应的请求如下
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个专业的408考研备考助手,用来帮助考生备考408。"
}, {
"role" : "user",
"content" : "我要考北京大学"
} ],
"stream" : true,
"stream_options" : {
"include_usage" : true
}
}
对应的回复效果如下

再继续提问最近几年的分数线如何
对应的请求如下
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个专业的408考研备考助手,用来帮助考生备考408。"
}, {
"role" : "user",
"content" : "我要考北京大学"
}, {
"role" : "assistant",
"content" : "太棒了!考北京大学是很多学子的梦想,尤其是作为顶尖学府,北大的408计算机学科专业基础综合考试竞争非常激烈。不过只要你科学规划、踏实复习,完全有机会实现目标!我来帮你系统地准备408考研。\n\n📌 首先确认你的基本信息(方便我个性化建议):\n1. 你现在是大几?/ 是否在职备考?\n2. 数学和英语基础如何?\n3. 是否已经确定报考北京大学的哪个学院?比如:\n - 计算机科学技术系(通常考408)\n - 软件与微电子学院(部分方向也考408)\n - 王选所 或 其他?\n\n---\n\n📘 关于408统考科目(满分150×4=600分中的4门专业课):\n\n| 科目 | 分值 | 复习重点 |\n|------|------|----------|\n| 数据结构 | 45分 | 线性表、栈队列、树(二叉树、平衡树、哈夫曼)、图、查找、排序(尤其手写快排堆排) |\n| 计算机组成原理 | 45分 | 数据表示、Cache、主存、虚拟内存、指令系统、CPU控制、流水线 |\n| 操作系统 | 35分 | 进程线程、调度算法、同步互斥(PV操作)、死锁、内存管理、文件系统 |\n| 计算机网络 | 25分 | OSI/TCP/IP模型、IP协议、TCP/UDP、HTTP、DNS、路由算法 |\n\n---\n\n🎯 北京大学计算机相关专业考研特点:\n\n- **分数线高**:近年复试线常在370+甚至390+(政治60+,英语60+,数学110+,专业课110+)\n- **重视专业课深度**:408题目偏难,尤其数据结构和计组常有灵活题\n- **看重项目/竞赛背景**:初试过线后,复试很看重科研潜力(如ACM、大创、论文等)\n\n---\n\n📅 推荐全年复习规划(假设你现在开始备考):\n\n### 📆 第一阶段:基础夯实(现在 – 6月)\n- 使用教材精学四门课:\n - 《数据结构》严蔚敏(配合王道笔记)\n - 《计算机组成与设计》唐朔飞 or 王道书\n - 《操作系统》汤小丹 + 王道\n - 《计算机网络》谢希仁 + 王道\n- 每天安排2门课交替学习,做课后题\n- 英语背单词(推荐墨墨背单词),每天1小时\n\n### 📆 第二阶段:强化突破(7月 – 9月)\n- 主攻王道408全套复习书,至少刷完第一遍\n- 开始做真题分类题库(按知识点刷)\n- 数学进入强化阶段(张宇/李永乐)\n- 政治可从9月开始(徐涛强化班)\n\n### 📆 第三阶段:真题实战(10月 – 11月)\n- 历年408真题逐套训练(2009–2024),限时模拟\n- 整理错题本,查漏补缺\n- 参加模考,提升应试节奏\n\n### 📆 第四阶段:冲刺复盘(12月)\n- 回顾错题、公式、易混概念\n- 背诵政治大题(肖四)\n- 调整作息,保持状态\n\n---\n\n💡 提升竞争力的小建议:\n1. **动手写代码**:用C/C++实现常见数据结构(链表、堆、图算法),这对复试很有帮助。\n2. **参加力扣周赛**:锻炼算法思维,北大复试可能问LeetCode中等难度题。\n3. **了解导师研究方向**:提前看北大计算机学院官网,关注AI、系统、网络安全等方向。\n4. **准备简历和自我介绍**:突出课程设计、编程经历、获奖情况。\n\n---\n\n✅ 下一步我可以帮你:\n- 制定详细的每周学习计划表\n- 推荐高性价比资料组合(省钱省时)\n- 解答408各科疑难问题(随时提问)\n- 分析历年真题考点分布\n- 模拟面试训练(中英文自我介绍+专业问答)\n\n告诉我你目前的进度,咱们一起定制属于你的“上岸计划”!\n\n加油!**燕园等你**!🌸📚"
}, {
"role" : "user",
"content" : "最近几年的分数线如何"
} ],
"stream" : true,
"stream_options" : {
"include_usage" : true
}
}
这里其实是把前面的信息和回复一起封装到messages中,然后再次发给大模型
对应的回复效果

从这里可以看出,我们的第二个问题最近几年的分数线如何并没有提到北京大学,但是回答的答案仍然是围绕北京大学展开的
这其实就是会话记忆的基本实现
会话记忆隔离
当前实现的会话记忆存在一个关键问题:未做到用户级别的会话隔离。由于所有访问程序的用户共享同一个会话记忆对象,导致不同用户的会话记录相互混淆,无法准确区分并维护各用户独立的会话上下文。
我们打开另一个浏览器问问题或另外一个用户问问题,仍然会把前面的问答给带上,不管是谁发的消息,都会堆在这一个盒子里,根本分不清哪个记录是哪个用户的,相当于大家共用一个笔记本,写出来的东西全混在一起了。
实现用户级会话隔离,核心就一个逻辑:给每个用户分配一个专属的 “记忆盒子”,谁的会话谁用自己的盒子,互不干扰。
引入ChatMemoryProvider
这里来提供两个会话,如下图所示

这里的两个会话如果要保存各自的记忆的话,其实需要两个容器对象,用来容纳各自的历史聊天记录
那其实也意味着整个应用中应该有多个容器对象,多个容器对象彼此相互独立,管理各自的聊天记忆

每个聊天都需要各自独立的ChatMemory对象,这时候如果我们自己来管理那么就非常复杂了,这时候LangChain4j中提供了一个接口ChatMemoryProvider
在 langchain4j 中,ChatMemoryProvider 和 ChatMemory 是 “工厂与产品” 的关系,核心目的是帮你优雅实现「用户级会话隔离」—— 刚好对应你之前遇到的 “所有用户共用一个记忆盒子” 的问题,两者分工明确、协作解决会话隔离需求。
先搞懂:各自到底是干啥的?
用之前 “记忆盒子” 的通俗比喻延伸:
- ChatMemory:单个用户的 “专属记忆盒子”
- 本质:存储单个用户会话记录的容器(比如用户 A 的聊天记录、上下文),是 “产品”。
- 核心职责:只管自己这个盒子的 “增删改查”—— 比如添加新消息、获取历史记录、清理过期记忆等。
- ChatMemoryProvider:“记忆盒子工厂”
- 本质:负责创建 / 获取用户专属 ChatMemory 的工具(工厂),是 “生产者”。
- 核心职责:根据「用户唯一标识(比如 user_id)」,为每个用户分配 / 复用一个专属的 ChatMemory 实例,避免不同用户共用同一个 “盒子”。
协作流程(关键)
用户发消息时,整个流程是:
- 后端拿到用户的「唯一 ID」(比如登录后的 user_id、游客的临时 ID);
- 你调用
ChatMemoryProvider的get(用户ID)方法,让工厂 “找盒子”; - 工厂逻辑:
- 如果该用户是第一次来,就新建一个 ChatMemory(新盒子),并和用户 ID 绑定;
- 如果用户之前来过,就直接返回之前给该用户创建的 ChatMemory(复用盒子);
- 后端用这个专属的 ChatMemory,添加用户的新消息、获取历史记录,和大模型交互;
- 不同用户的 ChatMemory 完全独立,数据互不干扰。
从以上流程中我们可以得到一个核心的结论:
ChatMemoryProvider全面接管了ChatMemory,我们只需要提供memoryId即可
这时候我们需要管理的就是代理对象和ChatMemoryProvider对象之间的关系即可,由ChatMemoryProvider来帮我们管理ChatMemory
这时候我们的流程就变为

配置
ChatMemoryProvider组件注册- 进入到配置类中,注册组件:方法的返回值为向容器中注册的组件,默认的组件id为方法名
- “`java // 组件的名称默认为方法名,后面创建的代理组件中的chatMemoryProvider属性使用的值和这里保持一致 @Bean public ChatMemoryProvider chatMemoryProvider() { // 这里也可以改造为lambda表达式 ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() { @Override public ChatMemory get(Object memoryId) { return MessageWindowChatMemory.builder() .id(memoryId)//id值 .maxMessages(20)//最大会话记录数量 .build(); } }; return chatMemoryProvider; }
- 其中`ChatMemoryProvider`为接口,通过匿名内部类的形式完成其内部定义,并且创建其实例并返回
- ```java
@FunctionalInterface
public interface ChatMemoryProvider {
ChatMemory get(Object var1);
}
代理接口配置- 代理接口中指定Provider
- “`java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, streamingChatModel = “openAiStreamingChatModel”, chatMemoryProvider = “chatMemoryProvider” // 指定chatMemoryProvider组件的名称 ) public interface ExamPreparationAssistantWithMemory { @SystemMessage(“你是一个专业的408考研备考助手,用来帮助考生备考408。”) @UserMessage(“{{task}}”) Flux chat(@V(“task”) String task); }
- **`chat方法`**增加memoryId参数,同时增加**`@MemoryId注解`**
- 通过提供这个id其实就是让代理对象获取指定id的ChatMemory;如果没有的话,通过`chatMemoryProvider`调用get方法获取
- ```java
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
streamingChatModel = "openAiStreamingChatModel",
chatMemoryProvider = "chatMemoryProvider" // 指定chatMemoryProvider组件的名称
)
public interface ExamPreparationAssistantWithMemory {
@SystemMessage("你是一个专业的408考研备考助手,用来帮助考生备考408。")
@UserMessage("{{task}}")
Flux<String> chat(@MemoryId String memoryId, @V("task") String task);
}
Controller
在Controller中引入ExamPreparationAssistantWithMemory,在请求参数中传入(前端已经开发好)并获取memoryId即可
@RestController
public class MemoryChatController {
@Autowired
private ExamPreparationAssistantWithMemory assistant;
// 请求的body中包含message和memoryId两个字段
// {"message":"操作系统","streaming":true,"memoryId":"mem_zczzgp7ss_mhyje612"}
@RequestMapping(value = "/api/streamingchat/send",produces = "text/event-stream;charset=utf-8")
public Flux<String> chat(@RequestBody Map bodyMap) {
String userMessage = (String) bodyMap.get("message");
// 获取memoryId
String memoryId = (String) bodyMap.get("memoryId");
// 调用assistant的chat方法,传入memoryId和userMessage
Flux<String> aiMessage = assistant.chat(memoryId, userMessage);
return aiMessage;
}
}
对应的两次请求,请求体的参数分别为
{"message":"你是谁","streaming":true,"memoryId":"mem_zczzgp7ss_mhyje612"}
{"message":"操作系统","streaming":true,"memoryId":"mem_zczzgp7ss_mhyje612"}
因为提供的相同的memoryId,所以可以正常的获取上下文,先来看一下对应的效果

然后再来看一下两次发给大模型的请求信息
第一次
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个专业的408考研备考助手,用来帮助考生备考408。"
}, {
"role" : "user",
"content" : "你是谁"
} ],
"stream" : true,
"stream_options" : {
"include_usage" : true
}
}
第二次
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...f7], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个专业的408考研备考助手,用来帮助考生备考408。"
}, {
"role" : "user",
"content" : "你是谁"
}, {
"role" : "assistant",
"content" : "你好!我是你的408考研备考助手 👩🎓 \n专门为你解答计算机专业基础综合(404、408)相关的问题,包括:\n\n- 数据结构 🌲 \n- 计算机组成原理 🖥️ \n- 操作系统 🧠 \n- 计算机网络 🌐 \n\n我可以帮你: \n✅ 理解知识点 \n✅ 解题思路与技巧 \n✅ 真题解析 \n✅ 学习规划建议 \n\n无论你是刚开始复习,还是冲刺阶段,我都在这里陪你一起上岸 💪 \n你现在想了解哪部分内容呢?😊"
}, {
"role" : "user",
"content" : "操作系统"
} ],
"stream" : true,
"stream_options" : {
"include_usage" : true
}
}
从上面发给大模型的请求来看,本质上就是ChatMemory帮我们管理了会话的历史,然后将对应的消息整合后发给大模型
会话记忆持久化
我们已通过聊天记忆功能实现了对话上下文的实时关联,让交互能够承接历史信息、变得更具连贯性和针对性,显著提升了用户对话体验。但当前的聊天记忆仅停留在内存层面,缺乏持久化机制 —— 当对话会话中断、应用重启或用户跨场景访问时,历史对话上下文会随之丢失,不仅会导致用户需要重复输入关键信息,也无法实现长期对话逻辑的延续。为解决这一问题,我们需要为聊天记忆补充持久化能力。
通过集成合适的持久化方案(如数据库存储、文件存储、缓存存储等),将历史对话记忆安全、可靠地存储起来,既能保障会话的跨会话、跨设备连续性,又能为后续的对话数据分析、用户意图回溯等场景提供支撑,让对话系统的实用性和稳定性更进一步。
在前面我们实现聊天记忆的实例MessageWindowChatMemory中,有一个成员变量ChatMemoryStore,ChatMemoryStore 作为 MessageWindowChatMemory 的核心存储依赖,承担着历史消息的存储与读取职责,是上下文关联能力的基础载体。
也就是说会话的记忆和ChatMemoryStore直接关联
MessageWindowChatMemory
我们通过代码来看一下MessageWindowChatMemory和ChatMemory之间的关系
public class MessageWindowChatMemory implements ChatMemory {
private final Object id;
private final Integer maxMessages;
private final ChatMemoryStore store; // 用于存储会话记录
// ...省略部分方法
public void add(ChatMessage message) {
// ... 省略部分实现
// 更新会话记录
this.store.updateMessages(this.id, messages);
}
// 获取会话记录
public List<ChatMessage> messages() {
List<ChatMessage> messages = new LinkedList(this.store.getMessages(this.id));
ensureCapacity(messages, this.maxMessages);
return messages;
}
// 当历史消息列表长度超过 maxMessages(最大允许消息数)时,通过 “裁剪最旧消息” 的方式压缩列表,同时兼顾上下文连贯性(比如保护关键系统消息、清理工具调用相关的关联消息),确保消息列表始终在设定的窗口大小内。
private static void ensureCapacity(List<ChatMessage> messages, int maxMessages) {
}
// 清除会话记录
public void clear() {
this.store.deleteMessages(this.id);
}
}
同样可以看出来,会话记忆是由ChatMemoryStore来进行管理的
ChatMemoryStore接口
这里的ChatMemoryStore是一个接口
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object var1);
void updateMessages(Object var1, List<ChatMessage> var2);
void deleteMessages(Object var1);
}
该接口对应的实现类如下

我们前面的会话就是默认使用的就是SingleSlotChatMemoryStore,它做的事情就是将会话记录存在内存中
另外一个InMemoryChatMemoryStore也是将会话记录存在内存中,这里我们就不展开了。
那么如果需要持久化存储,就需要我们自己来提供ChatMemoryStore的实现类,并且通过注册组件的方式提供给ChatMemoryProvider

ChatMemoryStore实现类
接下来提供ChatMemoryStore的实现类,并且将其实例在配置类中的方法中返回,作为容器中的组件
这里的实现类中具体的实现,其实就是我们将会话记录通过何种方式保存
- 如果使用的是Redis的实现,那么就是使用Redis进行持久化
- 如果使用的是MySQL的实现,那么就是使用MySQL进行持久化
| 实现类 | 存储类型 | 会话隔离 | 持久化 | 外部依赖 | 核心特点 | 适用场景 |
|---|---|---|---|---|---|---|
| RedisChatMemoryStore | 分布式缓存 | 支持(sessionId) | 是 | Redis 服务 + jedis 依赖 | 高性能、分布式共享,支持过期时间配置 | 生产环境、多服务实例、高并发对话系统 |
| JdbcChatMemoryStore | 关系型数据库 | 支持(sessionId) | 是 | 数据库(MySQL/PG)+ JDBC 驱动 | 适配已有数据库,数据可追溯、备份 | 生产环境、需和业务数据联动、需事务支持的场景 |
| MongoDbChatMemoryStore | 文档型数据库 | 支持(sessionId) | 是 | MongoDB 服务 + 依赖 | 文档结构灵活,无需复杂表设计 | 生产环境、消息结构多变、需分布式部署的场景 |
| FileChatMemoryStore | 本地文件 | 支持(sessionId) | 是 | 无(langchain4j-core 自带) | 单文件 / 多文件存储,本地持久化 | 本地 Demo、单机生产环境、无需数据库的场景 |
这里我们通过Redis来做持久化
基于Redis的ChatMemoryStore
首先引入Redis对于SpringBoot支持的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
实现并注册组件
当前完成的是流程中的这个部分

实现该接口并且注册为容器中的组件,其中使用到了RedisTemplate的相关API参考上面
getMessages- 传入sessionId也就是key从Redis中获取序列化后的JSON字符串
- 将JSON字符串转换为对应的List\
updateMessages- 传入sessionId作为key,List\转换为JSON字符串后作为value
- 然后在Redis中更新,同时设置过期时间
deleteMessages- 传入sessionId作为key,删除Redis中的数据
@Component
public class StringRedisTemplateMemoryStore implements ChatMemoryStore {
// Redis Key前缀(可通过配置文件覆盖,默认值兜底)
@Value("${langchain4j.chat.memory.redis-key-prefix:langchain4j:chat:memory:}")
private String redisKeyPrefix;
// 会话过期时间(天,可通过配置文件覆盖)
@Value("${langchain4j.chat.memory.session-expire-days:7}")
private long sessionExpireDays;
// 注入配置好String序列化的RedisTemplate(Key/Value均为String类型)
private final RedisTemplate<String, String> redisTemplate;
// 构造器注入RedisTemplate
public StringRedisTemplateMemoryStore(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public List<ChatMessage> getMessages(Object session) {
// 校验会话标识非空
Assert.notNull(session, "会话标识session不能为null");
// 构建Redis完整Key(前缀 + 会话标识字符串)
String redisKey = buildRedisKey(session);
// 从Redis获取JSON字符串
String messagesJson = redisTemplate.opsForValue().get(redisKey);
// 无数据返回空列表(避免上层空指针)
if (messagesJson == null || messagesJson.isBlank()) {
return new ArrayList<>();
}
try {
// 反序列化为ChatMessage列表(langchain4j原生方法,兼容所有消息子类)
return ChatMessageDeserializer.messagesFromJson(messagesJson);
} catch (Exception e) {
throw new RuntimeException("Redis会话消息反序列化失败,session: " + session, e);
}
}
@Override
public void updateMessages(Object session, List<ChatMessage> messages) {
// 校验会话标识非空
Assert.notNull(session, "会话标识session不能为null");
// 处理空消息列表(避免序列化异常)
List<ChatMessage> targetMessages = messages == null ? new ArrayList<>() : messages;
// 构建Redis完整Key
String redisKey = buildRedisKey(session);
try {
// 序列化消息列表为JSON字符串(langchain4j原生方法,保留消息类型元信息)
String messagesJson = ChatMessageSerializer.messagesToJson(targetMessages);
// 覆盖式存入Redis,并设置过期时间
redisTemplate.opsForValue().set(redisKey, messagesJson);
redisTemplate.expire(redisKey, sessionExpireDays, TimeUnit.DAYS);
} catch (Exception e) {
throw new RuntimeException("Redis会话消息序列化失败,session: " + session, e);
}
}
@Override
public void deleteMessages(Object session) {
// 校验会话标识非空
Assert.notNull(session, "会话标识session不能为null");
// 构建Redis完整Key并删除
String redisKey = buildRedisKey(session);
redisTemplate.delete(redisKey);
}
/**
* 辅助方法:构建Redis Key(前缀 + 会话标识字符串)
* 支持String、Long、Integer等Object类型的会话标识
*/
private String buildRedisKey(Object session) {
// 安全转换Object为String,避免类型转换异常
String sessionStr = String.valueOf(session);
return redisKeyPrefix + sessionStr;
}
}
配置(组件之间的依赖关系)
对应的就是如下的流程

前面我们已经将ChatMemoryStore开发基于Redis的实现类,并且通过@Component注解注册为容器中的组件
但是并没有人使用到它,接下来我们来配置和ChatMemoryProvider之间的关系
// 组件的名称默认为方法名,后面创建的代理组件中的chatMemoryProvider属性使用的值和这里保持一致
@Bean
public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore chatMemoryStore) {
// 这里也可以改造为lambda表达式
ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
return MessageWindowChatMemory.builder()
.id(memoryId)//id值
.maxMessages(20)//最大会话记录数量
// 设置为自定义的ChatMemoryStore(这个就是我们基于Redis实现的)
.chatMemoryStore(chatMemoryStore)
.build();
}
};
return chatMemoryProvider;
}
解释一下,这里的方法中的参数为ChatMemoryStore chatMemoryStore,通过@Bean注册组件,对应的方法的参数意味着默认按照类型(下面说明)从容器中获取组件
按照类型从容器获取组件:当容器中某个类型(使用类或接口都行)的组件在容器中只有一个,可以直接按照类型从容器中获取,如果这个类型的组件在容器中不止一个,是不能直接从容器中取出,会抛出异常(NoUniqueBeanDefinitionException),这时候可以使用@Qualifier(“名称或id”)指定组件的名称
实现效果
接下来不同的聊天就会在Redis中进行保存对应的会话记录

以上的聊天记录就会在Redis中进行保存,以JSON字符串的形式保存

以下是其他的会话的

