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

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

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

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

系统整体设计

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

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

主要功能包括:

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

项目初始化与模型接入

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

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

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

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

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

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

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

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

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

聊天记忆持久化

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

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

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

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

相关推荐
小程故事多_801 小时前
AI软件工程范式革命,终结五十年的“手工伪工程”时代
人工智能·软件工程
刘一说1 小时前
AI科技热点日报 | 2026年6月14日
人工智能·科技
诺***帝1 小时前
GPT-Image-2架构深度拆解:2026年图像生成模型技术教程
人工智能·gpt
java1234_小锋1 小时前
Spring Boot 中 Starter 是什么?它的核心规范有哪些?请说明如何自定义一个 Starter。
java·spring boot·后端
良枫1 小时前
自进化 agent:核心模块一任务规划器 Planner
java·服务器·windows
安逸sgr1 小时前
《图解机器学习-第三章》:训练、验证、测试:三分数据,缺一不可!
人工智能·深度学习·机器学习·计算机视觉
湘美书院--湘美谈教育1 小时前
湘美谈教育湘美书院考古教育系列:湖湘一万年序列整理研究
大数据·人工智能·深度学习·神经网络·机器学习
星辰AI打工人1 小时前
Gemma 4 E2B LoRA 微调全记录:从 ROCm 环境搭建到 Ollama 本地部署
人工智能
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第114题】【并发篇】第14题:说一下悲观锁的优点和缺点?
java·开发语言·面试