(第二篇)Spring AI 实战进阶:从 0 搭建 SaaS 模式多租户 AI 客服平台(核心难点 + 性能优化全解析)

(第二篇)Spring AI 实战进阶:从 0 搭建 SaaS 模式多租户 AI 客服平台(核心难点 + 性能优化全解析)

前言

随着 AI 大模型技术的普及,智能客服已成为企业降本增效的核心工具,但传统的单租户 AI 客服系统无法满足 SaaS 平台的规模化需求 —— 不同租户需要独立的模型配置、数据隔离、流量管控,同时还要保证高并发下的性能稳定性。

笔者近期主导了基于 Spring AI 的多租户 AI 客服 SaaS 平台开发,踩遍了多租户模型隔离、缓存隔离、流量控制、高并发优化等核心坑点。本文将从实战角度,完整拆解 SaaS 模式 AI 客服平台的开发全流程:从架构设计到核心难点突破,从功能实现到性能压测优化,所有代码均为生产环境可直接复用的实战代码,同时结合可视化图表清晰呈现核心逻辑,希望能给做 AI SaaS 开发的同学提供有价值的参考。

一、项目背景与架构设计

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.1Spring 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() { 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 并注入上下文:

/** * 租户拦截器:所有请求先解析租户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/**") // 拦截所有API请求 .excludePathPatterns("/api/public/**"); // 排除公开接口 } } 
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()); 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 核心代码实现
  1. 自定义 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(); } } 
  1. 租户缓存工具类,封装 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); 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); }); } // 其他方法:del、expire等(略) } 
  1. 业务层使用示例
// 业务层调用缓存,无需关心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 核心配置
  1. 引入依赖
<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> 
  1. 配置文件(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 
  1. 自定义租户限流管理器
/** * 租户级限流管理器:每个租户独立的限流计数器 */ @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(); } } 
  1. 限流 + 熔断实战代码
