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

张开发
2026/6/15 6:56:22 15 分钟阅读

分享文章

Spring AI 智能咨询系统综合实战
Spring AI 智能咨询系统实战RAG、MCP、安全与持久化一体化落地一个真正可用的 AI 咨询系统不能只停留在“用户问一句模型答一句”。它需要记住会话上下文能基于企业知识库回答问题遇到无法处理的需求时能转交外部系统还要具备安全拦截和数据持久化能力。这里以“比特就业课”咨询场景为例搭建一套面向课程咨询的智能聊天系统。整体目标是提供 Web 聊天入口支持流式回复、会话管理、RAG 知识库问答、敏感词引导、MCP 工单调用以及 MySQL 持久化。系统整体设计系统由两个核心服务组成服务职责chat-bot-service对外提供 Web 服务默认端口8081负责聊天接口、会话管理、RAG 检索、安全引导和 MCP 客户端调用ticket-serviceMCP 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 版本propertiesspring-ai.version1.0.1/spring-ai.version/propertiesdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementchat-bot-service中引入 Web、WebFlux 和 DashScopedependencygroupIdcom.alibaba.cloud.ai/groupIdartifactIdspring-ai-alibaba-starter-dashscope/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-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加载MarkdownDocumentReaderConfigconfigMarkdownDocumentReaderConfig.builder().withHorizontalRuleCreateDocument(true).withIncludeCodeBlock(false).withIncludeBlockquote(false).withAdditionalMetadata(filename,fileName).build();MarkdownDocumentReaderreadernewMarkdownDocumentReader(resource,config);ListDocumentdocumentsreader.get();文本分割的重点是避免把语义完整的一句话切断。可以在参考TokenTextSplitter的基础上按中文标点和换行位置做截断让每个文本块既不超出模型上下文限制又尽量保持语义完整。完成切分后再使用KeywordMetadataEnricher为每个文本块生成关键词KeywordMetadataEnricherenricherKeywordMetadataEnricher.builder(chatModel).keywordCount(5).build();returnenricher.apply(documents);初始阶段可以使用SimpleVectorStoreBeanpublicVectorStorevectorStore(DashScopeEmbeddingModelembeddingModel){returnSimpleVectorStore.builder(embeddingModel).build();}最后通过初始化组件把流程串起来PostConstructpublicvoidinitData(){ListDocumentdocumentListdocumentLoader.loadMarkdowns();ListDocumenttokenDocumentssplitter.apply(documentList);ListDocumentenrichDocumentkeywordEnricher.enrich(tokenDocuments);vectorStore.add(enrichDocument);}将知识库绑定到 ChatClient知识库构建完成后需要通过QuestionAnswerAdvisor把检索结果注入对话上下文。提示词的设计非常关键。它要告诉模型如果上下文里有答案就直接回答如果没有答案就引导用户联系专业顾问回答时不要反复出现“根据上下文”这类冗余表达。QuestionAnswerAdvisorquestionAnswerAdvisorQuestionAnswerAdvisor.builder(vectorStore).promptTemplate(promptTemplate).build();returnChatClient.builder(chatModel).defaultSystem( 你是一名专业的企业培训课程咨询助手代表【比特就业课】为客户提供课程咨询服务。 你的职责是准确、礼貌、高效地解答客户关于比特就业课培训课程的各类问题。 ).defaultAdvisors(newSimpleLoggerAdvisor()).defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()).defaultAdvisors(questionAnswerAdvisor).build();为了让配置更清晰可以把 Advisor 创建逻辑抽到AdvisorFactory中后续增加重排序、安全策略或 MCP 工具时ChatClient的结构会更容易维护。使用多线程优化文档处理在知识库初始化过程中“补充关键词元信息”需要调用大模型是最容易拖慢启动的环节。文档数量较多时整体耗时可能达到数分钟。优化思路是将文档分批并用线程池并发处理。主线程通过CountDownLatch等待所有批次完成privatefinalExecutorServiceexecutorServiceExecutors.newFixedThreadPool(8);publicvoidprocessDocuments(ListDocumentdocuments,intbatchSize){ListListDocumentbatchessplitToBatches(documents,batchSize);CountDownLatchlatchnewCountDownLatch(batches.size());for(ListDocumentbatch:batches){executorService.submit(()-{try{ListDocumentenrichkeywordEnricher.enrich(batch);vectorStore.add(enrich);}finally{latch.countDown();}});}latch.await(15,TimeUnit.MINUTES);}这里要注意每个批次只处理自己的Document集合避免多个线程同时修改同一对象。对于真实生产环境还要考虑模型服务限流、失败重试和批次状态记录。从内存向量库切换到 RedisSimpleVectorStore适合本地验证但存在两个明显问题向量数据在内存中服务重启后会丢失。每次重启都要重新加载、切分、向量化和写入启动效率低。解决方案是切换到 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此时删除原来的SimpleVectorStoreBeanSpring 会根据依赖和配置自动注入 Redis 版本的VectorStore。引入重排序提升检索质量向量检索负责快速召回候选文档但召回结果的顺序不一定最适合最终回答。可以在初步检索后加入重排序模型对候选内容重新打分再把更相关的内容交给大模型。Spring AI 中可以使用RetrievalRerankAdvisorpublicstaticAdvisorcreateRerankAdvisor(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.stdiotrue,-Dlogging.pattern.console,-Dfile.encodingUTF-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 是最轻量的选择。加入依赖dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-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_sessionsCREATETABLEchat_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 才能从演示能力变成可落地的业务系统。

更多文章