SpringAI 实战:搭建企业级智能客服系统

一、前言:为什么选择SpringAI构建智能客服?

在大模型爆发的时代,智能客服已从"可选功能"成为企业服务的"标配"。传统客服系统依赖固定规则和人工知识库,存在响应慢、意图识别不准、维护成本高等痛点;而基于大模型的智能客服能通过自然语言理解(NLU)实现精准意图识别、多轮对话和个性化回复,但面临大模型集成复杂、知识库联动难、分布式部署繁琐等问题。

SpringAI的出现彻底解决了这些痛点------作为Spring生态官方推出的AI开发框架,它将大模型调用、向量存储、知识库管理等能力封装为标准化组件,完美兼容Spring Boot、Spring Cloud等现有生态,让Java开发者无需深入学习大模型底层API,就能快速构建高可用、可扩展的智能客服系统。

本文将从底层逻辑到实战落地,手把手教你用SpringAI搭建企业级智能客服,涵盖数据库设计、核心模块开发、大模型集成、知识库检索、性能优化全流程。

二、核心技术栈选型

技术组件 版本号 作用说明
JDK 17.0.11 基础开发环境
Spring Boot 3.2.10 快速构建Spring应用
Spring AI 1.0.7 大模型集成、向量处理核心框架
Spring Doc OpenAPI(Swagger3) 2.3.0 接口文档自动生成
MyBatis-Plus 3.5.5 持久层框架,简化CRUD操作
MySQL 8.0.40 存储用户信息、对话历史、知识库基础数据
Redis 7.4.0 缓存热门问题、对话上下文
Fastjson2 2.0.57 JSON序列化/反序列化
Lombok 1.18.30 简化JavaBean代码
Google Guava 33.2.1 集合工具类增强
Spring Retry 1.3.5 大模型调用重试机制
Docker 27.0.3 容器化部署

三、底层逻辑剖析:SpringAI智能客服工作流程

3.1 核心工作原理

智能客服的核心是"理解用户意图→匹配知识库→生成精准回复",SpringAI通过三大核心组件实现这一流程:

  1. ChatClient:统一封装大模型API(支持OpenAI、Azure OpenAI、本地化模型等),提供标准化对话接口;

  2. EmbeddingClient:将文本(用户提问、知识库内容)转换为向量,实现语义级检索;

  3. VectorStore:存储向量数据,支持高效相似度匹配(本文使用SpringAI内置的InMemoryVectorStore,生产环境可替换为Milvus、Pinecone等专业向量数据库)。

3.2 完整流程图

3.3 关键技术区分:SpringAI vs 传统大模型集成

对比维度 传统大模型集成 SpringAI集成
开发成本 需手动封装大模型API、处理鉴权/超时/重试 标准化接口,自动处理鉴权、重试、异常封装
生态兼容性 与Spring生态整合繁琐(需自定义配置) 原生兼容Spring Boot/Cloud,支持依赖注入
向量处理 需手动集成第三方Embedding库 内置EmbeddingClient,支持多模型切换
扩展性 新增大模型需重构代码 基于SPI机制,新增模型仅需配置依赖
运维成本 需单独维护大模型调用逻辑 与Spring应用统一运维,支持监控/链路追踪

四、实战开发:从0搭建智能客服系统

4.1 环境准备

  1. JDK17安装:确保本地环境为JDK17;

  2. MySQL8.0配置 :创建数据库 springai-cs,字符集 utf8mb4

  3. Redis7.0+:本地启动Redis(默认端口6379,无密码);

  4. 大模型API密钥:本文使用OpenAI GPT-3.5-turbo(需申请API Key),也可替换为Azure OpenAI、通义千问等。

4.2 项目初始化(Maven配置)

创建Spring Boot项目,pom.xml 核心依赖如下:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.10</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.demo</groupId>
    <artifactId>springai-cs-demo</artifactId>
    <version>1.0.0</version>
    <name>springai-cs-demo</name>
    <description>SpringAI Enterprise Intelligent Customer Service Demo</description>
    
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.7</spring-ai.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <fastjson2.version>2.0.57</fastjson2.version>
        <guava.version>33.2.1</guava.version>
        <lombok.version>1.18.30</lombok.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-transaction</artifactId>
        </dependency>
        
        <!-- Spring AI核心依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-embedding</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-vector-store</artifactId>
        </dependency>
        
        <!-- 持久层依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- 工具类依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        
        <!-- Swagger3文档 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.3.0</version>
        </dependency>
        
        <!-- 重试机制 -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.3.5</version>
        </dependency>
        
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <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>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.3 配置文件(application.yml)

复制代码
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/springai-cs?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
    username: root
    password: root123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  # Redis配置
  redis:
    host: localhost
    port: 6379
    timeout: 3000ms
  # Spring AI OpenAI配置
  ai:
    openai:
      api-key: sk-your-openai-api-key # 替换为你的OpenAI API Key
      base-url: https://api.openai.com/v1
      chat:
        model: gpt-3.5-turbo
        temperature: 0.3 # 控制回复随机性,0.3表示更精准
      embedding:
        model: text-embedding-ada-002

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.jam.demo.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: isDeleted
      logic-delete-value: 1
      logic-not-delete-value: 0

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /cs

# 日志配置
logging:
  level:
    root: INFO
    com.jam.demo: DEBUG
    org.springframework.ai: INFO

