news 2026/6/15 6:56:20

Spring AI 智能咨询系统综合实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 智能咨询系统综合实战

Spring AI 智能咨询系统实战:RAG、MCP、安全与持久化一体化落地

一个真正可用的 AI 咨询系统,不能只停留在“用户问一句,模型答一句”。它需要记住会话上下文,能基于企业知识库回答问题,遇到无法处理的需求时能转交外部系统,还要具备安全拦截和数据持久化能力。

这里以“比特就业课”咨询场景为例,搭建一套面向课程咨询的智能聊天系统。整体目标是:提供 Web 聊天入口,支持流式回复、会话管理、RAG 知识库问答、敏感词引导、MCP 工单调用,以及 MySQL 持久化。

系统整体设计

系统由两个核心服务组成:

服务职责
chat-bot-service对外提供 Web 服务,默认端口8081,负责聊天接口、会话管理、RAG 检索、安全引导和 MCP 客户端调用
ticket-serviceMCP Server,无 Web 接口,通过 STDIO 启动,负责暴露创建工单、查询工单等工具能力,并将数据写入 MySQL

主要功能包括:

  1. 用户通过 Web 页面与机器人对话,消息支持流式返回。
  2. 支持创建新会话、查看历史会话、查看会话消息、删除会话。
  3. 对话上下文通过JdbcChatMemoryRepository存入 MySQL。
  4. 基于企业介绍和方向文档构建 RAG 知识库。
  5. 当知识库无法回答,或用户要求人工服务时,通过 MCP 创建工单。
  6. 对敏感词和高风险内容进行拦截或安全引导。

项目初始化与模型接入

项目可以命名为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 构建流程包括四步:

  1. 加载 Markdown 文档。
  2. 对长文本进行切分。
  3. 为文本块补充关键词元信息。
  4. 写入向量数据库。

文档可以放在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适合本地验证,但存在两个明显问题:

  1. 向量数据在内存中,服务重启后会丢失。
  2. 每次重启都要重新加载、切分、向量化和写入,启动效率低。

解决方案是切换到 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,核心工具包括:

  1. 创建工单。
  2. 根据工单 ID 查询工单。

MySQL 表可以设计为ticket_info,字段包括ticket_idtitledescriptionrelated_chat_idstatuscreatorassigneecreated_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="这个问题我暂时解答不了,我们聊点别的吧";

第二层是模型自身的语义级安全识别。很多高风险问题即使不包含显式关键词,模型也能通过语义判断识别出来,比如暴力、违法、网络攻击、自伤等请求。更稳妥的做法是将应用层关键词过滤与模型层语义识别结合:

  1. 应用层负责处理业务敏感词,并触发工单或人工流程。
  2. 模型层负责兜底法律、伦理和高危安全问题。
  3. 系统提示词中明确助手职责,减少越界回答。
  4. 对误拦截记录 request id,方便向模型服务商反馈。

聊天记忆持久化

如果使用默认内存存储,服务重启后会话上下文和会话列表都会丢失。这对生产环境不可接受。

Spring AI 提供了多种聊天记忆存储方式,包括InMemoryChatMemoryRepositoryJdbcChatMemoryRepositoryCassandraChatMemoryRepositoryNeo4jChatMemoryRepository。在已有 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 的多个关键能力组合到了一起:

  1. ChatClient负责统一对话入口。
  2. MessageChatMemoryAdvisorPromptChatMemoryAdvisor负责多轮记忆。
  3. RAG 解决垂直业务知识不足的问题。
  4. Redis Vector Store 解决向量数据持久化问题。
  5. 重排序提升检索上下文质量。
  6. MCP 将模型连接到工单系统。
  7. 安全 Advisor 和模型语义识别共同完成内容防护。
  8. JDBC 持久化让会话数据具备生产可用性。

真正完整的 AI 应用,不只是“模型能回答”,而是要形成一条可靠链路:能查知识,能记上下文,能调用工具,能处理风险,能持久保存数据,也能在无法解决时把问题交给人工流程。这样,AI 才能从演示能力变成可落地的业务系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 6:56:15

HFSS新手避坑指南:手把手教你用FR4板材设计2.45GHz侧馈微带天线

HFSS实战避坑指南&#xff1a;FR4板材2.45GHz微带天线设计全解析刚接触HFSS的天线设计新手&#xff0c;往往会在仿真阶段遇到各种"灵异现象"——谐振频率莫名偏移、方向图畸变、匹配失效。本文将以2.45GHz侧馈微带天线为例&#xff0c;拆解七个关键设计环节中的典型误…

作者头像 李华
网站建设 2026/6/15 6:54:42

STM32F103C8T6软件SPI驱动MAX6675避坑指南:为什么硬件SPI不行?

STM32F103C8T6与MAX6675的SPI通信困境&#xff1a;为什么硬件方案行不通&#xff1f; 当你在STM32F103C8T6上尝试用硬件SPI驱动MAX6675热电偶转换器时&#xff0c;是否遇到过数据读取失败的情况&#xff1f;这不是个例。许多开发者都曾在这个看似简单的接口问题上耗费数小时调试…

作者头像 李华
网站建设 2026/6/15 6:47:54

别慌!MCU死机后,用Ozone和Keil这招非侵入式调试,5分钟定位HardFault

MCU死机急救指南&#xff1a;用Ozone与Keil实现非侵入式HardFault定位当嵌入式设备在现场突然死机时&#xff0c;那种冷汗直流的体验每个工程师都懂。上周我的智能家居控制器在客户演示时突然卡死&#xff0c;屏幕定格在开机画面——典型的HardFault症状。传统方法需要重新烧录…

作者头像 李华
网站建设 2026/6/15 6:45:54

Sqribble电子书自动化排版原理与工程化实践

1. 项目概述&#xff1a;这不是“一键生成”&#xff0c;而是一套被精心封装的出版流水线你有没有过这种经历&#xff1a;手头有一篇写得不错的博客文章&#xff0c;或者一份整理好的课程讲义&#xff0c;突然需要把它变成一本像模像样的电子书——用来当知识付费产品的赠品、做…

作者头像 李华