Spring AI 智能咨询系统实战:RAG、MCP、安全与持久化一体化落地
一个真正可用的 AI 咨询系统,不能只停留在“用户问一句,模型答一句”。它需要记住会话上下文,能基于企业知识库回答问题,遇到无法处理的需求时能转交外部系统,还要具备安全拦截和数据持久化能力。
这里以“比特就业课”咨询场景为例,搭建一套面向课程咨询的智能聊天系统。整体目标是:提供 Web 聊天入口,支持流式回复、会话管理、RAG 知识库问答、敏感词引导、MCP 工单调用,以及 MySQL 持久化。
系统整体设计
系统由两个核心服务组成:
| 服务 | 职责 |
|---|---|
chat-bot-service | 对外提供 Web 服务,默认端口8081,负责聊天接口、会话管理、RAG 检索、安全引导和 MCP 客户端调用 |
ticket-service | MCP Server,无 Web 接口,通过 STDIO 启动,负责暴露创建工单、查询工单等工具能力,并将数据写入 MySQL |
主要功能包括:
- 用户通过 Web 页面与机器人对话,消息支持流式返回。
- 支持创建新会话、查看历史会话、查看会话消息、删除会话。
- 对话上下文通过
JdbcChatMemoryRepository存入 MySQL。 - 基于企业介绍和方向文档构建 RAG 知识库。
- 当知识库无法回答,或用户要求人工服务时,通过 MCP 创建工单。
- 对敏感词和高风险内容进行拦截或安全引导。
项目初始化与模型接入
项目可以命名为bit-chat-bot,其中核心模块为chat-bot-service。基础栈选择 Spring Boot3.5.3、Spring AI1.0.1,并接入 Spring AI Alibaba。
父级pom.xml统一管理 Spring AI 版本:
<properties><spring-ai.version>1.0.1</spring-ai.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>chat-bot-service中引入 Web、WebFlux 和 DashScope:
<dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-alibaba-starter-dashscope</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency>基础配置如下:
server:port:8081spring:application:name:spring-chat-botai:dashscope:api-key:${DASHSCOPE_API_KEY}聊天客户端可以统一在配置类中创建:
@BeanpublicChatMemorychatMemory(){returnMessageWindowChatMemory.builder().maxMessages(10).build();}@BeanpublicChatClientdashscopeChatClient(DashScopeChatModelchatModel,ChatMemorychatMemory){returnChatClient.builder(chatModel).defaultSystem(""" 你叫小特,是比特教育研发的智能 AI 助手,擅长 Java 和 C++, 主要工作是解决学生在学习过程中遇到的问题 """).defaultAdvisors(newSimpleLoggerAdvisor(),MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}启动后访问http://127.0.0.1:8081/index.html,先确认基础聊天能力可用。
构建 RAG 知识库
由于垂直业务的公开资料有限,模型仅靠通用知识很难准确回答课程细节。解决方式是引入企业内部文档,将其转成可检索的向量知识库,再把召回内容作为上下文交给模型。
RAG 构建流程包括四步:
- 加载 Markdown 文档。
- 对长文本进行切分。
- 为文本块补充关键词元信息。
- 写入向量数据库。
文档可以放在resources/bit目录下,通过MarkdownDocumentReader加载:
MarkdownDocumentReaderConfigconfig=MarkdownDocumentReaderConfig.builder().withHorizontalRuleCreateDocument(true).withIncludeCodeBlock(false).withIncludeBlockquote(false).withAdditionalMetadata("filename",fileName).build();MarkdownDocumentReaderreader=newMarkdownDocumentReader(resource,config);List<Document>documents=reader.get();文本分割的重点是避免把语义完整的一句话切断。可以在参考TokenTextSplitter的基础上,按中文标点和换行位置做截断,让每个文本块既不超出模型上下文限制,又尽量保持语义完整。
完成切分后,再使用KeywordMetadataEnricher为每个文本块生成关键词:
KeywordMetadataEnricherenricher=KeywordMetadataEnricher.builder(chatModel).keywordCount(5).build();returnenricher.apply(documents);初始阶段可以使用SimpleVectorStore:
@BeanpublicVectorStorevectorStore(DashScopeEmbeddingModelembeddingModel){returnSimpleVectorStore.builder(embeddingModel).build();}最后通过初始化组件把流程串起来:
@PostConstructpublicvoidinitData(){List<Document>documentList=documentLoader.loadMarkdowns();List<Document>tokenDocuments=splitter.apply(documentList);List<Document>enrichDocument=keywordEnricher.enrich(tokenDocuments);vectorStore.add(enrichDocument);}将知识库绑定到 ChatClient
知识库构建完成后,需要通过QuestionAnswerAdvisor把检索结果注入对话上下文。
提示词的设计非常关键。它要告诉模型:如果上下文里有答案,就直接回答;如果没有答案,就引导用户联系专业顾问;回答时不要反复出现“根据上下文”这类冗余表达。
QuestionAnswerAdvisorquestionAnswerAdvisor=QuestionAnswerAdvisor.builder(vectorStore).promptTemplate(promptTemplate).build();returnChatClient.builder(chatModel).defaultSystem(""" 你是一名专业的企业培训课程咨询助手,代表【比特就业课】为客户提供课程咨询服务。 你的职责是准确、礼貌、高效地解答客户关于比特就业课培训课程的各类问题。 """).defaultAdvisors(newSimpleLoggerAdvisor()).defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()).defaultAdvisors(questionAnswerAdvisor).build();为了让配置更清晰,可以把 Advisor 创建逻辑抽到AdvisorFactory中,后续增加重排序、安全策略或 MCP 工具时,ChatClient的结构会更容易维护。
使用多线程优化文档处理
在知识库初始化过程中,“补充关键词元信息”需要调用大模型,是最容易拖慢启动的环节。文档数量较多时,整体耗时可能达到数分钟。
优化思路是将文档分批,并用线程池并发处理。主线程通过CountDownLatch等待所有批次完成:
privatefinalExecutorServiceexecutorService=Executors.newFixedThreadPool(8);publicvoidprocessDocuments(List<Document>documents,intbatchSize){List<List<Document>>batches=splitToBatches(documents,batchSize);CountDownLatchlatch=newCountDownLatch(batches.size());for(List<Document>batch:batches){executorService.submit(()->{try{List<Document>enrich=keywordEnricher.enrich(batch);vectorStore.add(enrich);}finally{latch.countDown();}});}latch.await(15,TimeUnit.MINUTES);}这里要注意,每个批次只处理自己的Document集合,避免多个线程同时修改同一对象。对于真实生产环境,还要考虑模型服务限流、失败重试和批次状态记录。
从内存向量库切换到 Redis
SimpleVectorStore适合本地验证,但存在两个明显问题:
- 向量数据在内存中,服务重启后会丢失。
- 每次重启都要重新加载、切分、向量化和写入,启动效率低。
解决方案是切换到 Redis Vector Store,并把数据初始化流程做成可配置:
data:is-load:true初始化时判断配置:
@Value("${data.is-load}")privatebooleanisLoad;@PostConstructpublicvoidinitData(){if(!isLoad){log.info("知识库文档无需加载");return;}// 执行文档加载流程}加入 Redis 向量库依赖后,配置索引名称和 key 前缀:
spring:ai:vectorstore:redis:initialize-schema:trueindex-name:bit-chat-botprefix:"rag:"data:redis:url:redis://127.0.0.1:6379此时删除原来的SimpleVectorStoreBean,Spring 会根据依赖和配置自动注入 Redis 版本的VectorStore。
引入重排序提升检索质量
向量检索负责快速召回候选文档,但召回结果的顺序不一定最适合最终回答。可以在初步检索后加入重排序模型,对候选内容重新打分,再把更相关的内容交给大模型。
Spring AI 中可以使用RetrievalRerankAdvisor:
publicstaticAdvisorcreateRerankAdvisor(VectorStorevectorStore,RerankModelrerankModel){returnnewRetrievalRerankAdvisor(vectorStore,rerankModel,SearchRequest.builder().topK(100).build());}绑定到ChatClient:
.defaultAdvisors(AdvisorFactory.createQuestionAnswerAdvisor(vectorStore)).defaultAdvisors(AdvisorFactory.createRerankAdvisor(vectorStore,rerankModel))如果使用 DashScope 的重排序模型,还可以配置返回数量:
spring:ai:dashscope:rerank:options:topN:20调试时可以观察重排序前后Document的顺序变化,确认精排是否真正提升了上下文相关性。
通过 MCP 接入工单系统
RAG 能回答知识库覆盖的问题,但实际咨询中经常会出现超出范围的需求,比如退款申请、转人工顾问、复杂流程确认等。此时不应该让模型硬编答案,而是通过 MCP 调用外部工具,把问题转成工单。
ticket-service作为 MCP Server,核心工具包括:
- 创建工单。
- 根据工单 ID 查询工单。
MySQL 表可以设计为ticket_info,字段包括ticket_id、title、description、related_chat_id、status、creator、assignee、created_time等。
工具服务使用@Tool暴露能力:
@Tool(description="根据提供的信息创建工单")publicStringcreateTicket(@ToolParam(description="工单标题,不能为空")Stringtitle,@ToolParam(description="工单详细描述,不能为空")Stringdescription,@ToolParam(description="工单关联的会话ID,不能为空")StringrelatedChatId){// 参数校验、写入数据库、返回工单号}@Tool(description="根据工单ID查询工单信息")publicTicketInfoqueryTicket(@ToolParam(description="工单ID,不能为空")StringticketId){returnticketMapper.selectByTicketId(ticketId);}再通过MethodToolCallbackProvider暴露工具:
@BeanpublicToolCallbackProvidergetTicketInfo(TicketServiceticketService){returnMethodToolCallbackProvider.builder().toolObjects(ticketService).build();}客户端侧加入 MCP Client 依赖,并配置 STDIO 服务:
{"mcpServers":{"ticket-service":{"command":"java","args":["-Dspring.ai.mcp.server.stdio=true","-Dlogging.pattern.console=","-Dfile.encoding=UTF-8","-jar","ticket-service/target/ticket-service-1.0-SNAPSHOT.jar"]}}}聊天接口中需要把chatId传给模型,让工具创建工单时能关联会话:
returnthis.chatClient.prompt().system(builder->builder.text("当前会话ID:%s.".formatted(chatId))).user(prompt).advisors(spec->spec.param(ChatMemory.CONVERSATION_ID,chatId)).stream().content();最后把工具绑定到ChatClient:
.defaultToolCallbacks(toolCallbackProvider)提示词中应明确规则:如果答案不在知识库中,或者用户要求人工客服、人工顾问,就创建工单,并告知用户等待专业课程顾问跟进。
敏感词与安全引导
咨询系统还需要处理敏感内容。第一层可以使用SafeGuardAdvisor做关键词拦截:
.defaultAdvisors(newSafeGuardAdvisor(List.of("公务员","政府")))如果默认回复不符合业务语气,可以自定义 Advisor,修改失败响应,例如:
privatestaticfinalStringDEFAULT_FAILURE_RESPONSE="这个问题我暂时解答不了,我们聊点别的吧";第二层是模型自身的语义级安全识别。很多高风险问题即使不包含显式关键词,模型也能通过语义判断识别出来,比如暴力、违法、网络攻击、自伤等请求。更稳妥的做法是将应用层关键词过滤与模型层语义识别结合:
- 应用层负责处理业务敏感词,并触发工单或人工流程。
- 模型层负责兜底法律、伦理和高危安全问题。
- 系统提示词中明确助手职责,减少越界回答。
- 对误拦截记录 request id,方便向模型服务商反馈。
聊天记忆持久化
如果使用默认内存存储,服务重启后会话上下文和会话列表都会丢失。这对生产环境不可接受。
Spring AI 提供了多种聊天记忆存储方式,包括InMemoryChatMemoryRepository、JdbcChatMemoryRepository、CassandraChatMemoryRepository、Neo4jChatMemoryRepository。在已有 MySQL 环境下,JDBC 是最轻量的选择。
加入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId></dependency>配置自动建表:
spring:ai:chat:memory:repository:jdbc:initialize-schema:alwaysschema:classpath:/sql/schema-mysql.sql表结构示例:
CREATETABLEIFNOTEXISTSSPRING_AI_CHAT_MEMORY(conversation_idVARCHAR(36)NOTNULL,contentTEXTNOTNULL,typeVARCHAR(10)NOTNULL,timestampTIMESTAMPNOTNULL,INDEXSPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX(conversation_id,timestamp));使用 JDBC 仓库构建聊天记忆:
@BeanChatMemorychatMemory(JdbcChatMemoryRepositorychatMemoryRepository){returnMessageWindowChatMemory.builder().maxMessages(10).chatMemoryRepository(chatMemoryRepository).build();}会话列表也建议从内存Map改为数据库表,例如chat_sessions:
CREATETABLEchat_sessions(idINTNOTNULLAUTO_INCREMENT,chat_idVARCHAR(36)NOTNULL,titleVARCHAR(127)NOTNULLDEFAULT'新会话',created_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,updated_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(id));再通过 MyBatis 或 MyBatis-Plus 实现JdbcChatHistoryRepository,替换原来的内存实现。测试时重启服务,确认历史会话和上下文仍然可以恢复。
总结
这套智能咨询系统把 Spring AI 的多个关键能力组合到了一起:
ChatClient负责统一对话入口。MessageChatMemoryAdvisor或PromptChatMemoryAdvisor负责多轮记忆。- RAG 解决垂直业务知识不足的问题。
- Redis Vector Store 解决向量数据持久化问题。
- 重排序提升检索上下文质量。
- MCP 将模型连接到工单系统。
- 安全 Advisor 和模型语义识别共同完成内容防护。
- JDBC 持久化让会话数据具备生产可用性。
真正完整的 AI 应用,不只是“模型能回答”,而是要形成一条可靠链路:能查知识,能记上下文,能调用工具,能处理风险,能持久保存数据,也能在无法解决时把问题交给人工流程。这样,AI 才能从演示能力变成可落地的业务系统。