4.4 数据库设计(MySQL8.0)

创建4张核心表:用户表、对话记录表、知识库表、意图表,SQL如下:

复制代码
-- 用户表(存储客服系统用户信息)
CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`),
  KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 知识库表(存储客服问答知识库)
CREATE TABLE `cs_knowledge` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '知识库ID',
  `question` varchar(500) NOT NULL COMMENT '问题',
  `answer` text NOT NULL COMMENT '答案',
  `category` varchar(50) NOT NULL COMMENT '分类(如:账号问题、订单问题)',
  `hit_count` int NOT NULL DEFAULT '0' COMMENT '命中次数',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
  PRIMARY KEY (`id`),
  KEY `idx_category` (`category`),
  FULLTEXT KEY `ft_question` (`question`) COMMENT '问题全文索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库表';

-- 对话记录表(存储用户与客服的对话历史)
CREATE TABLE `cs_chat_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `user_question` text NOT NULL COMMENT '用户提问',
  `ai_answer` text NOT NULL COMMENT 'AI回复',
  `knowledge_ids` varchar(200) DEFAULT NULL COMMENT '关联知识库ID(多个用逗号分隔)',
  `chat_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '对话时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_chat_time` (`chat_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话记录表';

-- 意图表(存储用户意图分类)
CREATE TABLE `cs_intent` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '意图ID',
  `intent_name` varchar(50) NOT NULL COMMENT '意图名称(如:查询订单、修改密码)',
  `intent_desc` varchar(200) DEFAULT NULL COMMENT '意图描述',
  `keywords` varchar(500) NOT NULL COMMENT '关键词(多个用逗号分隔)',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_intent_name` (`intent_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='意图表';

-- 初始化知识库数据
INSERT INTO `cs_knowledge` (`question`, `answer`, `category`) VALUES
('如何修改密码?', '修改密码步骤:1.登录个人中心;2.点击账号安全;3.输入原密码和新密码;4.提交保存。', '账号问题'),
('订单多久发货?', '普通商品下单后48小时内发货,预售商品按预售页面标注时间发货,节假日顺延。', '订单问题'),
('如何申请退款?', '退款申请步骤:1.进入我的订单;2.找到对应订单点击申请退款;3.选择退款原因;4.提交审核,审核通过后3-7个工作日到账。', '退款问题'),
('商品质量有问题怎么办?', '若收到商品存在质量问题,请在收货后7天内联系在线客服,提供商品照片和订单号,客服将协助办理退换货。', '售后问题');

-- 初始化意图数据
INSERT INTO `cs_intent` (`intent_name`, `intent_desc`, `keywords`) VALUES
('修改密码', '用户需要修改账号密码', '修改密码,密码修改,更改密码,重置密码'),
('查询发货', '用户查询订单发货时间', '发货时间,多久发货,发货状态,物流信息'),
('申请退款', '用户申请订单退款', '退款,申请退款,退款申请,退钱'),
('质量问题', '用户反馈商品质量问题', '质量问题,商品质量,瑕疵,损坏,故障');

4.5 核心配置类

4.5.1 Swagger3配置(OpenApiConfig.java)
复制代码
package com.jam.demo.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Swagger3配置类
 * @author ken
 */
@Configuration
public class OpenApiConfig {

    /**
     * 配置接口文档基本信息
     */
    @Bean
    public OpenAPI springAiCsOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("SpringAI智能客服系统API文档")
                        .description("基于SpringAI构建的企业级智能客服系统,提供用户对话、知识库查询等接口")
                        .version("1.0.0")
                        .contact(new Contact()
                                .name("智能客服研发团队")
                                .description("如有问题请联系技术支持")));
    }
}
4.5.2 MyBatis-Plus配置(MyBatisPlusConfig.java)
复制代码
package com.jam.demo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus配置类(分页插件)
 * @author ken
 */
@Configuration
public class MyBatisPlusConfig {

    /**
     * 注册分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加MySQL分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
4.5.3 SpringAI向量存储配置(VectorStoreConfig.java)
复制代码
package com.jam.demo.config;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.inmemory.InMemoryVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * SpringAI向量存储配置(基于内存向量库,生产环境可替换为Milvus等)
 * @author ken
 */
@Configuration
public class VectorStoreConfig {

    /**
     * 配置EmbeddingClient(文本转向量)
     */
    @Bean
    public EmbeddingClient embeddingClient(OpenAiApi openAiApi) {
        // 使用text-embedding-ada-002模型,维度1536
        return new OpenAiEmbeddingClient(openAiApi,
                OpenAiEmbeddingOptions.builder()
                        .withModel("text-embedding-ada-002")
                        .withDimensions(1536)
                        .build());
    }

    /**
     * 配置向量存储(InMemoryVectorStore适合开发/测试,生产用专业向量数据库)
     */
    @Bean
    public VectorStore vectorStore(EmbeddingClient embeddingClient) {
        return new InMemoryVectorStore(embeddingClient);
    }
}

4.6 实体类设计

4.6.1 用户实体(SysUser.java)
复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体
 * @author ken
 */
@Data
@TableName("sys_user")
@Schema(description = "用户实体")
public class SysUser {

    @TableId(type = IdType.AUTO)
    @Schema(description = "用户ID")
    private Long id;

    @Schema(description = "用户名", required = true)
    private String username;

    @Schema(description = "手机号")
    private String phone;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除(0-正常,1-删除)")
    private Integer isDeleted;
}
4.6.2 知识库实体(CsKnowledge.java)
复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 知识库实体
 * @author ken
 */
@Data
@TableName("cs_knowledge")
@Schema(description = "知识库实体")
public class CsKnowledge {

    @TableId(type = IdType.AUTO)
    @Schema(description = "知识库ID")
    private Long id;

    @Schema(description = "问题", required = true)
    private String question;

    @Schema(description = "答案", required = true)
    private String answer;

    @Schema(description = "分类", required = true)
    private String category;

    @Schema(description = "命中次数")
    private Integer hitCount;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除(0-正常,1-删除)")
    private Integer isDeleted;
}
4.6.3 对话记录实体(CsChatRecord.java)
复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 对话记录实体
 * @author ken
 */
@Data
@TableName("cs_chat_record")
@Schema(description = "对话记录实体")
public class CsChatRecord {

    @TableId(type = IdType.AUTO)
    @Schema(description = "记录ID")
    private Long id;

    @Schema(description = "用户ID", required = true)
    private Long userId;

    @Schema(description = "用户提问", required = true)
    private String userQuestion;

    @Schema(description = "AI回复", required = true)
    private String aiAnswer;

    @Schema(description = "关联知识库ID(多个用逗号分隔)")
    private String knowledgeIds;

    @Schema(description = "对话时间")
    private LocalDateTime chatTime;

    @Schema(description = "逻辑删除(0-正常,1-删除)")
    private Integer isDeleted;
}
4.6.4 意图实体(CsIntent.java)
复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 意图实体
 * @author ken
 */
@Data
@TableName("cs_intent")
@Schema(description = "意图实体")
public class CsIntent {

    @TableId(type = IdType.AUTO)
    @Schema(description = "意图ID")
    private Long id;

    @Schema(description = "意图名称", required = true)
    private String intentName;

    @Schema(description = "意图描述")
    private String intentDesc;

    @Schema(description = "关键词(多个用逗号分隔)", required = true)
    private String keywords;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除(0-正常,1-删除)")
    private Integer isDeleted;
}

4.7 Mapper层设计(MyBatis-Plus)

4.7.1 用户Mapper(SysUserMapper.java)
复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 用户Mapper
 * @author ken
 */
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

    /**
     * 根据用户名查询用户
     * @param username 用户名
     * @return 用户信息
     */
    SysUser selectByUsername(@Param("username") String username);
}
4.7.2 知识库Mapper(CsKnowledgeMapper.java)
复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jam.demo.entity.CsKnowledge;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 知识库Mapper
 * @author ken
 */
@Mapper
public interface CsKnowledgeMapper extends BaseMapper<CsKnowledge> {

    /**
     * 分页查询知识库
     * @param page 分页参数
     * @param category 分类(可为空)
     * @return 分页结果
     */
    IPage<CsKnowledge> selectPageByCategory(Page<CsKnowledge> page, @Param("category") String category);

    /**
     * 根据关键词模糊查询知识库
     * @param keyword 关键词
     * @return 匹配的知识库列表
     */
    List<CsKnowledge> selectByKeyword(@Param("keyword") String keyword);

    /**
     * 批量更新命中次数
     * @param ids 知识库ID列表
     * @return 更新行数
     */
    int batchUpdateHitCount(@Param("ids") List<Long> ids);
}
4.7.3 对话记录Mapper(CsChatRecordMapper.java)
复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jam.demo.entity.CsChatRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 对话记录Mapper
 * @author ken
 */
@Mapper
public interface CsChatRecordMapper extends BaseMapper<CsChatRecord> {

    /**
     * 分页查询用户对话记录
     * @param page 分页参数
     * @param userId 用户ID
     * @return 分页结果
     */
    IPage<CsChatRecord> selectPageByUserId(Page<CsChatRecord> page, @Param("userId") Long userId);
}
4.7.4 意图Mapper(CsIntentMapper.java)
复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.CsIntent;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 意图Mapper
 * @author ken
 */
@Mapper
public interface CsIntentMapper extends BaseMapper<CsIntent> {

    /**
     * 根据关键词匹配意图
     * @param keyword 关键词
     * @return 匹配的意图列表
     */
    List<CsIntent> selectByKeyword(@Param("keyword") String keyword);
}

4.8 Service层设计(核心业务逻辑)

4.8.1 核心DTO(数据传输对象)
复制代码
package com.jam.demo.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.util.StringUtils;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 用户提问DTO
 * @author ken
 */
@Data
@Schema(description = "用户提问DTO")
public class UserQuestionDTO {

    @NotNull(message = "用户ID不能为空")
    @Schema(description = "用户ID", required = true)
    private Long userId;

    @NotBlank(message = "提问内容不能为空")
    @Schema(description = "提问内容", required = true)
    private String question;

    @Schema(description = "对话上下文ID(多轮对话时使用)")
    private String contextId;

    /**
     * 校验参数合法性
     */
    public void validate() {
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("用户ID必须为正整数");
        }
        if (!StringUtils.hasText(question, "提问内容不能为空")) {
            throw new IllegalArgumentException("提问内容不能为空且不能仅包含空格");
        }
        if (question.length() > 500) {
            throw new IllegalArgumentException("提问内容不能超过500字符");
        }
    }
}

package com.jam.demo.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.util.List;

/**
 * AI回复DTO
 * @author ken
 */
@Data
@Schema(description = "AI回复DTO")
public class AiResponseDTO {

    @Schema(description = "回复内容")
    private String answer;

    @Schema(description = "关联知识库信息")
    private List<KnowledgeDTO> relatedKnowledges;

    @Schema(description = "识别的用户意图")
    private String intent;

    @Schema(description = "对话上下文ID(用于多轮对话)")
    private String contextId;

    @Schema(description = "响应时间(毫秒)")
    private Long responseTime;

    /**
     * 关联知识库DTO
     */
    @Data
    @Schema(description = "关联知识库DTO")
    public static class KnowledgeDTO {
        @Schema(description = "知识库ID")
        private Long id;

        @Schema(description = "问题")
        private String question;

        @Schema(description = "答案")
        private String answer;

        @Schema(description = "相似度(0-1)")
        private Float similarity;
    }
}
4.8.2 Service接口(CsService.java)
复制代码
package com.jam.demo.service;

import com.jam.demo.dto.AiResponseDTO;
import com.jam.demo.dto.UserQuestionDTO;
import com.jam.demo.entity.CsChatRecord;
import com.baomidou.mybatisplus.core.metadata.IPage;

/**
 * 智能客服核心服务接口
 * @author ken
 */
public interface CsService {

    /**
     * 处理用户提问,返回AI回复
     * @param questionDTO 用户提问DTO
     * @return AI回复DTO
     */
    AiResponseDTO handleUserQuestion(UserQuestionDTO questionDTO);

    /**
     * 分页查询用户对话记录
     * @param userId 用户ID
     * @param pageNum 页码(从1开始)
     * @param pageSize 每页条数
     * @return 分页对话记录
     */
    IPage<CsChatRecord> queryChatRecordByUserId(Long userId, Integer pageNum, Integer pageSize);

    /**
     * 初始化知识库向量(系统启动时调用)
     */
    void initKnowledgeVector();
}
4.8.3 Service实现类(CsServiceImpl.java)
复制代码
package com.jam.demo.service.impl;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.jam.demo.dto.AiResponseDTO;
import com.jam.demo.dto.UserQuestionDTO;
import com.jam.demo.entity.CsChatRecord;
import com.jam.demo.entity.CsIntent;
import com.jam.demo.entity.CsKnowledge;
import com.jam.demo.mapper.CsChatRecordMapper;
import com.jam.demo.mapper.CsIntentMapper;
import com.jam.demo.mapper.CsKnowledgeMapper;
import com.jam.demo.service.CsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchResult;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 智能客服核心服务实现类
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CsServiceImpl implements CsService {

    // 热门问题缓存前缀(过期时间:2小时)
    private static final String CACHE_HOT_QUESTION_PREFIX = "cs:hot:question:";
    private static final long CACHE_HOT_QUESTION_EXPIRE = 7200;

    // 大模型Prompt模板(结合知识库回答,确保回复精准)
    private static final String PROMPT_TEMPLATE = """
            你是一个专业的智能客服,根据以下知识库内容回答用户问题:
            {knowledge}
            要求:
            1. 优先使用知识库中的答案,不要编造信息;
            2. 如果知识库没有相关内容,回复"非常抱歉,您的问题暂时无法解答,请联系人工客服";
            3. 回复简洁明了,不要超过300字;
            4. 保持友好语气。
            用户问题:{question}
            """;

    private final CsKnowledgeMapper knowledgeMapper;
    private final CsIntentMapper intentMapper;
    private final CsChatRecordMapper chatRecordMapper;
    private final VectorStore vectorStore;
    private final ChatClient chatClient;
    private final StringRedisTemplate redisTemplate;
    private final PlatformTransactionManager transactionManager;

    /**
     * 处理用户提问,核心流程:参数校验→缓存查询→意图识别→向量检索→大模型生成→记录存储
     */
    @Override
    public AiResponseDTO handleUserQuestion(UserQuestionDTO questionDTO) {
        long startTime = System.currentTimeMillis();
        AiResponseDTO responseDTO = new AiResponseDTO();
        String contextId = StringUtils.hasText(questionDTO.getContextId()) 
                ? questionDTO.getContextId() 
                : UUID.randomUUID().toString();
        responseDTO.setContextId(contextId);

        try {
            // 1. 参数校验
            questionDTO.validate();
            Long userId = questionDTO.getUserId();
            String question = questionDTO.getQuestion();
            log.info("处理用户提问:userId={}, question={}, contextId={}", userId, question, contextId);

            // 2. 查询热门问题缓存
            String cacheKey = CACHE_HOT_QUESTION_PREFIX + question.hashCode();
            String cacheAnswer = redisTemplate.opsForValue().get(cacheKey);
            if (StringUtils.hasText(cacheAnswer)) {
                log.info("热门问题缓存命中:question={}", question);
                responseDTO.setAnswer(cacheAnswer);
                responseDTO.setIntent("热门问题");
                responseDTO.setResponseTime(System.currentTimeMillis() - startTime);
                return responseDTO;
            }

            // 3. 意图识别
            String intent = recognizeIntent(question);
            responseDTO.setIntent(intent);
            log.info("用户意图识别结果:intent={}", intent);

            // 4. 向量检索相似知识库(Top3)
            List<SearchResult> searchResults = vectorStore.similaritySearch(question, 3);
            List<AiResponseDTO.KnowledgeDTO> relatedKnowledges = Lists.newArrayList();
            List<Long> knowledgeIds = Lists.newArrayList();

            if (!CollectionUtils.isEmpty(searchResults)) {
                for (SearchResult result : searchResults) {
                    Document doc = result.getDocument();
                    Float similarity = result.getScore();
                    // 相似度阈值:0.7(低于阈值视为不相关)
                    if (similarity < 0.7) {
                        continue;
                    }
                    Long knowledgeId = Long.valueOf(doc.getMetadata().get("knowledgeId").toString());
                    CsKnowledge knowledge = knowledgeMapper.selectById(knowledgeId);
                    if (!ObjectUtils.isEmpty(knowledge)) {
                        AiResponseDTO.KnowledgeDTO knowledgeDTO = new AiResponseDTO.KnowledgeDTO();
                        BeanUtils.copyProperties(knowledge, knowledgeDTO);
                        knowledgeDTO.setSimilarity(similarity);
                        relatedKnowledges.add(knowledgeDTO);
                        knowledgeIds.add(knowledgeId);
                    }
                }
            }
            responseDTO.setRelatedKnowledges(relatedKnowledges);
            log.info("向量检索知识库结果:knowledgeIds={}, similarity={}", 
                    knowledgeIds, relatedKnowledges.stream().map(AiResponseDTO.KnowledgeDTO::getSimilarity).collect(Collectors.toList()));

            // 5. 构造大模型Prompt
            String knowledgeContent = CollectionUtils.isEmpty(relatedKnowledges) 
                    ? "无相关知识库内容" 
                    : relatedKnowledges.stream()
                            .map(k -> String.format("问题:%s,答案:%s", k.getQuestion(), k.getAnswer()))
                            .collect(Collectors.joining(";"));

            Map<String, Object> promptParams = Maps.newHashMap();
            promptParams.put("knowledge", knowledgeContent);
            promptParams.put("question", question);

            PromptTemplate promptTemplate = new PromptTemplate(PROMPT_TEMPLATE, promptParams);
            Prompt prompt = promptTemplate.create();

            // 6. 调用大模型生成回复(带重试机制)
            ChatResponse chatResponse = chatClient.call(prompt);
            String aiAnswer = chatResponse.getResult().getOutput().getContent();
            responseDTO.setAnswer(aiAnswer);
            log.info("大模型生成回复:answer={}", aiAnswer);

            // 7. 编程式事务:保存对话记录+更新知识库命中次数
            saveChatRecordAndUpdateKnowledge(userId, question, aiAnswer, knowledgeIds);

            // 8. 热门问题缓存(仅缓存知识库命中的问题)
            if (!CollectionUtils.isEmpty(relatedKnowledges)) {
                redisTemplate.opsForValue().set(cacheKey, aiAnswer, CACHE_HOT_QUESTION_EXPIRE);
            }

        } catch (IllegalArgumentException e) {
            log.error("参数校验失败:", e);
            responseDTO.setAnswer("参数错误:" + e.getMessage());
        } catch (Exception e) {
            log.error("处理用户提问异常:", e);
            responseDTO.setAnswer("非常抱歉,服务异常,请稍后再试");
        }

        // 9. 计算响应时间
        responseDTO.setResponseTime(System.currentTimeMillis() - startTime);
        return responseDTO;
    }

    /**
     * 分页查询用户对话记录
     */
    @Override
    public IPage<CsChatRecord> queryChatRecordByUserId(Long userId, Integer pageNum, Integer pageSize) {
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("用户ID必须为正整数");
        }
        pageNum = ObjectUtils.isEmpty(pageNum) || pageNum < 1 ? 1 : pageNum;
        pageSize = ObjectUtils.isEmpty(pageSize) || pageSize < 1 || pageSize > 100 ? 20 : pageSize;

        Page<CsChatRecord> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<CsChatRecord> queryWrapper = new LambdaQueryWrapper<CsChatRecord>()
                .eq(CsChatRecord::getUserId, userId)
                .eq(CsChatRecord::getIsDeleted, 0)
                .orderByDesc(CsChatRecord::getChatTime);

        return chatRecordMapper.selectPage(page, queryWrapper);
    }

    /**
     * 初始化知识库向量(系统启动时调用,将知识库内容写入向量库)
     */
    @Override
    public void initKnowledgeVector() {
        log.info("开始初始化知识库向量...");
        try {
            // 查询所有有效知识库
            LambdaQueryWrapper<CsKnowledge> queryWrapper = new LambdaQueryWrapper<CsKnowledge>()
                    .eq(CsKnowledge::getIsDeleted, 0);
            List<CsKnowledge> knowledgeList = knowledgeMapper.selectList(queryWrapper);

            if (CollectionUtils.isEmpty(knowledgeList)) {
                log.warn("无有效知识库数据,向量初始化跳过");
                return;
            }

            // 转换为Document并写入向量库
            List<Document> documents = knowledgeList.stream().map(knowledge -> {
                Map<String, Object> metadata = Maps.newHashMap();
                metadata.put("knowledgeId", knowledge.getId());
                metadata.put("category", knowledge.getCategory());
                return new Document(knowledge.getQuestion() + " " + knowledge.getAnswer(), metadata);
            }).collect(Collectors.toList());

            vectorStore.add(documents);
            log.info("知识库向量初始化完成,共写入{}条数据", documents.size());
        } catch (Exception e) {
            log.error("知识库向量初始化失败:", e);
            throw new RuntimeException("知识库向量初始化失败", e);
        }
    }

    /**
     * 意图识别(基于关键词匹配+模糊查询)
     * @param question 用户提问
     * @return 识别的意图名称
     */
    private String recognizeIntent(String question) {
        // 1. 拆分提问关键词(简单分词:按空格、逗号、句号拆分)
        String[] keywords = question.split("[\\s,,。;;!!??]");
        List<String> keywordList = Lists.newArrayList();
        for (String keyword : keywords) {
            if (StringUtils.hasText(keyword) && keyword.length() >= 2) {
                keywordList.add(keyword);
            }
        }

        if (CollectionUtils.isEmpty(keywordList)) {
            return "未知意图";
        }

        // 2. 匹配意图
        Map<String, Integer> intentCountMap = Maps.newHashMap();
        for (String keyword : keywordList) {
            List<CsIntent> intentList = intentMapper.selectByKeyword(keyword);
            if (!CollectionUtils.isEmpty(intentList)) {
                for (CsIntent intent : intentList) {
                    intentCountMap.put(intent.getIntentName(), 
                            intentCountMap.getOrDefault(intent.getIntentName(), 0) + 1);
                }
            }
        }

        // 3. 返回匹配次数最多的意图
        if (intentCountMap.isEmpty()) {
            return "未知意图";
        }

        return intentCountMap.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .map(Map.Entry::getKey)
                .orElse("未知意图");
    }

    /**
     * 编程式事务:保存对话记录+更新知识库命中次数
     */
    private void saveChatRecordAndUpdateKnowledge(Long userId, String question, String answer, List<Long> knowledgeIds) {
        // 开启事务
        DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        try {
            // 1. 保存对话记录
            CsChatRecord chatRecord = new CsChatRecord();
            chatRecord.setUserId(userId);
            chatRecord.setUserQuestion(question);
            chatRecord.setAiAnswer(answer);
            chatRecord.setKnowledgeIds(CollectionUtils.isEmpty(knowledgeIds) 
                    ? null 
                    : knowledgeIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
            chatRecord.setChatTime(LocalDateTime.now());
            chatRecord.setIsDeleted(0);
            chatRecordMapper.insert(chatRecord);
            log.info("保存对话记录成功:recordId={}", chatRecord.getId());

            // 2. 批量更新知识库命中次数
            if (!CollectionUtils.isEmpty(knowledgeIds)) {
                int updateCount = knowledgeMapper.batchUpdateHitCount(knowledgeIds);
                log.info("更新知识库命中次数:knowledgeIds={}, 更新行数={}", knowledgeIds, updateCount);
            }

            // 提交事务
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(transactionStatus);
            log.error("保存对话记录或更新知识库命中次数失败:", e);
            throw new RuntimeException("事务执行失败", e);
        }
    }
}

4.9 Controller层设计(REST接口)

复制代码
package com.jam.demo.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.jam.demo.dto.AiResponseDTO;
import com.jam.demo.dto.UserQuestionDTO;
import com.jam.demo.entity.CsChatRecord;
import com.jam.demo.service.CsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * 智能客服Controller
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/chat")
@RequiredArgsConstructor
@Tag(name = "智能客服接口", description = "用户对话、记录查询等接口")
public class CsController {

    private final CsService csService;

    /**
     * 处理用户提问
     */
    @PostMapping("/question")
    @Operation(summary = "用户提问接口", description = "接收用户提问,返回AI回复(支持多轮对话)")
    @ApiResponse(responseCode = "200", description = "请求成功",
            content = @Content(schema = @Schema(implementation = AiResponseDTO.class)))
    public ResponseEntity<AiResponseDTO> handleQuestion(
            @Valid @RequestBody @Parameter(description = "用户提问参数", required = true) UserQuestionDTO questionDTO) {
        AiResponseDTO responseDTO = csService.handleUserQuestion(questionDTO);
        return new ResponseEntity<>(responseDTO, HttpStatus.OK);
    }

    /**
     * 分页查询用户对话记录
     */
    @GetMapping("/records")
    @Operation(summary = "查询对话记录接口", description = "分页查询用户的历史对话记录")
    @ApiResponse(responseCode = "200", description = "请求成功",
            content = @Content(schema = @Schema(implementation = IPage.class)))
    public ResponseEntity<IPage<CsChatRecord>> queryChatRecords(
            @Parameter(description = "用户ID", required = true) @RequestParam Long userId,
            @Parameter(description = "页码(默认1)") @RequestParam(required = false) Integer pageNum,
            @Parameter(description = "每页条数(默认20,最大100)") @RequestParam(required = false) Integer pageSize) {
        IPage<CsChatRecord> chatRecordPage = csService.queryChatRecordByUserId(userId, pageNum, pageSize);
        return new ResponseEntity<>(chatRecordPage, HttpStatus.OK);
    }
}

4.10 系统启动类(SpringAiCsApplication.java)

复制代码
package com.jam.demo;

import com.jam.demo.service.CsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * 智能客服系统启动类
 * @author ken
 */
@Slf4j
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
@EnableTransactionManagement
@EnableCaching
@EnableRetry
@RequiredArgsConstructor
public class SpringAiCsApplication implements CommandLineRunner {

    private final CsService csService;

    public static void main(String[] args) {
        SpringApplication.run(SpringAiCsApplication.class, args);
        log.info("SpringAI智能客服系统启动成功!访问地址:http://localhost:8080/cs/swagger-ui.html");
    }

    /**
     * 系统启动后初始化知识库向量
     */
    @Override
    public void run(String... args) throws Exception {
        log.info("系统启动中,开始初始化知识库向量...");
        csService.initKnowledgeVector();
        log.info("知识库向量初始化完成,系统可正常使用");
    }
}

4.11 前端简单演示(HTML+JS)

创建 src/main/resources/static/index.html,实现简单的对话界面:

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>SpringAI智能客服</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; background: #f5f5f5; }
        .chat-container { width: 800px; margin: 50px auto; background: #fff; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        .chat-header { background: #409eff; color: #fff; padding: 16px; border-radius: 8px 8px 0 0; font-size: 18px; font-weight: bold; }
        .chat-content { height: 500px; overflow-y: auto; padding: 20px; border-bottom: 1px solid #eee; }
        .chat-message { margin-bottom: 20px; }
        .user-message { text-align: right; }
        .ai-message { text-align: left; }
        .message-bubble { display: inline-block; padding: 10px 16px; border-radius: 20px; max-width: 70%; }
        .user-bubble { background: #409eff; color: #fff; }
        .ai-bubble { background: #f0f0f0; color: #333; }
        .chat-input { padding: 20px; display: flex; gap: 10px; }
        #questionInput { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 24px; outline: none; font-size: 14px; }
        #sendBtn { padding: 12px 24px; background: #409eff; color: #fff; border: none; border-radius: 24px; cursor: pointer; font-size: 14px; }
        #sendBtn:hover { background: #3086e0; }
        .loading { color: #999; text-align: center; padding: 10px; }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">SpringAI智能客服</div>
        <div class="chat-content" id="chatContent">
            <div class="chat-message ai-message">
                <span class="message-bubble ai-bubble">您好!我是智能客服,有什么可以帮您?</span>
            </div>
        </div>
        <div class="chat-input">
            <input type="text" id="questionInput" placeholder="请输入您的问题...">
            <button id="sendBtn">发送</button>
        </div>
    </div>

    <script>
        // 全局变量:对话上下文ID(多轮对话用)
        let contextId = "";
        // 后端接口地址
        const apiUrl = "http://localhost:8080/cs/api/v1/chat";

        // 发送提问
        document.getElementById("sendBtn").addEventListener("click", sendQuestion);
        document.getElementById("questionInput").addEventListener("keydown", (e) => {
            if (e.key === "Enter") sendQuestion();
        });

        async function sendQuestion() {
            const questionInput = document.getElementById("questionInput");
            const question = questionInput.value.trim();
            if (!question) return;

            // 清空输入框
            questionInput.value = "";
            // 添加用户消息到界面
            addMessageToUI("user", question);

            // 显示加载中
            const loadingElement = addLoadingToUI();

            try {
                // 调用后端接口
                const response = await fetch(`${apiUrl}/question`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        userId: 1, // 测试用户ID(实际应从登录态获取)
                        question: question,
                        contextId: contextId
                    })
                });

                if (!response.ok) throw new Error("接口请求失败");
                const result = await response.json();
                // 更新上下文ID(支持多轮对话)
                contextId = result.contextId;
                // 添加AI回复到界面
                removeLoadingFromUI(loadingElement);
                addMessageToUI("ai", result.answer);
            } catch (error) {
                removeLoadingFromUI(loadingElement);
                addMessageToUI("ai", "非常抱歉,服务异常,请稍后再试");
                console.error("发送提问失败:", error);
            }
        }

        // 添加消息到界面
        function addMessageToUI(role, content) {
            const chatContent = document.getElementById("chatContent");
            const messageDiv = document.createElement("div");
            messageDiv.className = `chat-message ${role}-message`;

            const bubbleDiv = document.createElement("span");
            bubbleDiv.className = `message-bubble ${role}-bubble`;
            bubbleDiv.textContent = content;

            messageDiv.appendChild(bubbleDiv);
            chatContent.appendChild(messageDiv);
            // 滚动到底部
            chatContent.scrollTop = chatContent.scrollHeight;
        }

        // 添加加载中提示
        function addLoadingToUI() {
            const chatContent = document.getElementById("chatContent");
            const loadingDiv = document.createElement("div");
            loadingDiv.className = "chat-message ai-message loading";
            loadingDiv.textContent = "正在思考...";
            chatContent.appendChild(loadingDiv);
            chatContent.scrollTop = chatContent.scrollHeight;
            return loadingDiv;
        }

        // 移除加载中提示
        function removeLoadingFromUI(loadingElement) {
            if (loadingElement && loadingElement.parentNode) {
                loadingElement.parentNode.removeChild(loadingElement);
            }
        }
    </script>
</body>
</html>

五、系统测试与验证

5.1 启动系统

  1. 启动MySQL8.0和Redis;

  2. 替换 application.yml 中的 spring.ai.openai.api-key 为你的OpenAI API Key;

  3. 运行 SpringAiCsApplication.java,启动成功后日志会显示:

    复制代码
    SpringAI智能客服系统启动成功!访问地址:http://localhost:8080/cs/swagger-ui.html
    系统启动中,开始初始化知识库向量...
    知识库向量初始化完成,共写入4条数据

5.2 接口测试(Swagger3)

访问 http://localhost:8080/cs/swagger-ui.html,可看到所有接口文档,测试 POST /api/v1/chat/question 接口:

  • 请求体:

    复制代码
    {
      "userId": 1,
      "question": "如何申请退款?"
    }
  • 响应体(示例):

    复制代码
    {
      "answer": "退款申请步骤:1.进入我的订单;2.找到对应订单点击申请退款;3.选择退款原因;4.提交审核,审核通过后3-7个工作日到账。",
      "relatedKnowledges": [
        {
          "id": 3,
          "question": "如何申请退款?",
          "answer": "退款申请步骤:1.进入我的订单;2.找到对应订单点击申请退款;3.选择退款原因;4.提交审核,审核通过后3-7个工作日到账。",
          "similarity": 0.9876
        }
      ],
      "intent": "申请退款",
      "contextId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "responseTime": 568
    }

5.3 前端界面测试

访问 http://localhost:8080/cs/index.html,输入问题(如"订单多久发货?""如何修改密码?"),可看到智能客服的即时回复,支持多轮对话。

六、性能优化与生产环境适配

6.1 性能优化策略

  1. 缓存优化:热门问题缓存到Redis,减少大模型调用次数;

  2. 向量数据库替换:生产环境将InMemoryVectorStore替换为Milvus、Pinecone等专业向量数据库,支持海量数据存储和高效检索;

  3. 大模型调用优化

    • 使用批量调用减少请求次数;

    • 配置超时重试(Spring Retry),避免网络波动导致失败;

    • 启用大模型流式响应,减少用户等待时间;

  4. 异步处理:对话记录存储、知识库命中次数更新改为异步执行(@Async);

  5. 数据库优化:给常用查询字段建立索引,开启MySQL连接池。

6.2 生产环境适配

  1. 容器化部署:编写Dockerfile打包应用,部署到K8s集群;

  2. 配置中心:使用Nacos/Apollo管理配置(如大模型API Key、数据库连接信息);

  3. 监控告警:集成Prometheus+Grafana监控系统性能,配置接口超时、大模型调用失败告警;

  4. 链路追踪:集成SkyWalking/Zipkin,追踪大模型调用、数据库操作等全链路;

  5. 权限控制:添加用户登录认证(如Spring Security+JWT),避免接口被恶意调用。

七、常见问题排查

7.1 大模型调用失败

  • 检查API Key是否正确、是否有余额;

  • 检查网络是否能访问OpenAI接口(国内服务器需配置代理);

  • 查看日志中的错误信息,如"429 Too Many Requests"表示请求频率超限,需调整调用频率。

7.2 向量检索不准确

  • 调整相似度阈值(当前0.7,可根据实际情况优化);

  • 优化知识库内容,确保问题和答案简洁明了;

  • 更换更优的Embedding模型(如text-embedding-3-small)。

7.3 系统响应慢

  • 检查Redis缓存是否生效,热门问题是否命中缓存;

  • 检查大模型调用响应时间,可更换更快的模型(如GPT-3.5-turbo比GPT-4快);

  • 优化数据库查询,确保索引生效。

八、总结

本文基于SpringAI构建了企业级智能客服系统,从底层逻辑剖析到实战落地,涵盖了数据库设计、核心模块开发、大模型集成、性能优化全流程。通过SpringAI的标准化封装,我们无需关注大模型底层API细节,就能快速实现"意图识别→知识库检索→AI回复"的完整流程,且系统具备良好的扩展性和可维护性。

该系统也可根据业务需求进行扩展:

  • 集成更多大模型(如通义千问、讯飞星火);

  • 增加人工坐席转接功能;

  • 实现多语言对话支持;

  • 基于用户对话数据进行模型微调,提升回复精准度。

SpringAI作为Spring生态的重要组成部分,正在快速迭代发展,未来将支持更多AI能力和场景。对于Java开发者而言,SpringAI无疑是构建AI应用的最佳选择,让我们一起拥抱AI时代,用技术赋能业务创新!

相关推荐
、BeYourself11 小时前
PGvector :在 Spring AI 中实现向量数据库存储与相似性搜索
数据库·人工智能·spring·springai
、BeYourself2 天前
Spring AI ETL Pipeline Transformers 详细指南
人工智能·spring·etl·springai
fanruitian2 天前
Springai RAG 外挂知识库增强
llm·知识库·rag·springai·elt·外挂知识库
、BeYourself2 天前
Spring AI RAG 系统文档加载
java·后端·spring·springai
、BeYourself3 天前
✅ 宝塔 PostgreSQL 安装 contrib 扩展完整指南
数据库·postgresql·springai
fanruitian3 天前
SpringAi 创建mcp服务器,客户端连接服务器
springboot·springai·mcp
、BeYourself4 天前
✅ 宝塔 PostgreSQL 安装UUID指南
数据库·postgresql·springai
梵得儿SHI4 天前
(第六篇)Spring AI 核心技术攻坚:多模态模型集成与全场景落地实战
人工智能·springai·多模态ai开发·whisper语音转录技术·springai的三层架构设计·prompt优化·多模态内容生成
fanruitian4 天前
springboot openai 调用functioncall
java·spring boot·spring·ai·springai