一、项目背景与架构设计
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 实现租户上下文隔离,保证多线程下租户信息不串用:
/**
* 租户上下文(核心类)
* 基于 ThreadLocal 实现租户信息隔离,支持动态切换
*/
@Component
public class TenantContext {
// 存储当前线程的租户 ID
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();
// 存储租户 ID -> 模型配置的映射(本地缓存,减轻 DB 压力)
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;
/**
* 设置当前租户 ID
*/
public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}
/**
* 获取当前租户 ID
*/
public static String getTenantId() {
return TENANT_ID.get();
}
/**
* 获取当前租户的模型配置
*/
public AiModelConfig getCurrentModelConfig() {
getTenantId();
(tenantId == ) {
();
}
{
MODEL_CONFIG_CACHE.get(tenantId);
} (Exception e) {
( + e.getMessage());
}
}
{
TENANT_ID.remove();
}
}
2.1.3 拦截器自动注入租户上下文
在请求入口拦截器中,从请求头 / Token 中解析租户 ID 并注入上下文:
/**
* 租户拦截器:所有请求先解析租户 ID,注入 TenantContext
*/
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取租户 ID(实际项目中可从 JWT Token 解析)
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) {
// 关键:请求结束后清除上下文,防止 ThreadLocal 内存泄漏
TenantContext.clear();
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TenantInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns();
}
}
2.1.4 Spring AI 动态切换模型配置
基于租户上下文的配置,动态构建 AI 客户端,实现多租户模型切换:
/**
* AI 模型工厂:根据租户配置动态创建不同的 AI 客户端
*/
@Service
public class AiModelFactory {
@Autowired
private TenantContext tenantContext;
/**
* 获取当前租户的 AI 客户端
*/
public AiClient getCurrentAiClient() {
AiModelConfig config = tenantContext.getCurrentModelConfig();
// 根据租户配置的模型类型,创建不同的 AI 客户端
switch (config.getModelType()) {
case "OPENAI":
return createOpenAiClient(config);
case "ERNIE":
return createErnieClient(config);
case "QIANWEN":
return createQianWenClient(config);
default:
throw new BusinessException("不支持的模型类型:" + config.getModelType());
}
}
// 创建 OpenAI 客户端
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());
client;
}
AiClient {
;
}
AiClient {
;
}
}
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:
/**
* 动态 Redis 连接工厂:支持根据租户 ID 切换 Redis DB
*/
@Component
public class DynamicRedisConnectionFactory extends JedisConnectionFactory {
/**
* 切换 Redis DB
* @param dbIndex DB 索引
*/
public void switchDb(int dbIndex) {
// 校验 DB 索引范围(Redis 默认 0-15)
if (dbIndex < 0 || dbIndex > 15) {
throw new BusinessException("Redis DB 索引超出范围:" + dbIndex);
}
// 关闭当前连接
if (super.isActive()) {
super.destroy();
}
// 设置新的 DB 索引
super.setDatabase(dbIndex);
// 重新初始化连接
super.afterPropertiesSet();
}
}
- 租户缓存工具类,封装 DB 切换逻辑:
/**
* 租户级 Redis 缓存工具类
* 自动根据租户 ID 切换 Redis DB,对业务层透明
*/
@Component
public class TenantRedisTemplate {
@Autowired
private DynamicRedisConnectionFactory redisConnectionFactory;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 租户 ID -> Redis DB 索引的映射规则(简单取模,可自定义)
private int getDbIndex(String tenantId) {
// 避免使用 DB0(默认库),从 DB1 开始分配
return Math.abs(tenantId.hashCode()) % 15 + 1;
}
/**
* 执行缓存操作(内部自动切换 DB)
*/
public <T> T execute(RedisCallback<T> callback) {
String tenantId = TenantContext.getTenantId();
if (tenantId == null) {
throw new BusinessException("租户 ID 为空,无法执行缓存操作");
}
// 切换 Redis DB
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);
[] valueBytes = redisTemplate.getValueSerializer().serialize(value);
connection.setEx(keyBytes, unit.toSeconds(timeout), valueBytes);
;
});
}
Object {
execute(connection -> {
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
[] keyBytes = serializer.serialize(key);
[] valueBytes = connection.get(keyBytes);
redisTemplate.getValueSerializer().deserialize(valueBytes);
});
}
}
- 业务层使用示例:
// 业务层调用缓存,无需关心 DB 切换,工具类自动处理
@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;
}
// 缓存未命中,从 DB 加载
template = promptTemplateMapper.selectById(templateId);
// 存入缓存(过期时间 1 小时)
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>
- 配置文件(application.yml):
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 # 失败率阈值 50%
wait-duration-in-open-state: 60s # 熔断后 60 秒尝试恢复
sliding-window-size: 100 # 滑动窗口大小
register-health-indicator: true
- 自定义租户限流管理器:
/**
* 租户级限流管理器:每个租户独立的限流计数器
*/
@Component
public class TenantRateLimiterManager {
// 存储租户 ID -> 限流器的映射
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();
}
}
- 限流 + 熔断实战代码:
/**
* AI 客服核心服务:整合限流、熔断、动态模型调用
*/
@Service
public class AiCustomerService {
@Autowired
private AiModelFactory aiModelFactory;
@Autowired
private TenantRateLimiterManager rateLimiterManager;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
/**
* 调用 AI 模型生成回复(核心方法)
*/
public String generateReply(String userQuestion) {
// 1. 限流控制(租户级)
return rateLimiterManager.executeRateLimitedSupplier(() -> {
// 2. 熔断控制
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("aiCallCircuitBreaker");
return CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
// 3. 获取当前租户的 AI 客户端
AiClient aiClient = aiModelFactory.getCurrentAiClient();
// 4. 构建 Prompt(后续模板管理会详细讲)
Prompt prompt = new Prompt(new UserMessage(userQuestion));
// 5. 调用 AI 模型
AiResponse response = aiClient.generate(prompt);
return response.getGeneration().getText();
}).get();
});
}
/**
* 降级方法:限流/熔断/模型调用失败时触发
*/
public String fallback {
(e RequestNotPermitted) {
;
} (e CircuitBreakerOpenException) {
;
} {
;
}
}
}
三、核心功能实现
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 模板渲染核心代码
/**
* Prompt 模板引擎:支持租户自定义模板 + 变量替换
*/
@Service
public class PromptTemplateEngine {
@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;
@Autowired
private PromptTemplateMapper promptTemplateMapper;
/**
* 渲染模板
* @param templateType 模板类型
* @param variables 变量(如 tenantName、userName 等)
*/
public String renderTemplate(String templateType, Map<String, Object> variables) {
String tenantId = TenantContext.getTenantId();
// 1. 查询当前租户的模板
PromptTemplate template = promptTemplateMapper.selectByTenantIdAndType(tenantId, templateType);
if (template == null) {
throw new BusinessException("租户未配置 [" + templateType + "] 类型的 Prompt 模板");
}
// 2. FreeMarker 渲染模板
try {
Template fmTemplate = new Template("promptTemplate", new StringReader(template.getTemplateContent()), freeMarkerConfigurer.getConfiguration());
StringWriter writer = new StringWriter();
fmTemplate.process(variables, writer);
return writer.toString();
} (Exception e) {
( + e.getMessage());
}
}
{
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) {
// 1. 构建模板变量
Map<String, Object> variables = new HashMap<>();
variables.put("userQuestion", userQuestion);
variables.put("userName", userName);
variables.put("tenantName", "某电商企业"); // 从租户配置中获取
// 2. 渲染售后模板
String promptContent = templateEngine.renderTemplate("after_sale", variables);
// 3. 调用 AI 模型
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 核心实现代码
/**
* 对话质量评分服务:AI 驱动的多维度评分
*/
@Service
public class ConversationScoreService {
@Autowired
private AiModelFactory aiModelFactory;
@Autowired
private ConversationRecordMapper conversationRecordMapper;
/**
* 对对话记录进行评分
*/
public ConversationScore scoreConversation(Long conversationId) {
String tenantId = TenantContext.getTenantId();
// 1. 查询对话记录
ConversationRecord record = conversationRecordMapper.selectById(conversationId);
if (!tenantId.equals(record.getTenantId())) {
throw new BusinessException("无权限访问该对话记录");
}
// 2. 构建评分 Prompt
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());
// 3. 调用 AI 模型评分
AiClient aiClient = aiModelFactory.getCurrentAiClient();
( (scorePrompt));
aiClient.generate(prompt);
response.getGeneration().getText();
();
objectMapper.readValue(scoreJson, ConversationScore.class);
score.setConversationId(conversationId);
score.setTenantId(tenantId);
conversationScoreMapper.insert(score);
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 核心优化方案
- AI 回复缓存优化:
// 对相同问题的 AI 回复进行缓存(租户级)
@Service
public class AiCustomerService {
@Autowired
private TenantRedisTemplate tenantRedisTemplate;
public String generateReply(String userQuestion) {
// 1. 构建缓存 Key(租户级)
String cacheKey = "ai:reply:" + DigestUtils.md5DigestAsHex(userQuestion.getBytes());
// 2. 先查缓存
Object cacheValue = tenantRedisTemplate.get(cacheKey);
if (cacheValue != null) {
return cacheValue.toString();
}
// 3. 调用 AI 模型(省略限流/熔断逻辑)
String reply = doGenerateReply(userQuestion);
// 4. 存入缓存(过期时间 5 分钟,兼顾性能和实时性)
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 连接池优化
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 模型调用次数,实现按量计费;
- 国际化支持:适配多语言模板,支持海外租户接入。


