1. 从一个真实困境说起:为什么“能跑起来的 AI 应用”和“能交付的 AI 应用”是两回事
过去一年多,我参与过好几个企业内部的 AI 应用项目,从智能客服、文档问答到流程自动化,几乎每个项目都经历过同一个尴尬阶段:Demo 演示时全场鼓掌,真到了要接入公司账号体系、要对接内部工单系统、要做权限隔离、要扛住几十个人同时用的时候,问题就全冒出来了。模型调用本身其实是最简单的一环,真正拖慢进度的是那些“看起来跟 AI 无关”的东西——用户怎么登录、会话怎么保持、不同部门的调用额度怎么隔离、日志怎么审计、模型换了之后上层业务要不要改代码。
这就是AI 应用底座这个概念出现的背景。而QuickBlue就是我在这个方向上持续关注和实际折腾过的一个项目。简单说,它想做的事情是:把 AI 应用开发中那些重复、琐碎、但又绕不开的工程问题,收敛成一套标准化的底座能力,让开发者把精力放回业务逻辑本身。它基于Spring Cloud微服务生态构建,运行在JDK 21之上,目标用户是那些准备把 AI 能力真正落到企业生产环境里的团队——不是玩票,是要上线、要维护、要扩展的那种。
这篇文章我会从整体设计思路、核心技术点、实操落地过程、以及踩过的坑四个层面,把 QuickBlue 这类 AI 应用底座讲透。不管你是刚接触微服务的新手,还是已经在做企业级 AI 落地的老手,应该都能从中找到可以直接抄作业的部分。
2. QuickBlue 到底解决什么问题:AI 应用底座的定位拆解
2.1 先搞清楚“底座”和“应用”的边界在哪里
很多人第一次听到“AI 应用底座”会有点懵,觉得是不是又一个包装概念。我换个说法你就明白了:盖楼的时候,地基、承重墙、水电管网是底座,你家里怎么装修、买什么家具是应用。底座负责的是“不管你怎么装修,楼都不会塌”,应用负责的是“这间屋子住起来舒不舒服”。
放到 AI 应用场景里,底座要管的事情包括:统一的模型接入层(今天用这个模型,明天换那个,上层不用改)、统一的鉴权与租户隔离(A 部门和 B 部门的数据不能串)、统一的会话与上下文管理(多轮对话的状态存哪里、怎么过期)、统一的限流与配额(防止某个业务把模型额度跑爆)、统一的日志与可观测性(出了问题能查到是哪一步挂了)。而应用层要管的是:这个 AI 助手的人设是什么、它要完成什么具体任务、它的交互流程怎么设计。
QuickBlue 的定位就是前者。它不试图帮你写 prompt,也不试图帮你设计对话流程,它解决的是“当你有十个 AI 应用要上线时,怎么让它们共用一套稳定的工程基础设施”。
2.2 为什么这件事在 2024 年之后变得特别紧迫
我观察到一个很明显的分水岭。2023 年大家做 AI 应用,基本是单点突破,一个团队做一个助手,怎么快怎么来,直接在一个 Spring Boot 单体里调模型 API 就完事了。但到了 2024 年下半年,情况变了:一个中大型企业里往往同时有好几个部门在做 AI 相关的东西,各自为战,重复造轮子,而且每个轮子都造得不结实。
这时候如果还没有底座思维,就会出现几个典型症状。第一,模型密钥散落在各个项目里,安全审计根本过不了。第二,每个应用都自己实现一遍会话管理,代码重复率极高,改一个 bug 要改五个地方。第三,没有统一的限流,某个应用突然放量,把整个公司的模型调用额度吃光,其他应用全部瘫痪。第四,想换一个更便宜的模型,结果发现上层业务代码跟原模型 API 绑死了,改造成本巨大。
QuickBlue 这类底座的价值,就是在这四个症状全面爆发之前,先把标准立起来。
2.3 它和“若依微服务 plus”这类项目的本质区别
热词里出现了“若依微服务 plus”,我顺便说清楚这个容易混淆的点。若依(RuoYi)系列本质上是通用后台管理系统的脚手架,它帮你把用户、角色、菜单、权限这些后台管理标配做完了,你在此基础上加业务模块。而 QuickBlue 这类 AI 应用底座的重心不在后台管理,而在AI 能力的工程化封装——模型网关、会话上下文、Token 计量、流式响应处理、多模型路由,这些是若依不会帮你做的。
当然两者不是对立的。实际项目里我见过有人拿若依做管理后台,拿 QuickBlue 这类底座做 AI 能力层,中间通过内部 API 打通,各司其职,反而更清晰。关键是想明白:你要的是“管理系统的快速搭建”,还是“AI 能力的稳定供给”,这两个诉求对应的技术选型是不一样的。
3. 核心技术点拆解:Spring Cloud 微服务 + JDK 21 这套组合拳怎么打
3.1 为什么是微服务,而不是一个“大单体”
这是我在技术选型讨论里被问得最多的问题。有人会说,AI 应用底座听起来也没多复杂,搞个单体不就行了,微服务是不是过度设计?
我的回答通常是:看你的部署边界和故障边界。如果整个公司只有一个 AI 应用,那单体确实够了。但底座的本质是“被多个应用共享”,一旦共享,就必然面临不同应用对底座不同模块的差异化需求。比如模型网关需要频繁更新(新模型上线),但鉴权模块很稳定;会话管理可能压力很大需要独立扩容,但配置中心几乎没压力。如果全塞在一个单体里,你为了更新模型网关,得把整个应用重新部署一遍,鉴权模块也跟着重启,这在生产环境是不可接受的。
微服务化之后,模型网关、鉴权中心、会话服务、配额服务各自独立部署、独立扩容、独立发布。模型网关挂了,鉴权还在,至少不会全盘崩溃。这就是故障隔离带来的实际收益。当然代价是运维复杂度上升,所以我在实操里会强调:微服务拆分要克制,不是越细越好,按“变更频率”和“扩容需求”两个维度来拆,通常五到七个服务是比较舒服的粒度。
3.2 Spring Cloud 在这个底座里承担了哪些具体角色
Spring Cloud 不是一个单一技术,它是一整套微服务解决方案的集合。在 QuickBlue 这类底座里,我实际用到的核心组件和它们的分工是这样的:
| 组件 | 承担角色 | 为什么选它 |
|---|---|---|
| Nacos | 注册中心 + 配置中心 | 一个组件干两件事,减少运维负担,配置动态刷新很实用 |
| Spring Cloud Gateway | 统一入口网关 | 所有外部请求先过网关,鉴权、限流、路由在这里做 |
| OpenFeign | 服务间调用 | 声明式调用,代码简洁,配合负载均衡开箱即用 |
| Sentinel | 流量控制与熔断 | 保护模型调用不被突发流量打垮,配额控制的核心 |
| Spring Cloud Stream | 异步消息 | 日志、计量这类非实时任务走异步,不阻塞主链路 |
这里我要特别说一下Sentinel 配合 Redis 做集群限流这个点,因为热词里也提到了。单机限流很简单,但底座是多实例部署的,如果每个实例各自限流,总量就控制不住。Sentinel 的集群限流模式需要一个 token server 来统一发放令牌,而 token server 的状态需要共享存储,Redis 就是最自然的选择。配置的时候要注意,Redis 集群模式下 Sentinel 的 datasource 配置跟单机不太一样,这个坑我在第 5 节会详细讲。
3.3 JDK 21 带来的实际好处,不只是“版本新”
很多人升级 JDK 版本是跟风,但 JDK 21 对 AI 应用底座来说有几个实打实的收益,我用下来感受最深的是三点。
第一是虚拟线程。AI 应用的一个典型特征是大量 IO 等待——等模型返回、等数据库、等外部 API。传统线程池模式下,每个请求占一个平台线程,线程池大小成了吞吐量的硬瓶颈。虚拟线程让“一个请求一个线程”的编程模型重新变得可行,代码写起来简单,吞吐量还上去了。我在会话服务里把处理逻辑改成虚拟线程后,同样的硬件配置下并发处理能力大概提升了三到四倍。
第二是记录模式(Record Patterns)和模式匹配的增强,处理模型返回的复杂 JSON 结构时代码清爽很多。第三是分代 ZGC,对于会话管理这种“大量短生命周期对象 + 少量长生命周期对象”的场景,GC 停顿明显更可控。当然,升级 JDK 21 也要注意依赖兼容性,这个我在实操部分会提醒。
3.4 数据通信:微服务之间到底怎么传数据
热词里有“数据通信网络与微服务”,这其实是个容易被忽视但很关键的点。微服务之间的通信方式选择,直接影响到整个底座的延迟和可靠性。
我的实践原则是:同步调用走 Feign,异步解耦走消息队列,大数据量传输走对象存储。具体来说,用户发起一次 AI 对话,网关鉴权后同步调用会话服务拿上下文,会话服务同步调用模型网关拿结果,这条主链路必须是同步的,因为用户在等。但对话结束后的计量统计、日志落库、审计记录,这些走异步消息,不占用主链路时间。至于模型返回的大段文本或者生成的图片,不要塞进消息体里传来传去,存到对象存储,消息里只传一个引用地址。
这个原则听起来简单,但我见过太多项目把所有东西都塞进同步调用,结果一个日志服务卡顿,整个对话链路跟着超时。通信方式的选择,本质上是在一致性和可用性之间做权衡,想清楚哪些数据“晚一点没关系”,就能把主链路解放出来。
4. 实操落地:从零搭一个可用的 AI 应用底座
4.1 环境准备与依赖版本锁定
动手之前,版本一定要锁死,这是血泪教训。微服务生态里版本不兼容导致的诡异问题,能让你排查一整天。我用的这套组合是经过验证的:
# 核心版本清单 JDK: 21 (LTS) Spring Boot: 3.2.x Spring Cloud: 2023.0.x Spring Cloud Alibaba: 2023.0.1.0 Nacos: 2.3.x Sentinel: 1.8.x Redis: 7.x MySQL: 8.x注意:Spring Boot 3.x 要求 JDK 17 起步,用 JDK 21 完全没问题,但如果你项目里还有老的 javax.* 依赖,需要迁移到 jakarta.*,这个改造量要提前评估。
依赖锁定我推荐用 Maven 的dependencyManagement统一管理,或者在 Gradle 里用 platform BOM。千万不要让各个子模块自己写版本号,否则迟早出现“A 模块用 3.2.0,B 模块用 3.2.5”这种问题。
4.2 服务拆分方案与目录结构
我实际用的拆分方案是六个核心服务,这个粒度用下来比较舒服:
- gateway-service:统一入口,负责路由、鉴权前置、全局限流
- auth-service:认证授权,Token 签发与校验,租户信息管理
- session-service:会话与上下文管理,对话历史存储
- model-gateway-service:模型接入层,多模型路由、流式响应处理
- quota-service:配额与计量,Token 统计、额度扣减
- admin-service:管理后台接口,配置管理、监控数据聚合
目录结构上,我建议用聚合工程,父 pom 管版本,子模块管业务:
quickblue-parent/ ├── pom.xml # 父工程,版本管理 ├── quickblue-common/ # 公共依赖、工具类、常量 ├── quickblue-gateway/ # 网关服务 ├── quickblue-auth/ # 认证服务 ├── quickblue-session/ # 会话服务 ├── quickblue-model/ # 模型网关 ├── quickblue-quota/ # 配额服务 └── quickblue-admin/ # 管理服务quickblue-common这个模块很关键,把统一返回体、异常定义、工具类、常量放这里,其他服务依赖它,避免重复代码。但要注意别把业务逻辑塞进 common,否则又变成隐性单体了。
4.3 模型网关的核心实现:多模型路由与流式响应
模型网关是整个底座里技术含量最高的部分,我重点讲。它的核心职责是:屏蔽不同模型提供方的 API 差异,对上提供统一接口。
先定义统一请求体:
public record ChatRequest( String tenantId, String sessionId, String modelKey, // 逻辑模型标识,不是具体模型名 List<Message> messages, boolean stream ) {}注意modelKey是逻辑标识,比如general-chat、code-assist,具体映射到哪个真实模型,由配置决定。这样换模型时只改配置,不改代码。
路由逻辑用策略模式实现:
public interface ModelProvider { String getProviderName(); ChatResponse chat(ChatRequest request); Flux<ChatChunk> chatStream(ChatRequest request); } @Service public class ModelRouter { private final Map<String, ModelProvider> providers; private final ModelConfigRepository configRepo; public ModelProvider route(String modelKey) { ModelConfig config = configRepo.findByKey(modelKey); ModelProvider provider = providers.get(config.getProviderName()); if (provider == null) { throw new BizException("未找到模型提供方: " + config.getProviderName()); } return provider; } }流式响应用 Spring WebFlux 的Flux返回,配合 SSE(Server-Sent Events)推给前端。这里有个实操要点:流式响应一定要设置合理的超时和心跳,否则中间网络抖动一下,连接就断了,用户看到的是“回答到一半卡住”。我的做法是每 15 秒发一个空的心跳事件,前端收到心跳就重置超时计时。
4.4 会话上下文管理:存哪里、存多久、怎么取
会话管理看起来简单,实际是最容易出问题的地方。核心问题是三个:存哪里、存多久、怎么高效取。
我的方案是热数据放 Redis,冷数据落 MySQL。最近 N 轮对话(比如最近 20 条)放 Redis,读取快;完整历史落 MySQL,用于审计和回溯。Redis 的 key 设计成session:{tenantId}:{sessionId},value 用 List 结构存消息。
过期策略要分层:Redis 里的热数据设 2 小时过期,MySQL 里的冷数据保留 90 天。为什么是 2 小时?因为绝大多数对话的活跃期就在这个范围内,超过 2 小时基本是新一轮对话了。这个值可以根据业务调整,但不要设太长,否则 Redis 内存吃不消。
// 上下文裁剪逻辑:只取最近 N 轮,控制 Token 消耗 public List<Message> buildContext(String sessionId, int maxRounds) { List<Message> all = redisService.getMessages(sessionId); int fromIndex = Math.max(0, all.size() - maxRounds * 2); return all.subList(fromIndex, all.size()); }实操心得:上下文不是越多越好。我见过有人把整个对话历史都塞给模型,结果 Token 消耗爆炸,响应还慢。实际测试下来,保留最近 10 到 20 轮对话,对绝大多数场景已经足够,再往前的信息用摘要的方式压缩成一条系统消息反而更高效。
4.5 配额与限流的落地配置
配额控制分两个层面:租户级配额(这个部门这个月能用多少 Token)和接口级限流(每秒最多多少请求)。前者是业务概念,后者是技术保护。
租户级配额我用 Redis 的原子计数器实现,每次调用后异步扣减:
public boolean tryConsume(String tenantId, long tokens) { String key = "quota:" + tenantId + ":" + currentMonth(); Long remaining = redisTemplate.opsForValue().decrement(key, tokens); if (remaining < 0) { redisTemplate.opsForValue().increment(key, tokens); // 回滚 return false; } return true; }接口级限流用 Sentinel,集群模式配置如下:
spring: cloud: sentinel: datasource: flow: redis: redis: host: ${REDIS_HOST} port: 6379 database: 1 rule-type: flow transport: dashboard: ${SENTINEL_DASHBOARD}这里有个坑:Sentinel 集群限流需要单独部署 token server,而且 token server 的高可用要自己保证。如果 token server 挂了,集群限流会退化成单机限流,保护能力下降。所以我在生产环境里会给 token server 配两个实例,前面挂个负载均衡。
5. 常见问题与排查技巧实录
5.1 服务注册不上或频繁掉线
这是新手最常遇到的问题。表现是 Nacos 控制台里服务时有时无,或者干脆注册不上。排查顺序我总结成一张表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全注册不上 | 网络不通 / 端口错 | telnet 测 Nacos 端口,检查配置 |
| 注册后立即掉线 | 心跳超时 | 检查服务是否卡在 GC 或死锁 |
| 时有时无 | 网络抖动 / 心跳间隔不合理 | 调大心跳超时,检查网络质量 |
| 多网卡注册错 IP | 网卡选择问题 | 指定spring.cloud.nacos.discovery.ip |
我踩过最深的一个坑是多网卡环境。服务器上装了 Docker,多出来一个虚拟网卡,Nacos 客户端默认选了虚拟网卡的 IP 注册,结果其他服务根本连不上。解决办法是显式指定注册 IP,别让它自动选。
5.2 Sentinel 规则不生效的几种情况
Sentinel 规则不生效,八成是这几个原因。第一,规则推送了但没持久化,重启就没了,这个要配 datasource 持久化到 Nacos 或 Redis。第二,资源名对不上,Sentinel 的资源名默认是接口路径,如果你用了自定义资源名,要确保@SentinelResource的 value 和规则里的资源名一致。第三,集群限流模式下 token server 没起来,这时候规则会静默失效,日志里其实有提示,但容易被忽略。
排查技巧:打开 Sentinel 的 debug 日志,
logging.level.com.alibaba.csp.sentinel=DEBUG,规则加载和匹配的过程一目了然,比瞎猜快得多。
5.3 JDK 21 升级后的兼容性坑
升级 JDK 21 之后,我遇到两个典型问题。一是某些老版本的字节码增强库不兼容,比如低版本的 CGLIB,报Unsupported class file major version,解决办法是升级到支持 JDK 21 的版本。二是反射相关的警告变多,JDK 21 对反射的限制更严,一些老框架会刷警告,虽然不影响运行,但日志很吵,可以通过--add-opens参数缓解。
还有一个容易被忽视的点:虚拟线程不要和 synchronized 混用。虚拟线程在 synchronized 块里会发生 pinning(固定到平台线程),失去虚拟线程的优势。如果代码里有大量 synchronized,要么改成 ReentrantLock,要么评估一下是否真的需要上虚拟线程。
5.4 流式响应中断的排查思路
流式响应中断是 AI 应用特有的问题,排查起来比较绕。我的排查顺序是:先看网关超时配置,再看 Nginx(如果有)的缓冲配置,最后看模型提供方的超时。
网关和 Nginx 默认都会缓冲响应,这会导致流式效果失效——用户等了半天,然后一次性收到全部内容。解决办法是在网关和 Nginx 上都关闭缓冲:
# Nginx 关闭缓冲,支持流式 proxy_buffering off; proxy_cache off; proxy_read_timeout 300s;网关侧如果用 Spring Cloud Gateway,要确保没有配置ModifyResponseBody这类会缓冲整个响应体的过滤器。
6. 我对 AI 应用底座这件事的真实看法
折腾了这么久,我最大的体会是:AI 应用底座的价值不在于技术多先进,而在于把不确定性收敛掉。模型会换、业务会变、流量会波动,底座的作用就是让这些变化不至于把整个系统掀翻。QuickBlue 这类项目的意义,是给企业提供一个“不用每次从零开始”的起点。
但我也要泼一盆冷水:底座不是银弹。如果你的团队只有一两个 AI 应用,上微服务底座反而是负担,运维成本可能超过收益。底座的收益是随着应用数量增长而增长的,应用越多,共享基础设施的价值越大。所以我的建议是,先想清楚你的应用规模和发展预期,再决定要不要上底座,以及上多重的底座。
另外,技术选型上不要迷信“最新最全”。Spring Cloud 生态很庞大,但不是每个组件你都需要。我见过有人把 Sentinel、Seata、Stream、Sleuth 全用上,结果一半组件根本没发挥作用,反而增加了排查难度。按需引入,用不到的坚决不加,这是我这些年做微服务最深的教训。
最后分享一个我在配额设计上的小技巧:永远给配额留 10% 的缓冲。比如租户买了 100 万 Token,系统里实际配置 110 万。为什么?因为计量本身有延迟,如果卡得死死的,用户正常使用时会频繁遇到“明明还有额度却提示超限”的情况,体验极差。这 10% 的缓冲,换来的是用户投诉量的大幅下降,非常值。