在大模型赋能企业数字化转型的过程中,数据私密性 与对话记忆能力是两大核心痛点。企业核心数据(如客户信息、业务报表、内部文档)若上传至公有云大模型,可能面临数据泄露、合规风险;而原生大模型的会话上下文丢失问题,又会导致对话体验割裂,无法支撑复杂业务场景。
本文将介绍一种基于 Spring AI、Ollama 本地部署 DeepSeek/Qwen 大模型、MongoDB 存储会话上下文的解决方案,既通过本地部署保障数据不流出企业内网,又借助 MongoDB 实现大模型的"长期记忆",兼顾安全性与业务连续性。主打一个免费,节约企业运营费用。
一、核心技术选型与设计思路
1.1 技术栈选型
-
Spring AI :作为大模型应用开发的核心框架,提供了统一的大模型调用接口、对话模板、上下文管理抽象,可快速集成不同厂商的大模型,降低开发成本。其强大的依赖注入、AOP 特性也便于对接企业现有 Spring 生态系统。 官网地址https://www.spring-doc.cn/projects/spring-ai#overview,api地址[https://docs.springframework.org.cn/spring-ai/reference/concepts.html](https://docs.springframework.org.cn/spring-ai/reference/concepts.html "https://docs.springframework.org.cn/spring-ai/reference/concepts.html"),
-
Ollama + DeepSeek/Qwen:Ollama 是轻量级本地大模型部署工具,支持一键部署 DeepSeek(深度求索)、Qwen(通义千问)等开源大模型,无需复杂的环境配置,可直接运行在企业内网服务器,从源头阻断数据外泄。DeepSeek/Qwen 具备优秀的中文理解能力和业务场景适配性,且支持量化部署(如 4bit、8bit),可在普通 x86 服务器上高效运行。
-
MongoDB:作为文档型数据库,其灵活的 Schema 设计非常适合存储非结构化的会话上下文数据(如用户提问、大模型回复、对话时间戳、用户标识等)。支持高频读写、索引优化,能快速查询历史会话,为大模型提供记忆支撑;同时具备分布式部署能力,可满足高并发业务需求。
-
LLM: Large Language Model就是大语言模型,一种学了海量文字的 AI,能听懂人话、说人话,还能写东西、解问题。Spring AI 目前支持处理语言、图像和音频作为输入和输出的模型。

