Langchain4j-SpringBoot

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

ChatModel

image-20251031095354274

创建SpringBoot应用

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

image-20251030142623308

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

image-20251030144402251

选择spring-weblombok即可

image-20251030144947634

引入starter依赖

starter依赖是SpringBoot的核心,我们引入langchain4j-open-ai-spring-boot-starter,通过它

  1. 引入langchain4j的核心依赖
  2. 自动完成组件的管理
  3. 可以通过配置文件提供核心信息

引入以下依赖

<!--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构造请求,发送请求接收响应如下

image-20251030155723913

对应的控制台中的日志如下

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

接下来我们要围绕着接口来做些事情,以下是示意图image-20251030163520409

在接口上增加@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方法来进行交互

image-20251031115033803
@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构造请求,发送请求接收响应如下

image-20251030174923538

对应的控制台日志如下

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 – 探索未至之境,通义 – 你的超级个人助理等过程时,回复的消息是类似于打字机效果的,这里的结果其实就是流式响应的。

image-20251113114346633

引入依赖

在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,也就是考试助手)

image-20251114102326560

创建配置类

首先创建一个配置类,配置类上有@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();
  }
}

这里对应的就是上图中的这个部分

image-20251113161317855

代理接口配置

在代理接口中的@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,也就是上图中的这个部分

image-20251113221814405

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
  }
}

对应的回复效果如下

image-20251113162936375

再继续提问最近几年的分数线如何

对应的请求如下

- 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中,然后再次发给大模型

对应的回复效果

image-20251113163123615

从这里可以看出,我们的第二个问题最近几年的分数线如何并没有提到北京大学,但是回答的答案仍然是围绕北京大学展开的

这其实就是会话记忆的基本实现

会话记忆隔离

当前实现的会话记忆存在一个关键问题:未做到用户级别的会话隔离。由于所有访问程序的用户共享同一个会话记忆对象,导致不同用户的会话记录相互混淆,无法准确区分并维护各用户独立的会话上下文。

我们打开另一个浏览器问问题或另外一个用户问问题,仍然会把前面的问答给带上,不管是谁发的消息,都会堆在这一个盒子里,根本分不清哪个记录是哪个用户的,相当于大家共用一个笔记本,写出来的东西全混在一起了。

实现用户级会话隔离,核心就一个逻辑:给每个用户分配一个专属的 “记忆盒子”,谁的会话谁用自己的盒子,互不干扰

引入ChatMemoryProvider

这里来提供两个会话,如下图所示

image-20251113210225753

这里的两个会话如果要保存各自的记忆的话,其实需要两个容器对象,用来容纳各自的历史聊天记录

那其实也意味着整个应用中应该有多个容器对象,多个容器对象彼此相互独立,管理各自的聊天记忆

image-20251113211826367

每个聊天都需要各自独立的ChatMemory对象,这时候如果我们自己来管理那么就非常复杂了,这时候LangChain4j中提供了一个接口ChatMemoryProvider

在 langchain4j 中,ChatMemoryProvider 和 ChatMemory 是 “工厂与产品” 的关系,核心目的是帮你优雅实现「用户级会话隔离」—— 刚好对应你之前遇到的 “所有用户共用一个记忆盒子” 的问题,两者分工明确、协作解决会话隔离需求。

先搞懂:各自到底是干啥的?

用之前 “记忆盒子” 的通俗比喻延伸:

  1. ChatMemory:单个用户的 “专属记忆盒子”
  • 本质:存储单个用户会话记录的容器(比如用户 A 的聊天记录、上下文),是 “产品”。
  • 核心职责:只管自己这个盒子的 “增删改查”—— 比如添加新消息、获取历史记录、清理过期记忆等。
  1. ChatMemoryProvider:“记忆盒子工厂”
  • 本质:负责创建 / 获取用户专属 ChatMemory 的工具(工厂),是 “生产者”。
  • 核心职责:根据「用户唯一标识(比如 user_id)」,为每个用户分配 / 复用一个专属的 ChatMemory 实例,避免不同用户共用同一个 “盒子”。

协作流程(关键)

用户发消息时,整个流程是:

  1. 后端拿到用户的「唯一 ID」(比如登录后的 user_id、游客的临时 ID);
  2. 你调用 ChatMemoryProvider 的 get(用户ID) 方法,让工厂 “找盒子”;
  3. 工厂逻辑:
  • 如果该用户是第一次来,就新建一个 ChatMemory(新盒子),并和用户 ID 绑定;
  • 如果用户之前来过,就直接返回之前给该用户创建的 ChatMemory(复用盒子);
  1. 后端用这个专属的 ChatMemory,添加用户的新消息、获取历史记录,和大模型交互;
  2. 不同用户的 ChatMemory 完全独立,数据互不干扰。

从以上流程中我们可以得到一个核心的结论:

ChatMemoryProvider全面接管了ChatMemory,我们只需要提供memoryId即可

这时候我们需要管理的就是代理对象ChatMemoryProvider对象之间的关系即可,由ChatMemoryProvider来帮我们管理ChatMemory

这时候我们的流程就变为

image-20251114113315370

配置

  • 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,所以可以正常的获取上下文,先来看一下对应的效果

image-20251114154803749

然后再来看一下两次发给大模型的请求信息

  • 第一次
- 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中,有一个成员变量ChatMemoryStoreChatMemoryStore 作为 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);
}

该接口对应的实现类如下

image-20251117110140998

我们前面的会话就是默认使用的就是SingleSlotChatMemoryStore,它做的事情就是将会话记录存在内存中

另外一个InMemoryChatMemoryStore也是将会话记录存在内存中,这里我们就不展开了。

那么如果需要持久化存储,就需要我们自己来提供ChatMemoryStore的实现类,并且通过注册组件的方式提供给ChatMemoryProvider

image-20251118094755620

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>

实现并注册组件

当前完成的是流程中的这个部分

image-20251118094859833

实现该接口并且注册为容器中的组件,其中使用到了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;
  }
}

配置(组件之间的依赖关系)

对应的就是如下的流程

image-20251118100601265

前面我们已经将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中进行保存对应的会话记录

image-20251118102553229

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

image-20251118102928572

以下是其他的会话的

image-20251118103039318
image-20251118103105783
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