基于Spring AI+本地大模型+MongoDB实现私密化与记忆能力-企业级免费大模型应用

在大模型赋能企业数字化转型的过程中,数据私密性对话记忆能力是两大核心痛点。企业核心数据(如客户信息、业务报表、内部文档)若上传至公有云大模型,可能面临数据泄露、合规风险;而原生大模型的会话上下文丢失问题,又会导致对话体验割裂,无法支撑复杂业务场景。

本文将介绍一种基于 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 整体架构设计

方案采用分层架构设计,从下至上分为数据存储层、模型服务层、应用框架层、业务接口层,各层职责清晰、解耦性强:

  1. 数据存储层:MongoDB 负责存储会话上下文、用户信息、权限配置等数据,通过集合(Collection)划分不同数据类型,建立用户 ID、索引提升查询效率。

  2. 模型服务层:Ollama 本地部署 DeepSeek/Qwen 大模型,对外提供 HTTP API 接口,接收 Spring AI 的请求并返回模型响应,所有计算均在企业内网完成,数据不上云。

  3. 应用框架层:Spring AI 作为核心枢纽,封装大模型调用逻辑,实现会话上下文的获取、组装与持久化,对接 MongoDB 完成数据读写,同时提供统一的业务适配接口。

  4. 业务接口层:对外提供 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)编程模型。与 ChatModelMessageChatMemory 等原子 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 性能优化策略

  1. 上下文长度控制:大模型上下文窗口存在长度限制,可在服务中设置历史消息最大保留条数(如最近10条),或通过语义压缩(调用小模型对历史上下文进行摘要)减少 tokens 消耗,提升响应速度。

  2. MongoDB 索引优化:除 userId 索引外,可根据业务场景添加对话 ID、时间戳等复合索引,优化查询性能;对于高频访问的会话,可启用 MongoDB 缓存机制。

  3. 模型量化与资源分配:使用 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 框架实现快速开发与企业生态集成,为企业级大模型应用提供了安全、高效、可扩展的落地路径。

相关推荐
云卓SKYDROID2 小时前
无人机飞行模式详解
人工智能·无人机·高科技·云卓科技·技术解析、
数字游民95272 小时前
小程序上新,猜对了么更新110组素材
人工智能·ai·小程序·ai绘画·自媒体·数字游民9527
泰迪智能科技2 小时前
分享|联合编写教材入选第二批“十四五”职业教育国家规划教材名单
大数据·人工智能
模型时代2 小时前
热力学计算技术或将大幅降低AI图像生成能耗
人工智能
企业老板ai培训2 小时前
从九尾狐AI实战案例拆解AI短视频获客的架构设计:智能矩阵如何提升企业效率?
人工智能
龙腾AI白云3 小时前
知识图谱如何在制造业实际落地应用
人工智能·知识图谱
力学与人工智能3 小时前
“高雷诺数湍流数据库的构建及湍流机器学习集成研究”湍流重大研究计划集成项目顺利结题
数据库·人工智能·机器学习·高雷诺数·湍流·重大研究计划·项目结题
娟宝宝萌萌哒3 小时前
智能体设计模式重点
人工智能·设计模式
哪里不会点哪里.3 小时前
什么是 Spring Cloud?
后端·spring·spring cloud