-
**token:**是 AI 模型工作原理的基石。输入时,模型将单词转换为token。输出时,它们将token转换回单词。
在英语中,一个token大约对应一个单词的 75%。作为参考,莎士比亚的全集总共约 90 万个单词,翻译过来大约有 120 万个token。主流大语言模型(如 GPT、Llama、文心一言等)的 token 化规则下,1 个 token 大约对应 1.5~2 个汉字 (这是行业内最通用的近似值)
-
也许更重要的是 "token = 金钱"。在托管 AI 模型的背景下,您的费用由使用的token数量决定。输入和输出都会影响总token数量。
-
开源在线演示,支持多编码(cl100k_base 等),实时分词与 token 计数,可视化强https://tiktokenizer.vercel.app/
1.2 整体架构设计
方案采用分层架构设计,从下至上分为数据存储层、模型服务层、应用框架层、业务接口层,各层职责清晰、解耦性强:
-
数据存储层:MongoDB 负责存储会话上下文、用户信息、权限配置等数据,通过集合(Collection)划分不同数据类型,建立用户 ID、索引提升查询效率。
-
模型服务层:Ollama 本地部署 DeepSeek/Qwen 大模型,对外提供 HTTP API 接口,接收 Spring AI 的请求并返回模型响应,所有计算均在企业内网完成,数据不上云。
-
应用框架层:Spring AI 作为核心枢纽,封装大模型调用逻辑,实现会话上下文的获取、组装与持久化,对接 MongoDB 完成数据读写,同时提供统一的业务适配接口。
-
业务接口层:对外提供 RESTful API ,支撑前端对话界面、企业业务系统(如 CRM、OA)的集成需求。
核心流程:用户发起提问 → 业务接口层接收请求 → Spring AI 从 MongoDB 查询该用户历史会话上下文 → 组装"历史上下文+当前提问"为完整请求 → 调用本地 Ollama 模型服务 → 模型返回响应 → Spring AI 将本次对话(提问+响应)存入 MongoDB → 向用户返回结果。
二、关键功能实现步骤
2.1 环境准备与依赖配置
2.1.1 本地大模型部署(Ollama + DeepSeek)
请看博客《》
2.1.2 项目依赖配置(Spring Boot + Spring AI + MongoDB)
在 Spring Boot 项目的 pom.xml 中引入核心依赖(版本需根据实际情况适配):
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-ollama</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--ollama-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<!-- 无需指定版本,Spring Boot 父工程已管理 -->
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>aliyunmaven</id>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
</project>
2.2 会话上下文模型设计(MongoDB)
MongoDB 存储会话上下文时,需设计合理的文档结构,兼顾查询效率与数据完整性。
2.2 1实现MongoDB访问
java
package com.conca.ai.mongodb;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import java.util.Arrays;
import java.util.Collections;
@Configuration
public class MongoConfig {
// 从配置文件读取参数(也可以硬编码,不推荐)
private final String mongoHost = "127.0.0.1";
private final int mongoPort = 27017;
private final String mongoDatabase = "chat_memory_db";
private final String mongoUsername = "root";
private final String mongoPassword = "123123";
private final String authDatabase = "admin";
@Bean
public MongoTemplate mongoTemplate() {
// 1. 创建认证信息
MongoCredential credential = MongoCredential.createCredential(
mongoUsername, // 用户名
authDatabase, // 认证数据库
mongoPassword.toCharArray() // 密码
);
// 2. 配置 MongoClient 连接参数
MongoClientSettings settings = MongoClientSettings.builder()
.applyToClusterSettings(builder ->
builder.hosts(Collections.singletonList(new ServerAddress(mongoHost, mongoPort))))
.credential(credential) // 添加认证信息
.build();
// 3. 创建 MongoClient 并初始化 MongoTemplate
return new MongoTemplate(MongoClients.create(settings), mongoDatabase);
}
}
2.2.2 新建MongodbMessage对象封装Message信息。
java
package com.conca.ai.mongodb;
import org.springframework.data.mongodb.core.mapping.Document;
// 会话实体:一个会话对应一条MongoDB记录
@Document(collection = "chat_conversations") // 集合名改为会话维度
public class MongodbMessage {
private String conversationId;
private String essageType;
private String textContent;
private long updateTime; // 可选:记录最后更新时间
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getEssageType() {
return essageType;
}
public void setEssageType(String essageType) {
this.essageType = essageType;
}
public String getTextContent() {
return textContent;
}
public void setTextContent(String textContent) {
this.textContent = textContent;
}
public long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(long updateTime) {
this.updateTime = updateTime;
}
}
ai消息Message转成MongoDB对象,Message没有get,set方法,mongodb序列化的时候报错。报错内容如下:
java
rg.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate org.springframework.ai.chat.messages.UserMessage using constructor NO_CONSTRUCTOR with arguments ] with root cause
java.lang.NoSuchMethodException: org.springframework.ai.chat.messages.UserMessage.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3585) ~[na:na]
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754) ~[na:na]
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:141) ~[spring-beans-6.1.1.jar:6.1.1]
at org.springframework.data.mapping.model.ReflectionEntityInstantiator.instantiateClass(ReflectionEntityInstantiator.java:139) ~[spring-data-commons-3.2.0.jar:3.2.0]
at org.springframework.data.mapping.model.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:57) ~[spring-data-commons-3.2.0.jar:3.2.0]
at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:98) ~[spring-data-commons-3.2.0.jar:3.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:519) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:487) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$DefaultConversionContext.convert(MappingMongoConverter.java:2366) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$ConversionContext.convert(MappingMongoConverter.java:2175) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readCollectionOrArray(MappingMongoConverter.java:1441) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$DefaultConversionContext.convert(MappingMongoConverter.java:2343) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$ConversionContext.convert(MappingMongoConverter.java:2175) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1941) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:626) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:544) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:522) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:487) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:423) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:419) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:119) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3278) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2874) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2536) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.MongoTemplate.findOne(MongoTemplate.java:817) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at org.springframework.data.mongodb.core.MongoTemplate.findOne(MongoTemplate.java:804) ~[spring-data-mongodb-4.2.0.jar:4.2.0]
at com.conca.ai.mongodb.MongoChatMemoryRepository.findByConversationId(MongoChatMemoryRepository.java:58) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:352) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765) ~[spring-aop-6.1.1.jar:6.1.1]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717) ~[spring-aop-6.1.1.jar:6.1.1]
at com.conca.ai.mongodb.MongoChatMemoryRepository$$SpringCGLIB$$0.findByConversationId(<generated>) ~[classes/:na]
at org.springframework.ai.chat.memory.MessageWindowChatMemory.add(MessageWindowChatMemory.java:63) ~[spring-ai-model-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor.after(MessageChatMemoryAdvisor.java:111) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.advisor.api.BaseAdvisor.adviseCall(BaseAdvisor.java:53) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextCall$1(DefaultAroundAdvisorChain.java:110) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.12.0.jar:1.12.0]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextCall(DefaultAroundAdvisorChain.java:110) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.lambda$doGetObservableChatClientResponse$1(DefaultChatClient.java:469) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.12.0.jar:1.12.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatClientResponse(DefaultChatClient.java:467) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatClientResponse(DefaultChatClient.java:446) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.content(DefaultChatClient.java:441) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
at com.conca.ai.ChatMemoryMongodoController.chat(ChatMemoryMongodoController.java:57) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:254) ~[spring-web-6.1.1.jar:6.1.1]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:182) ~[spring-web-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.1.jar:6.1.1]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.1.jar:6.1.1]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.16.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.1.jar:6.1.1]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.16.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.1.jar:6.1.1]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.1.jar:6.1.1]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.1.jar:6.1.1]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.1.jar:6.1.1]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
at java.base
2.3 Spring AI 集成本地大模型与上下文管理
大模型本身不具备持久化的 "自主记忆" 能力 ,也无法主动 "记住" 历史上下文,它对对话的理解完全依赖于每次请求时传入的上下文信息。
大模型的训练是离线完成的,推理阶段是 "无状态" 的 ------ 每一次请求都是独立的,模型不会主动存储、保留上一轮的对话内容,也没有内置的数据库来记录交互历史。所谓的 "对话记忆",本质是人为将历史消息拼接进当前请求的输入中,让模型在单次推理时 "看到" 完整上下文。
2.3.1 人为将历史消息拼接进当前请求的输入中
程序员可以手工处理历史消息,自己写代码也可以实现记忆力,就是代码丑点。下面是个简单例子。后面重点说的是spring ai 提供的记忆力怎么实现。
java
List<Message> messages = new ArrayList<>();
//第一轮对话
messages.add(new UserMessage("2乘以5是多少"));
ChatResponse response = chatModel.call(new Prompt(messages));
String content = response.getResult().getOutput().getContent();
messages.add(new AssistantMessage(content));
//第二轮对话
messages.add(new UserMessage("再乘以50是多少"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();
messages.add(new AssistantMessage(content));
//第三轮对话
messages.add(new UserMessage("最后除以2是多少?"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();
System.out.printf("content: %s\n", content);
2.3.2 配置 Ollama 模型客户端
在 application.properties中配置 Ollama 服务地址、模型名称等参数:
java
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=qwen3:0.6b
创建配置类,注入 Ollama 模型客户端,注册了qwen客户端、deepeek客户端,deepeek+MongoDB持久化客户端等。重点是ollamaDeepseekMongoChatClient注册了MongoDB驱动类。
java
package com.conca.ai.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.conca.ai.mongodb.MongoChatMemoryRepository;
/**
*/
@Configuration
public class OllamaLLMConfig
{
// 从 application.properties 读取 ollama 的 base-url 配置
@Value("${spring.ai.ollama.base-url}")
private String ollamaBaseUrl;
//第一个是配置文件默认生成的,名字是默认的ollamaChatModel
@Bean(name="ollamaChatModel")
public OllamaChatModel ollamaChatModel() {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(ollamaBaseUrl).build();
OllamaOptions options = OllamaOptions.builder()
.model("qwen3:0.6b") // 第二个模型名称
.temperature(0.9) // 不同的参数配置
.build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(options).build();
}
/**
* 知识出处:
* https://java2ai.com/docs/1.0.0.2/tutorials/basics/chat-client/?spm=5176.29160081.0.0.2856aa5cmUTyXC#%E5%88%9B%E5%BB%BA-chatclient
* @param dashscopeChatModel
* @return
*/
@Bean(name = "ollamaChatClient")
public ChatClient chatClient(@Qualifier("ollamaChatModel") ChatModel dashscopeChatModel)
{
return ChatClient.builder(dashscopeChatModel).build();
}
// 第二个模型:qwen(通义千问)
@Bean(name="ollamaQwenChatModel")
public OllamaChatModel ollamaQwenChatModel() {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(ollamaBaseUrl).build();
OllamaOptions options = OllamaOptions.builder()
.model("qwen3:0.6b") // 第二个模型名称
.temperature(0.9) // 不同的参数配置
.build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(options).build();
}
@Bean(name = "ollamaQwenChatClient")
public ChatClient qwenChatClient(@Qualifier("ollamaQwenChatModel") ChatModel qwen)
{
return ChatClient.builder(qwen)
.defaultOptions(OllamaOptions.builder()
.build())
.build();
}
@Bean(name = "ollamaQwenMongoChatClient")
public ChatClient ollamaQwenMongoChatClient(@Qualifier("ollamaQwenChatModel") ChatModel qwen,
MongoChatMemoryRepository chatMemoryRepository)
{
MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(10)
.build();
return ChatClient.builder(qwen)
.defaultOptions(OllamaOptions.builder()
.build())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
.build();
}
// 第三个模型:TinyLlama 是由 TinyLlama Team 开发的轻量级开源大语言模型,基于 Meta 的 Llama 2 架构精简而来,核心目标是在低资源设备(如普通 PC、边缘设备) 上实现高性能的本地推理,兼顾轻量化和实用性。
@Bean(name="ollamaTinyllama")
public OllamaChatModel ollamaTinyllamaChatModel() {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(ollamaBaseUrl).build();
OllamaOptions options = OllamaOptions.builder()
.model("tinyllama") // 模型名称
.temperature(0.9) // 不同的参数配置
.build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(options).build();
}
@Bean(name = "ollamaTinyllamaChatClient")
public ChatClient tinyllamaChatClient(@Qualifier("ollamaTinyllama") ChatModel qwen)
{
return ChatClient.builder(qwen)
.defaultOptions(OllamaOptions.builder()
.build())
.build();
}
@Bean(name="ollamaDeepseekChatModel")
public OllamaChatModel ollamaDeepseekChatModel() {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(ollamaBaseUrl).build();
OllamaOptions options = OllamaOptions.builder()
.model("deepseek-r1:1.5b") //模型名称
.temperature(0.9) // 不同的参数配置
.build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(options).build();
}
@Bean(name = "ollamaDeepseekMongoChatClient")
public ChatClient ollamaDeepseekMongoChatClient(@Qualifier("ollamaDeepseekChatModel") ChatModel qwen,
MongoChatMemoryRepository chatMemoryRepository)
{
MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(10)
.build();
return ChatClient.builder(qwen)
.defaultOptions(OllamaOptions.builder()
.build())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
.build();
}
}
2.3.2 ChatModel和ChatClient简介
ChatModel API 让应用开发者可以非常方便的与 AI 模型进行文本交互,它抽象了应用与模型交互的过程,包括使用 Prompt 作为输入,使用 ChatResponse 作为输出等。ChatModel 的工作原理是接收 Prompt 或部分对话作为输入,将输入发送给后端大模型,模型根据其训练数据和对自然语言的理解生成对话响应,应用程序可以将响应呈现给用户或用于进一步处理。

ChatClient 提供了与 AI 模型通信的 Fluent API,它支持同步和反应式(Reactive)编程模型。与 ChatModel、Message、ChatMemory 等原子 API 相比,使用 ChatClient 可以将与 LLM 及其他组件交互的复杂性隐藏在背后,因为基于 LLM 的应用程序通常要多个组件协同工作(例如,提示词模板、聊天记忆、LLM Model、输出解析器、RAG 组件:嵌入模型和存储),并且通常涉及多个交互,因此协调它们会让编码变得繁琐。当然使用 ChatModel 等原子 API 可以为应用程序带来更多的灵活性,成本就是您需要编写大量样板代码。
2.3.3 会话上下文持久化与读取
大型语言模型(LLM)是无状态的,意味着它们不会保留之前互动的信息。当你需要在多次交互中保持上下文或状态时,这可能会成为一个限制。为此,Spring AI 提供了聊天内存功能,允许你在多次与 LLM 交互中存储和检索信息。
这聊天记忆抽象允许你实现多种类型的内存,以支持不同的使用场景。消息的底层存储由聊天记忆仓库,其唯一职责是存储和检索消息。这取决于聊天记忆实现以决定保留哪些消息以及何时删除它们。策略的例子包括保留最后N条消息、保留消息一定时间段,或将消息限制在某个Tokens限制内。
Spring AI 会自动配置聊天记忆你可以直接在应用中使用BEAN。默认情况下,它使用内存存储库来存储消息(InMemoryChatMemoryRepository)。JdbcChatMemoryRepository也是一个内置实现,使用 JDBC 将消息存储在关系数据库中。它开箱即用支持多个数据库,适合需要持续存储聊天内存的应用。
咱们不使用mysql传统关系型数据库,而是把聊天消息持久化存储到MongoDB,防止停机丢失问题。
创建 ConversationRepository 接口,继承ChatMemoryRepository,实现会话数据的 CRUD 操作:
java
package com.conca.ai.mongodb;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
// 核心注解:让Spring扫描并管理这个类,自动注入MongoTemplate
@Repository
public class MongoChatMemoryRepository implements ChatMemoryRepository {
private final MongoTemplate mongoTemplate;
// 构造函数注入MongoTemplate
@Autowired
public MongoChatMemoryRepository(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* 核心修改:将整个消息列表存入ID为conversationId的单条记录
*/
@Override
public void saveAll(String conversationId, List<Message> messages) {
Query query = new Query(Criteria.where("conversationId").is(conversationId));
mongoTemplate.remove(query, MongodbMessage.class);
// 1. 创建/更新会话记录:ID=conversationId,消息列表=传入的messages
for (Message bean: messages) {
MongodbMessage conversation = new MongodbMessage();
conversation.setConversationId(conversationId);
conversation.setEssageType(bean.getMessageType().name());
conversation.setTextContent(bean.getText());
conversation.setUpdateTime(System.currentTimeMillis());
// 2. 保存/更新:MongoTemplate的save方法会根据ID自动upsert(存在则更新,不存在则插入)
mongoTemplate.save(conversation);
}
}
/**
* 查询所有会话ID(即MongoDB中所有文档的_id)
*/
@Override
public List<String> findConversationIds() {
// 查询所有Conversation文档,提取conversationId字段
return mongoTemplate.query(MongodbMessage.class)
.distinct("conversationId") .as(String.class) .all();
}
/**
* 根据会话ID查询:直接获取该ID对应的记录,返回其messages列表
*/
@Override
public List<Message> findByConversationId(String conversationId) {
// 构造查询条件:按主键_id=conversationId查询
Query query = new Query(Criteria.where("conversationId").is(conversationId));
List<MongodbMessage> list = mongoTemplate.find(query, MongodbMessage.class);
List<Message> result= new ArrayList<Message>();
for (MongodbMessage bean : list) {
if("USER".equals(bean.getEssageType())) {
result.add(new UserMessage(bean.getTextContent()));
} else if("ASSISTANT".equals(bean.getEssageType())) {
result.add(new AssistantMessage(bean.getTextContent()));
} else if("SYSTEM".equals(bean.getEssageType())) {
result.add(new SystemMessage(bean.getTextContent()));
}
}
return result;
}
/**
* 删除会话:直接删除ID为conversationId的整条记录
*/
@Override
public void deleteByConversationId(String conversationId) {
Query query = new Query(Criteria.where("conversationId").is(conversationId));
mongoTemplate.remove(query, MongodbMessage.class);
}
}
2.4 对外提供业务接口
创建 ChatMemoryMongodoController,提供 RESTful API 供前端或其他业务系统调用:
java
package com.conca.ai;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
@RestController
public class ChatMemoryMongodoController
{
@Resource(name="ollamaDeepseekMongoChatClient")
private ChatClient chatClient;
// private ChatClient chatClient;
// @Autowired
// public ChatMemoryMongodoController(ChatModel chatModel, MongoChatMemoryRepository chatMemoryRepository)
// {
// this.chatModel = chatModel;
//
// MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
// .chatMemoryRepository(chatMemoryRepository)
// .maxMessages(10)
// .build();
//
// this.chatClient = ChatClient.builder(chatModel)
// .defaultOptions(ChatOptions.builder().build())
// .defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
// .build();
// }
/**
* @param msg
* @param userId
* @return
*/
@GetMapping("/chatmemory/chat")
public String chat(String msg, String userId)
{
return chatClient
.prompt(msg)
.advisors(advisorSpec -> advisorSpec.param(CONVERSATION_ID, userId))
.call()
.content();
}
}
三、会话测试记忆力
3.1 启动程序
java
package com.conca.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.conca.ai"}) // 扫描整个com.conca.ai包及其子包
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// 1. 获取并打印项目包路径(当前类的包名)
String packagePath = Application.class.getPackage().getName();
// 2. 定义项目名称(可根据实际情况修改)
String projectName = "Alibaba AI Stream Output Project"; // 你可以改成自己的项目名
// 格式化输出信息
System.out.println("====================================");
System.out.println(" 应用启动完毕 🚀");
System.out.println("====================================");
System.out.println("项目名称:" + projectName);
System.out.println("包路径:" + packagePath);
System.out.println("欢迎来到ai");
System.out.println("====================================");
}
}
第一次浏览器访问:http://localhost:8080/chatmemory/chat?msg=2乘以5是多少\&userId=1001
大模型回答:2 乘以 5 的结果是 10。\n\n\\[ 2 \\times 5 = 10 \\]
第二次浏览器访问:http://localhost:8080/chatmemory/chat?msg=再乘以50是多少\&userId=1001
大模型回答:好的!让我们一步一步来计算:\n\n首先,计算 \\(2 \\times 5 = 10\\)。\n\n接下来,再将这个结果乘以 50:\n\\[\n10 \\times 50 = 500\n\\]\n\n所以,最后的答案是 **500**。
第三次浏览器访问:http://localhost:8080/chatmemory/chat?msg=最后除以2是多少\&userId=1001
大模型回答:\\[\n2 \\times 5 = 10\n\\]\n\\[\n10 \\times 50 = 500\n\\]\n\\[\n500 \\div 2 = 250\n\\]\n\n所以,最后的答案是 **250**。
3.2 性能优化策略
-
上下文长度控制:大模型上下文窗口存在长度限制,可在服务中设置历史消息最大保留条数(如最近10条),或通过语义压缩(调用小模型对历史上下文进行摘要)减少 tokens 消耗,提升响应速度。
-
MongoDB 索引优化:除 userId 索引外,可根据业务场景添加对话 ID、时间戳等复合索引,优化查询性能;对于高频访问的会话,可启用 MongoDB 缓存机制。
-
模型量化与资源分配:使用 4bit/8bit 量化版模型,降低内存占用与推理时间;根据服务器配置调整 Ollama 模型的 CPU/GPU 资源分配,提升并发处理能力。
3.3 记忆会话是怎么发送给大模型的?
MessageChatMemoryAdvisor代码实现了 Spring AI 框架中的聊天记忆顾问(MessageChatMemoryAdvisor) ,核心作用是:在每次与 AI 模型交互时,自动加载历史对话记录 并拼接到当前请求中(让模型感知上下文),同时保存新的用户提问和助手回复到聊天记忆中,实现多轮对话的上下文连贯性。
我重写了MessageChatMemoryAdvisor类before方法,打印了用户消息内容:
java
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.chat.client.advisor;
import java.util.ArrayList;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import org.springframework.ai.chat.client.ChatClientMessageAggregator;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.util.Assert;
/**
* Memory is retrieved added as a collection of messages to the prompt
*
* @author Christian Tzolov
* @author Mark Pollack
* @author Thomas Vitale
* @since 1.0.0
*/
public final class MessageChatMemoryAdvisor implements BaseChatMemoryAdvisor {
private final ChatMemory chatMemory;
private final String defaultConversationId;
private final int order;
private final Scheduler scheduler;
private MessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int order,
Scheduler scheduler) {
Assert.notNull(chatMemory, "chatMemory cannot be null");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
Assert.notNull(scheduler, "scheduler cannot be null");
this.chatMemory = chatMemory;
this.defaultConversationId = defaultConversationId;
this.order = order;
this.scheduler = scheduler;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public Scheduler getScheduler() {
return this.scheduler;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);
System.out.println("===== Spring AI 发送给模型的请求详情 ====="+conversationId);
// System.out.println("1. 消息内容:");
// 1. Retrieve the chat memory for the current conversation.
List<Message> memoryMessages = this.chatMemory.get(conversationId);
for (int i = 0; i < memoryMessages.size(); i++) {
Message message = memoryMessages.get(i);
// System.out.printf(" 消息%d - 角色: %s, 内容: %s%n",
// i+1, message.getMessageType(), message.getText());
}
// 2. Advise the request messages list.
// System.out.println("2. 请求参数:");
List<Message> processedMessages = new ArrayList<>(memoryMessages);
processedMessages.addAll(chatClientRequest.prompt().getInstructions());
// System.out.println(chatClientRequest.prompt().getInstructions().toString());
// 3. Create a new request with the advised messages.
ChatClientRequest processedChatClientRequest = chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build())
.build();
// 4. Add the new user message to the conversation memory.
// System.out.println("4. 请求参数:");
UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();
System.out.println(userMessage);
System.out.println(processedChatClientRequest.prompt());
this.chatMemory.add(conversationId, userMessage);
return processedChatClientRequest;
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
List<Message> assistantMessages = new ArrayList<>();
if (chatClientResponse.chatResponse() != null) {
assistantMessages = chatClientResponse.chatResponse()
.getResults()
.stream()
.map(g -> (Message) g.getOutput())
.toList();
}
this.chatMemory.add(this.getConversationId(chatClientResponse.context(), this.defaultConversationId),
assistantMessages);
return chatClientResponse;
}
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
StreamAdvisorChain streamAdvisorChain) {
// Get the scheduler from BaseAdvisor
Scheduler scheduler = this.getScheduler();
// Process the request with the before method
return Mono.just(chatClientRequest)
.publishOn(scheduler)
.map(request -> this.before(request, streamAdvisorChain))
.flatMapMany(streamAdvisorChain::nextStream)
.transform(flux -> new ChatClientMessageAggregator().aggregateChatClientResponse(flux,
response -> this.after(response, streamAdvisorChain)));
}
public static Builder builder(ChatMemory chatMemory) {
return new Builder(chatMemory);
}
public static final class Builder {
private String conversationId = ChatMemory.DEFAULT_CONVERSATION_ID;
private int order = Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER;
private Scheduler scheduler = BaseAdvisor.DEFAULT_SCHEDULER;
private ChatMemory chatMemory;
private Builder(ChatMemory chatMemory) {
this.chatMemory = chatMemory;
}
/**
* Set the conversation id.
* @param conversationId the conversation id
* @return the builder
*/
public Builder conversationId(String conversationId) {
this.conversationId = conversationId;
return this;
}
/**
* Set the order.
* @param order the order
* @return the builder
*/
public Builder order(int order) {
this.order = order;
return this;
}
public Builder scheduler(Scheduler scheduler) {
this.scheduler = scheduler;
return this;
}
/**
* Build the advisor.
* @return the advisor
*/
public MessageChatMemoryAdvisor build() {
return new MessageChatMemoryAdvisor(this.chatMemory, this.conversationId, this.order, this.scheduler);
}
}
}
before() 方法:请求发送前的增强。执行时机:用户发送请求 → AI 模型处理前。重点打印了processedChatClientRequest信息
java
// 4. Add the new user message to the conversation memory.
// System.out.println("4. 请求参数:");
UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();
System.out.println(userMessage);
System.out.println(processedChatClientRequest.prompt());
this.chatMemory.add(conversationId, userMessage);
第三次访问浏览器http://localhost:8080/chatmemory/chat?msg=最后除以2是多少\&userId=1001。MessageChatMemoryAdvisor打印的具体内容如下:
===== Spring AI 发送给模型的请求详情 =====1001
UserMessage{content='最后除以2是多少', properties={messageType=USER}, messageType=USER}
Prompt{messages=[UserMessage{content='2乘以5是多少', properties={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=2 乘以 5 是 **10**。, metadata={messageType=ASSISTANT}], UserMessage{content='再乘以50是多少', properties={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=好的,我们一步步来解答这个问题:
首先,计算2乘以5:
\[ 2 \times 5 = 10 \]
接下来,用得到的结果再乘以50:
\[ 10 \times 50 = 500 \]
所以,答案是:\\boxed{500}。, metadata={messageType=ASSISTANT}], UserMessage{content='最后除以2是多少', properties={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.ollama.api.OllamaOptions@c9016ba0}
第三次访问访问大模型processedChatClientRequest输出内容进行格式化,具体如下:
┌──────────────────────────────────────────────────────────────────────────────┐
| 顶层字段 | 字段值/子字段详情 |
├──────────────────────────────────────────────────────────────────────────────┤
| messages | 数组(共5条消息,对话流程:用户→助手→用户→助手→用户) |
| ├─ 第1条(USER) | content: 2乘以5是多少 |
| │ | properties: {messageType=USER} |
| │ | messageType: USER |
| ├─ 第2条(ASSISTANT)| toolCalls: [](无工具调用) |
| │ | textContent: 2 乘以 5 是 **10**。 |
| │ | metadata: {messageType=ASSISTANT} |
| │ | messageType: ASSISTANT |
| ├─ 第3条(USER) | content: 再乘以50是多少 |
| │ | properties: {messageType=USER} |
| │ | messageType: USER |
| ├─ 第4条(ASSISTANT)| toolCalls: [](无工具调用) |
| │ | textContent: 好的,我们一步步来解答这个问题: |
| │ | 首先,计算2乘以5:\[ 2 × 5 = 10 \] |
| │ | 接下来,用得到的结果再乘以50:\[ 10 × 50 = 500 \] |
| │ | 所以,答案是:\\boxed{500}。 |
| │ | metadata: {messageType=ASSISTANT} |
| │ | messageType: ASSISTANT |
| ├─ 第5条(USER) | content: 最后除以2是多少 |
| │ | properties: {messageType=USER} |
| │ | messageType: USER |
| modelOptions | org.springframework.ai.ollama.api.OllamaOptions@c9016ba0 |
└──────────────────────────────────────────────────────────────────────────────┘
重点:把前面的一问一答,内容全部组装到了第三次会话中了。大模型是没有记忆力的。只是把用户存储的问题和答案,组装好,装成Prompt,一次性发送给大模型。

四、mongo数据库会话分析
4.1 MongoDB表数据,用户问题和模型回答都是一条记录。

4.2 Message 角色

Spring AI 的 Message 有4 个核心角色 ,由 org.springframework.ai.chat.messages.MessageType 枚举定义,分别是 SYSTEM(系统)、USER(用户)、ASSISTANT(助手)、TOOL(工具 / 函数)。以下是各角色的详细说明与对应实现类:
| 角色名称 | 枚举值 | 对应消息实现类 | 核心作用 |
|---|---|---|---|
| 系统角色 | SYSTEM | SystemMessage | 设定 AI 的行为准则、全局指令、响应风格,约束整个对话的上下文规则 |
| 用户角色 | USER | UserMessage | 承载用户的提问、指令或输入,是 AI 生成响应的核心触发条件 |
| 助手角色 | ASSISTANT | AssistantMessage | 存储 AI 模型生成的回复内容,也可包含工具调用请求,用于维持多轮对话连贯性 |
| 工具 / 函数角色 | TOOL | ToolResponseMessage | 响应助手的工具调用请求,返回外部工具(如 API、数据库)的执行结果 |
五、总结
5.1spring ai是什么?
Spring AI的本质 是**"大模型时代的Spring JDBC"**。正如JDBC用统一接口屏蔽了不同数据库(MySQL、Oracle)的差异,Spring AI试图用统一接口屏蔽不同大模型服务的差异。

5.2spring ai能做什么?
| 维度 | 能做什么 (能力与价值) | 不能做什么 (边界与限制) |
|---|---|---|
| 核心定位 | 大模型应用开发框架 :提供统一API,简化集成流程,专注于应用层。 | 不是大模型本身:不提供或训练底层模型,只是一个"调用方"和"编排方"。 |
| 开发体验 | 标准化与提效 :通过抽象接口、模板、工具,大幅降低集成复杂度,统一不同厂商的API。 | 有学习成本 :需理解其抽象概念(如OutputParser, PromptTemplate)和编程范式,并非零代码工具。 |
| 核心功能 | 支持RAG全流程:从文档加载、向量化存储到检索、对话,提供完整组件支持。 | 不是向量数据库:提供对接接口,但本身不实现存储引擎,需依赖Chroma、PgVector等。 |
| 生态与集成 | 连接多种模型与平台:支持OpenAI、Azure、Anthropic、阿里云等主流服务。 | 并非万能中间件:对新模型或小众平台的支持有滞后,依赖社区或官方适配。 |
| 高级功能 | 实现AI原生功能 :支持函数调用 、多模态 、智能体等复杂交互模式。 | 无法突破模型本身能力:最终效果上限取决于所选用的大模型。 |
| 生产就绪 | 提供项目管理工具:如评估、可观测性功能的早期支持,助力应用上线。 | 无法保证稳定性与成本:不解决模型服务的SLA、费率、速率限制等问题,需自行处理。 |
本文提出的 Spring AI + Ollama 本地大模型 + MongoDB 方案,通过本地部署解决数据私密性问题 ,通过文档数据库存储会话上下文实现大模型记忆能力,同时依托 Spring AI 框架实现快速开发与企业生态集成,为企业级大模型应用提供了安全、高效、可扩展的落地路径。