跳到主要内容
Spring AI 实战:搭建 SaaS 模式多租户 AI 客服平台 | 极客日志
Java SaaS AI java
Spring AI 实战:搭建 SaaS 模式多租户 AI 客服平台 介绍基于 Spring AI 构建 SaaS 模式多租户 AI 客服平台的实战方案。涵盖多租户隔离(TenantContext、Redis 多库)、流量控制(Resilience4j)、Prompt 模板管理(FreeMarker)及性能优化(缓存、分表)。通过动态模型切换、租户级限流降级及并发压测实践,解决高并发下的数据隔离与稳定性问题,提供可复用的生产环境代码参考。
极客工坊 发布于 2026/4/6 更新于 2026/5/22 27 浏览前言
随着 AI 大模型技术的普及,智能客服已成为企业降本增效的核心工具。传统的单租户 AI 客服系统无法满足 SaaS 平台的规模化需求 —— 不同租户需要独立的模型配置、数据隔离、流量管控,同时还要保证高并发下的性能稳定性。
本文基于 Spring AI 的多租户 AI 客服 SaaS 平台开发实战经验,拆解 SaaS 模式 AI 客服平台的开发全流程:从架构设计到核心难点突破,从功能实现到性能压测优化,所有代码均为生产环境可直接复用的实战代码。
一、项目背景与架构设计
1.1 项目定位与核心需求
项目定位 :SaaS 模式的智能客服解决方案,支持多企业租户接入,每个租户可自定义 AI 话术模板、独立配置大模型(如 GPT-3.5/4、文心一言、通义千问),平台提供对话记录存储、AI 质量评分、流量管控等能力。
核心需求 :
维度 核心需求 技术挑战 多租户隔离 模型配置隔离、数据隔离、缓存隔离 动态切换租户上下文、Redis 多库隔离 性能稳定性 支持 100 + 租户并发调用 AI 模型 限流降级、缓存优化、数据库分表 功能定制化 租户自定义 Prompt 模板、模型参数 模板引擎渲染、动态模型配置 可观测性 对话记录分析、客服质量评分 Spring AI 调用多模型、数据可视化
1.2 整体架构设计
以下是平台的核心架构图,清晰呈现各模块的交互逻辑。
1.3 技术栈选型
结合项目需求和 Spring 生态最佳实践,最终选型如下:
技术领域 选型 选型理由 核心框架 Spring Boot 3.2 + Spring AI 0.8.1 Spring AI 原生适配 Spring 生态,支持多模型统一调用 多租户核心 ThreadLocal + TenantContext 轻量、高性能的租户上下文切换方案 缓存 Redis 7.0 支持多数据库隔离,性能优异 流量控制 Resilience4j 轻量、适配 Spring Boot,支持限流 / 降级 / 熔断 模板引擎 FreeMarker 灵活的 Prompt 模板渲染,支持租户自定义变量 数据库 MySQL 8.0 + MyBatis-Plus 支持分表,适配多租户数据存储 压测工具 JMeter 模拟 100 租户并发场景,精准定位性能瓶颈
二、核心技术难点突破
2.1 多租户模型配置:TenantContext 动态切换模型
2.1.1 问题背景
SaaS 平台中,每个租户可能配置不同的 AI 模型(如租户 A 用 GPT-3.5,租户 B 用文心一言)、不同的 API Key、不同的模型参数(温度、topP 等),核心挑战是请求链路中动态切换租户的模型配置,且保证线程安全。
2.1.2 TenantContext 核心实现
基于 ThreadLocal 实现租户上下文隔离,保证多线程下租户信息不串用:
@Component
public class TenantContext {
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal <>();
private static final LoadingCache<String, AiModelConfig> MODEL_CONFIG_CACHE = CacheBuilder.newBuilder()
.expireAfterWrite(30 , TimeUnit.MINUTES)
.maximumSize(1000 )
.build(new CacheLoader <String, AiModelConfig>() {
@Override
public AiModelConfig load (String tenantId) {
return aiModelConfigService.getByTenantId(tenantId);
}
});
@Autowired
private AiModelConfigService aiModelConfigService;
public static void setTenantId (String tenantId) {
TENANT_ID.set(tenantId);
}
public static String getTenantId () {
return TENANT_ID.get();
}
public AiModelConfig getCurrentModelConfig () {
String tenantId = getTenantId();
if (tenantId == null ) {
throw new BusinessException ("租户 ID 不能为空" );
}
try {
return MODEL_CONFIG_CACHE.get(tenantId);
} catch (Exception e) {
throw new BusinessException ("加载租户模型配置失败:" + e.getMessage());
}
}
public static void clear () {
TENANT_ID.remove();
}
}
2.1.3 拦截器自动注入租户上下文 在请求入口拦截器中,从请求头 / Token 中解析租户 ID 并注入上下文:
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-Id" );
if (StringUtils.isBlank(tenantId)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false ;
}
TenantContext.setTenantId(tenantId);
return true ;
}
@Override
public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TenantInterceptor tenantInterceptor;
@Override
public void addInterceptors (InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor)
.addPathPatterns("/api/**" )
.excludePathPatterns("/api/public/**" );
}
}
2.1.4 Spring AI 动态切换模型配置 基于租户上下文的配置,动态构建 AI 客户端,实现多租户模型切换:
@Service
public class AiModelFactory {
@Autowired
private TenantContext tenantContext;
public AiClient getCurrentAiClient () {
AiModelConfig config = tenantContext.getCurrentModelConfig();
switch (config.getModelType()) {
case "OPENAI" :
return createOpenAiClient(config);
case "ERNIE" :
return createErnieClient(config);
case "QIANWEN" :
return createQianWenClient(config);
default :
throw new BusinessException ("不支持的模型类型:" + config.getModelType());
}
}
private AiClient createOpenAiClient (AiModelConfig config) {
OpenAiApi api = new OpenAiApi (config.getApiBaseUrl(), config.getApiKey());
OpenAiChatClient client = new OpenAiChatClient (api);
client.setTemperature(config.getTemperature());
client.setTopP(config.getTopP());
client.setModel(config.getModelName());
return client;
}
private AiClient createErnieClient (AiModelConfig config) {
return null ;
}
private AiClient createQianWenClient (AiModelConfig config) {
return null ;
}
}
2.1.5 实战踩坑与解决方案 踩坑场景 原因 解决方案 租户上下文串用 异步线程中 ThreadLocal 值丢失 异步任务中手动传递租户 ID:String tenantId = TenantContext.getTenantId(); CompletableFuture.runAsync(() -> {TenantContext.setTenantId(tenantId); ...}) 模型配置加载慢 每次请求都查数据库 引入 Guava Cache 本地缓存,30 分钟过期,兼顾性能和配置实时性 ThreadLocal 内存泄漏 请求结束未清除上下文 拦截器 afterCompletion 中调用 TenantContext.clear()
2.2 租户级缓存:Redis 多数据库隔离方案
2.2.1 缓存隔离痛点 多租户场景下,若所有租户的缓存共用一个 Redis 库,会出现缓存 key 冲突、数据泄露、清理困难等问题。核心解决方案是 Redis 多数据库隔离:每个租户分配独立的 Redis DB(如租户 1 用 DB1,租户 2 用 DB2),同时保证缓存操作的透明化。
2.2.2 Redis 多库隔离设计
2.2.3 核心代码实现
自定义 Redis 连接工厂,支持动态切换 DB :
@Component
public class DynamicRedisConnectionFactory extends JedisConnectionFactory {
public void switchDb (int dbIndex) {
if (dbIndex < 0 || dbIndex > 15 ) {
throw new BusinessException ("Redis DB 索引超出范围:" + dbIndex);
}
if (super .isActive()) {
super .destroy();
}
super .setDatabase(dbIndex);
super .afterPropertiesSet();
}
}
@Component
public class TenantRedisTemplate {
@Autowired
private DynamicRedisConnectionFactory redisConnectionFactory;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private int getDbIndex (String tenantId) {
return Math.abs(tenantId.hashCode()) % 15 + 1 ;
}
public <T> T execute (RedisCallback<T> callback) {
String tenantId = TenantContext.getTenantId();
if (tenantId == null ) {
throw new BusinessException ("租户 ID 为空,无法执行缓存操作" );
}
int dbIndex = getDbIndex(tenantId);
redisConnectionFactory.switchDb(dbIndex);
return redisTemplate.execute(callback);
}
public void set (String key, Object value, long timeout, TimeUnit unit) {
execute(connection -> {
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
byte [] keyBytes = serializer.serialize(key);
byte [] valueBytes = redisTemplate.getValueSerializer().serialize(value);
connection.setEx(keyBytes, unit.toSeconds(timeout), valueBytes);
return null ;
});
}
public Object get (String key) {
return execute(connection -> {
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
byte [] keyBytes = serializer.serialize(key);
byte [] valueBytes = connection.get(keyBytes);
return redisTemplate.getValueSerializer().deserialize(valueBytes);
});
}
}
@Service
public class PromptTemplateService {
@Autowired
private TenantRedisTemplate tenantRedisTemplate;
public PromptTemplate getTemplate (String templateId) {
String cacheKey = "prompt:template:" + templateId;
PromptTemplate template = (PromptTemplate) tenantRedisTemplate.get(cacheKey);
if (template != null ) {
return template;
}
template = promptTemplateMapper.selectById(templateId);
tenantRedisTemplate.set(cacheKey, template, 1 , TimeUnit.HOURS);
return template;
}
}
2.3 流量控制:Resilience4j 实现限流与降级
2.3.1 流量控制需求 AI 模型调用成本高、QPS 有限,需对每个租户进行限流(如单租户最大 QPS 10),同时在模型服务不可用时降级(返回预设话术),避免平台整体雪崩。
2.3.2 Resilience4j 核心配置 <dependency >
<groupId > io.github.resilience4j</groupId >
<artifactId > resilience4j-spring-boot3</artifactId >
<version > 2.1.0</version >
</dependency >
<dependency >
<groupId > io.github.resilience4j</groupId >
<artifactId > resilience4j-ratelimiter</artifactId >
<version > 2.1.0</version >
</dependency >
<dependency >
<groupId > io.github.resilience4j</groupId >
<artifactId > resilience4j-circuitbreaker</artifactId >
<version > 2.1.0</version >
</dependency >
resilience4j:
ratelimiter:
instances:
aiCallRateLimiter:
limit-for-period: 10
limit-refresh-period: 1s
timeout-duration: 0
register-health-indicator: true
circuitbreaker:
instances:
aiCallCircuitBreaker:
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
sliding-window-size: 100
register-health-indicator: true
@Component
public class TenantRateLimiterManager {
private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap <>();
@Autowired
private RateLimiterRegistry rateLimiterRegistry;
public RateLimiter getCurrentRateLimiter () {
String tenantId = TenantContext.getTenantId();
return rateLimiterMap.computeIfAbsent(tenantId, key -> {
RateLimiterConfig config = rateLimiterRegistry.getConfiguration("aiCallRateLimiter" )
.orElse(RateLimiterConfig.ofDefaults());
return RateLimiter.of(key, config);
});
}
public <T> T executeRateLimitedSupplier (Supplier<T> supplier) {
RateLimiter rateLimiter = getCurrentRateLimiter();
return RateLimiter.decorateSupplier(rateLimiter, supplier).get();
}
}
@Service
public class AiCustomerService {
@Autowired
private AiModelFactory aiModelFactory;
@Autowired
private TenantRateLimiterManager rateLimiterManager;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
public String generateReply (String userQuestion) {
return rateLimiterManager.executeRateLimitedSupplier(() -> {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("aiCallCircuitBreaker" );
return CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
AiClient aiClient = aiModelFactory.getCurrentAiClient();
Prompt prompt = new Prompt (new UserMessage (userQuestion));
AiResponse response = aiClient.generate(prompt);
return response.getGeneration().getText();
}).get();
});
}
public String fallback (String userQuestion, Exception e) {
if (e instanceof RequestNotPermitted) {
return "当前咨询人数过多,请稍后再试(租户限流)" ;
} else if (e instanceof CircuitBreakerOpenException) {
return "AI 服务暂时不可用,请稍后再试(服务熔断)" ;
} else {
return "非常抱歉,暂时无法为您解答,请联系人工客服" ;
}
}
}
三、核心功能实现
3.1 话术模板管理:租户自定义 Prompt 模板
3.1.1 需求分析 每个租户需要自定义 AI 客服的话术模板(如售前模板、售后模板),模板支持变量替换(如 {{tenantName}}、{{userName}}),同时支持模板的 CRUD 操作。
3.1.2 表结构设计 CREATE TABLE `prompt_template` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键' ,
`tenant_id` varchar (64 ) NOT NULL COMMENT '租户 ID' ,
`template_name` varchar (128 ) NOT NULL COMMENT '模板名称' ,
`template_type` varchar (32 ) NOT NULL COMMENT '模板类型(售前/售后)' ,
`template_content` text NOT NULL COMMENT '模板内容(FreeMarker 语法)' ,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ,
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户 ID 索引'
) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COMMENT= '租户 Prompt 模板表' ;
3.1.3 模板渲染核心代码
@Service
public class PromptTemplateEngine {
@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;
@Autowired
private PromptTemplateMapper promptTemplateMapper;
public String renderTemplate (String templateType, Map<String, Object> variables) {
String tenantId = TenantContext.getTenantId();
PromptTemplate template = promptTemplateMapper.selectByTenantIdAndType(tenantId, templateType);
if (template == null ) {
throw new BusinessException ("租户未配置 [" + templateType + "] 类型的 Prompt 模板" );
}
try {
Template fmTemplate = new Template ("promptTemplate" , new StringReader (template.getTemplateContent()), freeMarkerConfigurer.getConfiguration());
StringWriter writer = new StringWriter ();
fmTemplate.process(variables, writer);
return writer.toString();
} catch (Exception e) {
throw new BusinessException ("模板渲染失败:" + e.getMessage());
}
}
public void saveTemplate (PromptTemplate template) {
template.setTenantId(TenantContext.getTenantId());
promptTemplateMapper.insert(template);
}
}
3.1.4 模板使用示例
@Service
public class AiCustomerService {
@Autowired
private PromptTemplateEngine templateEngine;
public String generateReply (String userQuestion, String userName) {
Map<String, Object> variables = new HashMap <>();
variables.put("userQuestion" , userQuestion);
variables.put("userName" , userName);
variables.put("tenantName" , "某电商企业" );
String promptContent = templateEngine.renderTemplate("after_sale" , variables);
Prompt prompt = new Prompt (new UserMessage (promptContent));
AiClient aiClient = aiModelFactory.getCurrentAiClient();
AiResponse response = aiClient.generate(prompt);
return response.getGeneration().getText();
}
}
3.2 对话记录分析:AI 驱动的客服质量评分
3.2.1 评分逻辑设计 基于用户与 AI 的对话记录,调用大模型对回复准确性、语气友好度、解决率三个维度进行评分(1-5 分),最终生成综合评分,帮助租户分析客服质量。
3.2.2 核心实现代码
@Service
public class ConversationScoreService {
@Autowired
private AiModelFactory aiModelFactory;
@Autowired
private ConversationRecordMapper conversationRecordMapper;
public ConversationScore scoreConversation (Long conversationId) {
String tenantId = TenantContext.getTenantId();
ConversationRecord record = conversationRecordMapper.selectById(conversationId);
if (!tenantId.equals(record.getTenantId())) {
throw new BusinessException ("无权限访问该对话记录" );
}
String scorePrompt = "请对以下 AI 客服对话进行质量评分,评分规则:\n" +
"1. 回复准确性:1-5 分,回复是否准确解答用户问题\n" +
"2. 语气友好度:1-5 分,回复语气是否友好、专业\n" +
"3. 解决率:1-5 分,是否有效解决用户问题\n" +
"输出格式为 JSON:{\"accuracy\": 5, \"friendliness\": 4, \"solveRate\": 5, \"totalScore\": 4.7}\n" +
"对话内容:\n" +
"用户问题:%s\n" +
"AI 回复:%s" .formatted(record.getUserQuestion(), record.getAiReply());
AiClient aiClient = aiModelFactory.getCurrentAiClient();
Prompt prompt = new Prompt (new UserMessage (scorePrompt));
AiResponse response = aiClient.generate(prompt);
String scoreJson = response.getGeneration().getText();
ObjectMapper objectMapper = new ObjectMapper ();
ConversationScore score = objectMapper.readValue(scoreJson, ConversationScore.class);
score.setConversationId(conversationId);
score.setTenantId(tenantId);
conversationScoreMapper.insert(score);
return score;
}
}
3.3 性能压测:100 租户并发场景优化实践
3.3.1 压测环境与工具
压测工具 :JMeter 5.6
压测场景 :模拟 100 个租户,每个租户 10 个并发用户,持续调用 AI 客服接口 10 分钟
服务器配置 :4 核 8G 云服务器,Redis 7.0(单机),MySQL 8.0(单机)
3.3.2 初始压测结果与瓶颈分析 指标 初始结果 性能瓶颈 平均响应时间 2.5s 1. AI 模型调用无缓存;2. MySQL 单表查询慢;3. Redis 未做连接池优化 QPS 50 低于预期的 100 QPS 错误率 8% 1. 租户限流触发;2. 数据库连接池耗尽
3.3.3 核心优化方案
@Service
public class AiCustomerService {
@Autowired
private TenantRedisTemplate tenantRedisTemplate;
public String generateReply (String userQuestion) {
String cacheKey = "ai:reply:" + DigestUtils.md5DigestAsHex(userQuestion.getBytes());
Object cacheValue = tenantRedisTemplate.get(cacheKey);
if (cacheValue != null ) {
return cacheValue.toString();
}
String reply = doGenerateReply(userQuestion);
tenantRedisTemplate.set(cacheKey, reply, 5 , TimeUnit.MINUTES);
return reply;
}
}
MySQL 分表优化 :对话记录表按租户 ID 分表(conversation_record_${tenantId % 10}),减少单表数据量,提升查询性能。
连接池优化 :
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
idle-timeout: 300000
connection-timeout: 20000
redis:
jedis:
pool:
max-active: 100
max-idle: 20
min-idle: 5
max-wait: 2000ms
3.3.4 优化后压测结果 指标 优化后结果 提升幅度 平均响应时间 800ms 提升 68% QPS 120 提升 140% 错误率 0.5% 降低 93.75%
四、实战踩坑与解决方案汇总 问题分类 具体问题 根因 最终解决方案 多租户隔离 异步线程租户上下文丢失 ThreadLocal 不支持跨线程传递 异步任务手动传递租户 ID,使用 InheritableThreadLocal(仅适合父子线程) 缓存问题 Redis DB 切换后连接泄漏 未正确关闭旧连接 自定义 Redis 连接工厂,切换 DB 前关闭当前连接 性能问题 AI 模型调用重复请求 相同问题重复调用模型 租户级 Redis 缓存 AI 回复,5 分钟过期 限流问题 租户限流计数器串用 限流器未按租户隔离 实现 TenantRateLimiterManager,每个租户独立限流器 模板问题 模板渲染 XSS 风险 租户自定义模板含恶意脚本 渲染前对模板内容进行 XSS 过滤,限制模板变量类型
五、总结与进阶规划
5.1 核心总结
多租户隔离 :基于 ThreadLocal 实现 TenantContext 动态切换租户信息,Redis 多数据库隔离保证缓存安全,是 SaaS 平台的核心基础;
流量管控 :Resilience4j 实现租户级限流 + 熔断,避免单租户滥用资源导致平台雪崩;
功能定制化 :FreeMarker 模板引擎支持租户自定义 Prompt,满足不同行业的话术需求;
性能优化 :AI 回复缓存、MySQL 分表、连接池调优,是支撑 100 租户并发的关键;
5.2 进阶规划
模型私有化部署 :支持租户私有化部署 AI 模型,降低 API 调用成本,提升数据安全性;
多模型融合 :实现多模型调用结果融合,提升回复准确性(如 GPT + 文心一言);
监控可视化 :基于 Prometheus+Grafana 搭建租户级监控面板,实时监控 QPS、响应时间、错误率;
成本管控 :统计每个租户的 AI 模型调用次数,实现按量计费;
国际化支持 :适配多语言模板,支持海外租户接入。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online