一、前言:为什么选择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通过三大核心组件实现这一流程:
-
ChatClient:统一封装大模型API(支持OpenAI、Azure OpenAI、本地化模型等),提供标准化对话接口;
-
EmbeddingClient:将文本(用户提问、知识库内容)转换为向量,实现语义级检索;
-
VectorStore:存储向量数据,支持高效相似度匹配(本文使用SpringAI内置的InMemoryVectorStore,生产环境可替换为Milvus、Pinecone等专业向量数据库)。
3.2 完整流程图
3.3 关键技术区分:SpringAI vs 传统大模型集成
| 对比维度 | 传统大模型集成 | SpringAI集成 |
|---|---|---|
| 开发成本 | 需手动封装大模型API、处理鉴权/超时/重试 | 标准化接口,自动处理鉴权、重试、异常封装 |
| 生态兼容性 | 与Spring生态整合繁琐(需自定义配置) | 原生兼容Spring Boot/Cloud,支持依赖注入 |
| 向量处理 | 需手动集成第三方Embedding库 | 内置EmbeddingClient,支持多模型切换 |
| 扩展性 | 新增大模型需重构代码 | 基于SPI机制,新增模型仅需配置依赖 |
| 运维成本 | 需单独维护大模型调用逻辑 | 与Spring应用统一运维,支持监控/链路追踪 |
四、实战开发:从0搭建智能客服系统
4.1 环境准备
-
JDK17安装:确保本地环境为JDK17;
-
MySQL8.0配置 :创建数据库
springai-cs,字符集utf8mb4; -
Redis7.0+:本地启动Redis(默认端口6379,无密码);
-
大模型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 启动系统
-
启动MySQL8.0和Redis;
-
替换
application.yml中的spring.ai.openai.api-key为你的OpenAI API Key; -
运行
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 性能优化策略
-
缓存优化:热门问题缓存到Redis,减少大模型调用次数;
-
向量数据库替换:生产环境将InMemoryVectorStore替换为Milvus、Pinecone等专业向量数据库,支持海量数据存储和高效检索;
-
大模型调用优化 :
-
使用批量调用减少请求次数;
-
配置超时重试(Spring Retry),避免网络波动导致失败;
-
启用大模型流式响应,减少用户等待时间;
-
-
异步处理:对话记录存储、知识库命中次数更新改为异步执行(@Async);
-
数据库优化:给常用查询字段建立索引,开启MySQL连接池。
6.2 生产环境适配
-
容器化部署:编写Dockerfile打包应用,部署到K8s集群;
-
配置中心:使用Nacos/Apollo管理配置(如大模型API Key、数据库连接信息);
-
监控告警:集成Prometheus+Grafana监控系统性能,配置接口超时、大模型调用失败告警;
-
链路追踪:集成SkyWalking/Zipkin,追踪大模型调用、数据库操作等全链路;
-
权限控制:添加用户登录认证(如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时代,用技术赋能业务创新!
