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 Boot 3.5.3、Spring AI 1.0.1,并接入 Spring AI Alibaba。
父级 pom.xml 统一管理 Spring AI 版本:
xml
<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:
xml
<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>
基础配置如下:
yaml
server:
port: 8081
spring:
application:
name: spring-chat-bot
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
聊天客户端可以统一在配置类中创建:
java
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
}
@Bean
public ChatClient dashscopeChatClient(DashScopeChatModel chatModel, ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你叫小特,是比特教育研发的智能 AI 助手,擅长 Java 和 C++,
主要工作是解决学生在学习过程中遇到的问题
""")
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
启动后访问 http://127.0.0.1:8081/index.html,先确认基础聊天能力可用。
构建 RAG 知识库
由于垂直业务的公开资料有限,模型仅靠通用知识很难准确回答课程细节。解决方式是引入企业内部文档,将其转成可检索的向量知识库,再把召回内容作为上下文交给模型。
RAG 构建流程包括四步:
- 加载 Markdown 文档。
- 对长文本进行切分。
- 为文本块补充关键词元信息。
- 写入向量数据库。
文档可以放在 resources/bit 目录下,通过 MarkdownDocumentReader 加载:
java
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", fileName)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
List<Document> documents = reader.get();
文本分割的重点是避免把语义完整的一句话切断。可以在参考 TokenTextSplitter 的基础上,按中文标点和换行位置做截断,让每个文本块既不超出模型上下文限制,又尽量保持语义完整。
完成切分后,再使用 KeywordMetadataEnricher 为每个文本块生成关键词:
java
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
.keywordCount(5)
.build();
return enricher.apply(documents);
初始阶段可以使用 SimpleVectorStore:
java
@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
最后通过初始化组件把流程串起来:
java
@PostConstruct
public void initData() {
List<Document> documentList = documentLoader.loadMarkdowns();
List<Document> tokenDocuments = splitter.apply(documentList);
List<Document> enrichDocument = keywordEnricher.enrich(tokenDocuments);
vectorStore.add(enrichDocument);
}
将知识库绑定到 ChatClient
知识库构建完成后,需要通过 QuestionAnswerAdvisor 把检索结果注入对话上下文。
提示词的设计非常关键。它要告诉模型:如果上下文里有答案,就直接回答;如果没有答案,就引导用户联系专业顾问;回答时不要反复出现"根据上下文"这类冗余表达。
java
QuestionAnswerAdvisor questionAnswerAdvisor =
QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(promptTemplate)
.build();
return ChatClient.builder(chatModel)
.defaultSystem("""
你是一名专业的企业培训课程咨询助手,代表【比特就业课】为客户提供课程咨询服务。
你的职责是准确、礼貌、高效地解答客户关于比特就业课培训课程的各类问题。
""")
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
.defaultAdvisors(questionAnswerAdvisor)
.build();
为了让配置更清晰,可以把 Advisor 创建逻辑抽到 AdvisorFactory 中,后续增加重排序、安全策略或 MCP 工具时,ChatClient 的结构会更容易维护。
使用多线程优化文档处理
在知识库初始化过程中,"补充关键词元信息"需要调用大模型,是最容易拖慢启动的环节。文档数量较多时,整体耗时可能达到数分钟。
优化思路是将文档分批,并用线程池并发处理。主线程通过 CountDownLatch 等待所有批次完成:
java
private final ExecutorService executorService = Executors.newFixedThreadPool(8);
public void processDocuments(List<Document> documents, int batchSize) {
List<List<Document>> batches = splitToBatches(documents, batchSize);
CountDownLatch latch = new CountDownLatch(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,并把数据初始化流程做成可配置:
yaml
data:
is-load: true
初始化时判断配置:
java
@Value("${data.is-load}")
private boolean isLoad;
@PostConstruct
public void initData() {
if (!isLoad) {
log.info("知识库文档无需加载");
return;
}
// 执行文档加载流程
}
加入 Redis 向量库依赖后,配置索引名称和 key 前缀:
yaml
spring:
ai:
vectorstore:
redis:
initialize-schema: true
index-name: bit-chat-bot
prefix: "rag:"
data:
redis:
url: redis://127.0.0.1:6379
此时删除原来的 SimpleVectorStore Bean,Spring 会根据依赖和配置自动注入 Redis 版本的 VectorStore。
引入重排序提升检索质量
向量检索负责快速召回候选文档,但召回结果的顺序不一定最适合最终回答。可以在初步检索后加入重排序模型,对候选内容重新打分,再把更相关的内容交给大模型。
Spring AI 中可以使用 RetrievalRerankAdvisor:
java
public static Advisor createRerankAdvisor(VectorStore vectorStore, RerankModel rerankModel) {
return new RetrievalRerankAdvisor(
vectorStore,
rerankModel,
SearchRequest.builder().topK(100).build()
);
}
绑定到 ChatClient:
java
.defaultAdvisors(AdvisorFactory.createQuestionAnswerAdvisor(vectorStore))
.defaultAdvisors(AdvisorFactory.createRerankAdvisor(vectorStore, rerankModel))
如果使用 DashScope 的重排序模型,还可以配置返回数量:
yaml
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 暴露能力:
java
@Tool(description = "根据提供的信息创建工单")
public String createTicket(
@ToolParam(description = "工单标题,不能为空") String title,
@ToolParam(description = "工单详细描述,不能为空") String description,
@ToolParam(description = "工单关联的会话ID,不能为空") String relatedChatId) {
// 参数校验、写入数据库、返回工单号
}
@Tool(description = "根据工单ID查询工单信息")
public TicketInfo queryTicket(
@ToolParam(description = "工单ID,不能为空") String ticketId) {
return ticketMapper.selectByTicketId(ticketId);
}
再通过 MethodToolCallbackProvider 暴露工具:
java
@Bean
public ToolCallbackProvider getTicketInfo(TicketService ticketService) {
return MethodToolCallbackProvider.builder()
.toolObjects(ticketService)
.build();
}
客户端侧加入 MCP Client 依赖,并配置 STDIO 服务:
json
{
"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 传给模型,让工具创建工单时能关联会话:
java
return this.chatClient.prompt()
.system(builder -> builder.text("当前会话ID:%s.".formatted(chatId)))
.user(prompt)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
最后把工具绑定到 ChatClient:
java
.defaultToolCallbacks(toolCallbackProvider)
提示词中应明确规则:如果答案不在知识库中,或者用户要求人工客服、人工顾问,就创建工单,并告知用户等待专业课程顾问跟进。
敏感词与安全引导
咨询系统还需要处理敏感内容。第一层可以使用 SafeGuardAdvisor 做关键词拦截:
java
.defaultAdvisors(new SafeGuardAdvisor(List.of("公务员", "政府")))
如果默认回复不符合业务语气,可以自定义 Advisor,修改失败响应,例如:
java
private static final String DEFAULT_FAILURE_RESPONSE =
"这个问题我暂时解答不了,我们聊点别的吧";
第二层是模型自身的语义级安全识别。很多高风险问题即使不包含显式关键词,模型也能通过语义判断识别出来,比如暴力、违法、网络攻击、自伤等请求。更稳妥的做法是将应用层关键词过滤与模型层语义识别结合:
- 应用层负责处理业务敏感词,并触发工单或人工流程。
- 模型层负责兜底法律、伦理和高危安全问题。
- 系统提示词中明确助手职责,减少越界回答。
- 对误拦截记录 request id,方便向模型服务商反馈。
聊天记忆持久化
如果使用默认内存存储,服务重启后会话上下文和会话列表都会丢失。这对生产环境不可接受。
Spring AI 提供了多种聊天记忆存储方式,包括 InMemoryChatMemoryRepository、JdbcChatMemoryRepository、CassandraChatMemoryRepository、Neo4jChatMemoryRepository。在已有 MySQL 环境下,JDBC 是最轻量的选择。
加入依赖:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
配置自动建表:
yaml
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
schema: classpath:/sql/schema-mysql.sql
表结构示例:
sql
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL,
content TEXT NOT NULL,
type VARCHAR(10) NOT NULL,
timestamp TIMESTAMP NOT NULL,
INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX (conversation_id, timestamp)
);
使用 JDBC 仓库构建聊天记忆:
java
@Bean
ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.maxMessages(10)
.chatMemoryRepository(chatMemoryRepository)
.build();
}
会话列表也建议从内存 Map 改为数据库表,例如 chat_sessions:
sql
CREATE TABLE chat_sessions (
id INT NOT NULL AUTO_INCREMENT,
chat_id VARCHAR(36) NOT NULL,
title VARCHAR(127) NOT NULL DEFAULT '新会话',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
再通过 MyBatis 或 MyBatis-Plus 实现 JdbcChatHistoryRepository,替换原来的内存实现。测试时重启服务,确认历史会话和上下文仍然可以恢复。
总结
这套智能咨询系统把 Spring AI 的多个关键能力组合到了一起:
ChatClient负责统一对话入口。MessageChatMemoryAdvisor或PromptChatMemoryAdvisor负责多轮记忆。- RAG 解决垂直业务知识不足的问题。
- Redis Vector Store 解决向量数据持久化问题。
- 重排序提升检索上下文质量。
- MCP 将模型连接到工单系统。
- 安全 Advisor 和模型语义识别共同完成内容防护。
- JDBC 持久化让会话数据具备生产可用性。
真正完整的 AI 应用,不只是"模型能回答",而是要形成一条可靠链路:能查知识,能记上下文,能调用工具,能处理风险,能持久保存数据,也能在无法解决时把问题交给人工流程。这样,AI 才能从演示能力变成可落地的业务系统。