/** * 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(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 模板渲染核心代码
/** * 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(); } catch (Exception e) { throw new BusinessException("模板渲染失败:" + e.getMessage()); } } // 模板CRUD方法(略) 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) { // 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" 请对以下AI客服对话进行质量评分,评分规则: 1. 回复准确性:1-5分,回复是否准确解答用户问题 2. 语气友好度:1-5分,回复语气是否友好、专业 3. 解决率:1-5分,是否有效解决用户问题 输出格式为JSON:{"accuracy": 5, "friendliness": 4, "solveRate": 5, "totalScore": 4.7} 对话内容: 用户问题:%s AI回复:%s """.formatted(record.getUserQuestion(), record.getAiReply()); // 3. 调用AI模型评分 AiClient aiClient = aiModelFactory.getCurrentAiClient(); Prompt prompt = new Prompt(new UserMessage(scorePrompt)); AiResponse response = aiClient.generate(prompt); String scoreJson = response.getGeneration().getText(); // 4. 解析评分结果 ObjectMapper objectMapper = new ObjectMapper(); ConversationScore score = objectMapper.readValue(scoreJson, ConversationScore.class); // 5. 保存评分结果 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.5s1. AI 模型调用无缓存;2. MySQL 单表查询慢;3. Redis 未做连接池优化
QPS50低于预期的 100 QPS
错误率8%1. 租户限流触发;2. 数据库连接池耗尽
3.3.3 核心优化方案
  1. 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; } } 
  1. MySQL 分表优化:对话记录表按租户 ID 分表(conversation_record_${tenantId % 10}),减少单表数据量,提升查询性能。
  2. 连接池优化
# 数据库连接池优化 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%
QPS120提升 140%
错误率0.5%降低 93.75%

四、实战踩坑与解决方案汇总

问题分类具体问题根因最终解决方案
多租户隔离异步线程租户上下文丢失ThreadLocal 不支持跨线程传递异步任务手动传递租户 ID,使用 InheritableThreadLocal(仅适合父子线程)
缓存问题Redis DB 切换后连接泄漏未正确关闭旧连接自定义 Redis 连接工厂,切换 DB 前关闭当前连接
性能问题AI 模型调用重复请求相同问题重复调用模型租户级 Redis 缓存 AI 回复,5 分钟过期
限流问题租户限流计数器串用限流器未按租户隔离实现 TenantRateLimiterManager,每个租户独立限流器
模板问题模板渲染 XSS 风险租户自定义模板含恶意脚本渲染前对模板内容进行 XSS 过滤,限制模板变量类型

五、总结与进阶规划

5.1 核心总结

  1. 多租户隔离:基于 ThreadLocal 实现 TenantContext 动态切换租户信息,Redis 多数据库隔离保证缓存安全,是 SaaS 平台的核心基础;
  2. 流量管控:Resilience4j 实现租户级限流 + 熔断,避免单租户滥用资源导致平台雪崩;
  3. 功能定制化:FreeMarker 模板引擎支持租户自定义 Prompt,满足不同行业的话术需求;
  4. 性能优化:AI 回复缓存、MySQL 分表、连接池调优,是支撑 100 租户并发的关键;

5.2 进阶规划

  1. 模型私有化部署:支持租户私有化部署 AI 模型,降低 API 调用成本,提升数据安全性;
  2. 多模型融合:实现多模型调用结果融合,提升回复准确性(如 GPT + 文心一言);
  3. 监控可视化:基于 Prometheus+Grafana 搭建租户级监控面板,实时监控 QPS、响应时间、错误率;
  4. 成本管控:统计每个租户的 AI 模型调用次数,实现按量计费;
  5. 国际化支持:适配多语言模板,支持海外租户接入。

最后

本文从实战角度完整拆解了基于 Spring AI 的多租户 AI 客服 SaaS 平台开发,覆盖了多租户隔离、流量控制、模板定制、性能优化等核心难点,所有代码均经过生产环境验证。AI SaaS 开发的核心是隔离与复用—— 既要保证租户间的数据 / 资源隔离,又要实现平台功能的复用,希望本文的实战经验能给大家带来帮助。

如果对你有帮助,欢迎点赞 + 收藏 + 关注,后续会持续更新 Spring AI 进阶实战内容(如模型私有化部署、多模型融合)。

如果有任何问题或不同见解,欢迎在评论区交流~

Read more

地瓜机器人智慧医疗——贰贰玖想要分享的关于使用惯导的一些思路

地瓜机器人智慧医疗——贰贰玖想要分享的关于使用惯导的一些思路

前言 在第20届全国大学生智能车竞赛(智慧医疗机器人创意赛)中,我们贰贰玖拿下国一。在这里,作为队长兼技术主力兼机师兼……我想分享一下在备赛过程中的一些思路。当然,为了不把比赛搞成全都是20s以内,竞争激烈到前后几名差0.几秒,我不会开源我们的惯导和避障思路(实在太简单,太容易实现了)。 这是我们两年的备赛日记,也有我们第二年区域赛和国赛的全流程。 【贰贰玖|从省三到国一,从巡线到路径规划到惯导+纯视觉避障的贰贰玖智能车日记-哔哩哔哩】 https://b23.tv/IDJyM2P 数据集我放在这里了,一共2w9张,全都是640x480,有数据增强的(没有旋转):https://pan.baidu.com/s/10u4S4fiVATRyEeDpdzpk_A?pwd=0229 提取码:0229 下面面我会讲一下我们的网络问题怎么解决,上位机的一些辅助处理,如何半场扫码,如何准确返回 P 点,修改stm32,以及修改车的ekf.yaml。

By Ne0inhk
武汉火影数字:VR大空间在文旅产业的创新应用

武汉火影数字:VR大空间在文旅产业的创新应用

VR大空间是一种利用空旷的物理空间,结合先进的VR技术,让用户能够在其中自由移动并深度体验虚拟世界的创新项目方式。 在科技飞速发展的当下,文旅产业正经历着前所未有的变革。VR大空间技术宛如一颗璀璨的新星,迅速崛起并成为文旅产业的新宠。无论是繁华都市的商场,还是热门的旅游景区,都能看到VR大空间体验项目的身影,吸引着众多游客和消费者前来尝鲜。 VR 大空间:解锁文旅新体验 打破时空限制,畅游世界奇观 以往,人们想要领略世界各地的文化遗产和自然奇观,往往需要长途跋涉,花费大量的时间和金钱。而VR大空间技术的出现,彻底打破了这种时间和空间的限制,通过VR大空间技术,游客足不出户,或者在城市中的VR体验场馆,就能实现云游览,感受不同地域文化的震撼。 深度互动,化身故事主角 在传统的文旅体验中,游客大多是被动的观察者,而VR大空间技术让游客成为了故事的参与者,极大地增强了旅游体验的趣味性和参与感,游客不再是只能观看,而是能够真正地亲身参与。通过全新的手势交互方式,游客能够轻松地一秒入戏,成为故事中的主角,在唯美仙界、冰寒雪域、神秘宫殿中无尽漫游、梦幻角逐,全面调动触感、风感、冰感、

By Ne0inhk

Retinaface+CurricularFace部署教程:镜像内Python 3.11.14环境安全补丁升级方法

Retinaface+CurricularFace部署教程:镜像内Python 3.11.14环境安全补丁升级方法 你是不是也遇到过这样的问题:刚拉取一个功能齐全的人脸识别镜像,准备直接上手测试,结果发现Python版本存在已知安全漏洞?或者在企业级部署中,安全审计要求必须打上最新补丁,但又担心升级后破坏原有推理环境的稳定性?别急,这篇教程就是为你量身定制的——我们不讲抽象理论,不堆砌参数配置,而是手把手带你完成RetinaFace+CurricularFace镜像中Python 3.11.14的安全补丁升级,全程零兼容性风险,所有操作均可逆、可验证。 本教程面向真实工程场景:你拿到的是一个开箱即用的AI镜像,目标不是从头编译Python,而是以最小改动、最高安全性完成升级。我们会避开常见的“重装conda环境”陷阱,绕过PyTorch与CUDA的脆弱依赖链,用官方推荐的增量补丁方式,让Python版本号不变(仍是3.11.14),但底层安全漏洞全部修复。整个过程5分钟内完成,不影响任何已有推理脚本运行。 1. 镜像环境现状与升级必要性 1.1 当前镜像核心组件一览 Ret

By Ne0inhk

【FPGA】深入解析M25P16 SPI-FLASH的读写操作与Verilog实现

1. M25P16 SPI-FLASH基础解析 第一次接触M25P16时,我被它精巧的封装和强大的功能惊艳到了。这款只有8个引脚的芯片,竟然能存储2MB数据,而且支持10万次擦写循环。作为FPGA开发者最常用的外置存储器之一,理解它的工作原理是进行嵌入式存储开发的基础。 M25P16采用标准的SPI接口协议,支持模式0和模式3。这里有个容易混淆的点:虽然SPI有4种模式,但M25P16只支持其中两种。在实际项目中,我遇到过因为模式设置错误导致通信失败的案例。后来用逻辑分析仪抓取波形才发现,问题出在CPHA参数的配置上。 存储结构方面,M25P16采用三级寻址方式: * 32个扇区(Sector),每个扇区256页 * 每页256字节 * 总容量正好是16Mb(2MB) 这种结构直接影响我们的操作方式。比如进行页编程时,如果写入数据超过256字节,超出的部分会从当前页开头覆盖,这个特性我在早期开发时踩过坑。有次连续写入300字节数据,结果前44字节被意外覆盖,导致系统异常。 2. 关键操作指令详解 2.1 基本指令集剖析 M25P16的指令系统非常精简,但每个指令

By Ne0inhk