ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Spring Boot构建生产级AI应用平台:多租户架构与工程实践

基于Spring Boot构建生产级AI应用平台:多租户架构与工程实践

1. 为什么“生产级”三个字才是这个项目真正的门槛

先把结论摆在前面:用 Spring Boot 搭一个能跑通对话的 AI Demo,一个下午就够了;但要把它做成一个能扛住真实流量、能隔离不同客户数据、能控制成本、能排查线上问题的生产级 AI 应用平台,那是完全另一个量级的工程。这个项目标题里最值钱的不是“Spring Boot”,也不是“AI”,而是“生产级”这三个字。

我自己从最早用 Spring Boot 写单体接口,到后来接大模型做对话服务,再到给多个业务方做多租户的 AI 中台,中间踩的坑基本都集中在“Demo 能跑”和“线上能用”之间的那条鸿沟上。Demo 阶段你只需要一个 Controller 调一次模型 API 返回字符串;生产阶段你要面对的是:模型响应超时怎么降级、同一个租户的并发怎么限流、不同租户的 Key 和配额怎么隔离、对话上下文怎么存、流式输出怎么和网关配合、模型调用成本怎么核算、出问题怎么定位到具体是哪一次请求哪一段 Prompt。

所以这篇东西我不打算写成一份“Spring Boot 入门教程”,而是按一个真实落地项目的思路,把基于 Spring Boot 构建生产级 AI 应用平台这件事拆开讲:整体架构怎么设计、多租户怎么落地、Agent 能力怎么接、并发和安全怎么兜底、线上问题怎么排查。适合已经会写 Spring Boot、但还没把 AI 能力真正推到生产环境的同学,也适合正在做 AI 中台选型的技术负责人参考。

需要提前说明的是,下面涉及的具体版本号、参数值、目录结构,都是基于当前主流实践给出的合理参考方案,不是唯一解。你在自己项目里要结合团队规模、模型供应商、预算来调整,但设计思路和踩坑点是通用的。

2. 平台整体架构与核心思路拆解

2.1 先想清楚:这个平台到底给谁用

很多人一上来就写代码,结果写到一半发现架构根本撑不住需求。我的习惯是先把“谁用、怎么用、用到什么程度”这三件事写清楚,再动手。

一个生产级 AI 应用平台,典型的使用方有三类:

  • 内部业务团队:他们不想关心模型怎么调、Key 怎么管,只想拿到一个稳定的对话/生成接口,接入自己的业务系统。
  • 外部租户(多租户场景):每个租户有自己的用户、自己的数据、自己的配额,彼此绝对不能看到对方的东西。
  • 平台运营方:也就是你自己,需要看每个租户用了多少 Token、哪个模型最慢、哪个接口报错最多。

这三类使用方对应三种完全不同的诉求:业务团队要简单,租户要隔离,运营方要可观测。架构设计本质上就是同时满足这三件事。

2.2 分层设计:把“AI 能力”当成一种可替换的资源

我最终采用的是一种偏“中台”思路的分层结构,从上到下大致是:

层级职责关键技术点
接入层统一鉴权、限流、路由Spring Security、网关限流、租户识别
应用层对话、Agent、知识库等业务编排Spring AI、Agent 编排、会话管理
能力层模型调用、向量检索、工具调用模型适配器、Embedding、Function Calling
基础设施层存储、缓存、消息、监控MySQL、Redis、向量库、Micrometer

这么分层的核心目的只有一个:让模型是可替换的。今天你用某个云厂商的模型,明天要换成自建的,或者要同时接两家做灰度,如果模型调用散落在各个 Service 里,改起来就是灾难。所以我在能力层做了一个模型适配器抽象,上层只认接口,不认具体厂商。

2.3 为什么选 Spring Boot 而不是别的

这个问题我被问过很多次。老实说,如果只是做一个纯 AI 推理服务,Python 生态确实更顺手。但“生产级 AI 应用平台”的重点在“应用平台”四个字上——它本质上是一个企业级 Web 系统,AI 只是它的一个能力模块。

Spring Boot 在这个场景下的优势非常明确:

  • 生态成熟:鉴权、事务、缓存、消息、监控,这些企业系统必备的东西,Spring 全家桶都有现成方案,不用自己造轮子。
  • 多租户支持好:无论是基于 Schema 隔离还是基于字段隔离,Spring 的拦截器、AOP、数据源路由都能优雅实现。
  • 团队上手快:大部分后端团队本来就熟悉 Spring,招人、交接、维护成本都低。
  • 和 AI 框架衔接顺畅:Spring AI 这类框架本身就是为 Spring 生态设计的,接入成本比想象中低。

提示:如果你的团队是 Python 背景,硬上 Spring Boot 反而会增加沟通成本。技术选型永远要服务于团队,而不是服务于“哪个更酷”。

2.4 多租户:这个项目里最容易翻车的地方

多租户是标题里明确点出来的关键词,也是我见过翻车最多的地方。常见的三种隔离方案,各有取舍:

  • 独立数据库:隔离最彻底,但成本高、运维复杂,适合大客户。
  • 共享库独立 Schema:隔离较好,成本适中,适合中等规模。
  • 共享库共享表 + 租户字段:成本最低,隔离最弱,适合海量小租户。

我的建议是混合策略:默认走共享表 + 租户字段,给付费高的大租户提供独立 Schema 的选项。这样既控制了成本,又给了大客户安全感。具体实现上,我用一个TenantContext(基于 ThreadLocal 或 Reactor Context)在请求入口处解析出租户 ID,然后通过 MyBatis 拦截器自动给 SQL 加上租户条件,业务代码完全无感知。

这里有个坑必须提前说:异步和线程池会丢租户上下文。ThreadLocal 在跨线程时会失效,所以要么用可传递的上下文(如 TransmittableThreadLocal),要么在提交任务时手动把租户 ID 传进去。这个问题我在线上排查了整整一个下午,最后发现是某个异步日志任务把租户上下文丢了,导致日志串了租户。

3. 核心模块的细节解析与实操要点

3.1 模型适配器:让换模型像换配置一样简单

模型适配器的核心是一个接口加多个实现。接口定义要足够抽象,只暴露平台真正需要的能力:

public interface ModelProvider { ChatResponse chat(ChatRequest request); Flux<ChatResponse> streamChat(ChatRequest request); EmbeddingResponse embed(EmbeddingRequest request); String getName(); }

然后针对不同厂商写实现类,比如QwenProvider、OpenAiProvider、LocalModelProvider。上层业务通过一个ModelProviderFactory根据配置或租户策略拿到具体实现。

这么做的价值在于:灰度切换和故障降级变得极其简单。当某个模型供应商响应变慢时,我只需要在配置中心把对应租户的路由权重调低,流量自动切到备用模型,业务代码一行都不用改。

实操要点:

  • 每个 Provider 内部要统一处理超时、重试、熔断,不要让这些逻辑散落在业务层。
  • 请求和响应对象要做归一化,不同厂商的字段名不一样,统一映射到平台自己的 DTO。
  • 流式接口要特别注意背压,模型吐字速度和你下游消费速度不匹配时,容易把内存打爆。

3.2 会话与上下文管理:别把历史消息全塞进 Prompt

对话类应用绕不开上下文管理。新手最常见的做法是把所有历史消息拼成一个超长 Prompt 发给模型,结果就是:Token 消耗爆炸、响应变慢、还容易触发模型的上下文长度限制。

我的做法是分层管理上下文:

  • 短期上下文:最近 N 轮对话,直接进 Prompt。
  • 长期记忆:把历史对话做摘要或向量化存储,需要时检索回来。
  • 会话元数据:租户 ID、用户 ID、会话 ID、创建时间等,存数据库用于审计和统计。

具体到存储,我用 Redis 存活跃会话的短期上下文(设置合理过期时间),用 MySQL 存会话元数据和归档消息,用向量库存需要语义检索的记忆片段。三层各司其职,成本可控。

注意:上下文裁剪策略一定要可配置。不同模型支持的上下文长度不同,硬编码一个数字,换模型时必然出问题。

3.3 Agent 能力接入:从“会聊天”到“会干活”

Agent 是这个项目里最能体现“平台”价值的部分。普通对话只是问答,Agent 能调用工具、能多步推理、能完成复杂任务。Spring AI 提供了 Function Calling 的基础能力,但真正落地时,你需要自己解决几个问题:

  • 工具注册与发现:工具怎么定义、怎么注册、怎么让模型知道有哪些工具可用。
  • 多步编排:一次任务可能需要调用多个工具,中间结果怎么传递。
  • 执行安全:模型决定调用某个工具时,怎么防止它调用不该调用的东西。
  • 超时与中断:Agent 跑飞了怎么及时止损。

我的实现思路是:每个工具实现一个统一的Tool接口,包含名称、描述、参数 Schema 和执行逻辑。平台维护一个工具注册表,根据租户权限决定哪些工具对该租户可见。Agent 执行时,用一个带最大步数限制的循环来驱动,每一步都记录日志,超过步数或超时直接中断。

这里有个经验:工具描述写得好不好,直接决定 Agent 的成功率。模型是根据描述来判断该不该调用某个工具的,描述含糊,模型就会乱调。我一般要求工具描述包含“什么时候用、什么时候不用、参数含义”三部分。

3.4 多租户下的配额与限流

多租户平台必须做配额管理,否则一个租户把资源吃光,其他租户全遭殃。我的方案是两级限流:

  • 租户级:每个租户有独立的 QPS 和 Token 配额,用 Redis 做计数器。
  • 接口级:不同接口(对话、Embedding、Agent)分别限流,防止某个重接口拖垮整体。

配额的计算要结合模型成本。比如对话接口按 Token 计费,Embedding 按调用次数计费,Agent 因为可能多步调用,要按“预估步数 × 单步成本”来算。这些数据我都存在 Redis 里,定时同步到 MySQL 做账单。

限流算法我用的是令牌桶,因为它允许一定程度的突发流量,比固定窗口更平滑。实现上可以用 Redis + Lua 脚本保证原子性,也可以用现成的限流组件。

4. 完整实操流程与关键环节实现

4.1 项目骨架搭建

先给出我实际用的模块划分,这是一个多模块 Maven 项目:

ai-platform ├── ai-platform-common // 通用工具、常量、异常 ├── ai-platform-auth // 鉴权、租户识别 ├── ai-platform-model // 模型适配器 ├── ai-platform-agent // Agent 编排 ├── ai-platform-chat // 对话业务 ├── ai-platform-admin // 运营后台 └── ai-platform-bootstrap // 启动模块

这么拆的好处是职责清晰、依赖单向。common 被所有模块依赖,bootstrap 依赖所有模块,其他模块之间尽量不互相依赖,需要交互就走接口或事件。

4.2 租户上下文与拦截器实现

租户识别的入口是一个 Servlet Filter 或 Spring 拦截器,从请求头(比如X-Tenant-Id)或 JWT 中解析出租户 ID,写入上下文:

public class TenantContext { private static final ThreadLocal<String> CURRENT = new TransmittableThreadLocal<>(); public static void set(String tenantId) { CURRENT.set(tenantId); } public static String get() { return CURRENT.get(); } public static void clear() { CURRENT.remove(); } }

用TransmittableThreadLocal是为了解决线程池传递问题。然后在 MyBatis 拦截器里,自动给查询条件加上tenant_id = ?:

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class TenantInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { String tenantId = TenantContext.get(); if (tenantId != null) { // 改写 SQL,追加租户条件 } return invocation.proceed(); } }

提示:SQL 改写要特别小心,尤其是带子查询、Union 的复杂语句。我建议对关键表强制走拦截器,对统计类查询单独处理,避免改写出错。

4.3 模型调用的完整链路

一次对话请求的完整链路是这样的:

  1. 请求进入,Filter 解析租户 ID 和用户身份。
  2. 限流器检查租户配额,超限直接返回 429。
  3. 会话管理器加载上下文。
  4. 路由策略选择模型 Provider。
  5. Provider 调用模型,处理超时和重试。
  6. 结果写回会话,记录 Token 消耗。
  7. 返回响应,异步上报监控指标。

每一步都要有日志和指标。我用的监控方案是 Micrometer + Prometheus,关键指标包括:请求量、响应时间 P99、Token 消耗、错误率、各模型调用占比。这些指标按租户维度打标签,运营后台可以直接看板展示。

4.4 流式输出的实现细节

流式输出是 AI 应用的标配体验,但实现起来坑不少。Spring 里我用SseEmitter或 WebFlux 的Flux来做。关键点:

  • 超时设置:SSE 连接默认超时时间要调大,否则长回答会被截断。
  • 异常处理:模型中途报错,要能优雅地给前端发一个结束事件,而不是直接断连。
  • 租户上下文:流式场景下上下文传递更复杂,WebFlux 要用 Reactor Context,不能依赖 ThreadLocal。
  • 背压:下游消费慢时要有缓冲或丢弃策略,防止内存堆积。

我实测下来,流式接口的 P99 延迟比非流式低很多(因为首字返回快),但资源占用更高,因为连接保持时间长。所以流式接口的限流要比非流式更严格。

5. 常见问题与排查技巧实录

5.1 线上问题速查表

现象可能原因排查方向
响应突然变慢模型供应商限流或故障看各 Provider 的 P99 指标
租户数据串了上下文丢失或 SQL 未加租户条件检查异步任务和拦截器
Token 消耗异常高上下文未裁剪或 Agent 死循环看单次请求的 Token 统计
流式输出中断SSE 超时或网关缓冲检查网关配置和超时设置
内存持续上涨会话未过期或流式背压失效看 Redis 内存和 JVM 堆

5.2 几个我踩过的坑

坑一:异步任务丢租户上下文。前面提过,用普通 ThreadLocal 在线程池里会失效。解决方案是换成 TransmittableThreadLocal,或者在任务提交时显式传递租户 ID。这个坑的隐蔽性在于,它不会立刻报错,而是偶发地串数据,排查起来很费劲。

坑二:模型超时没做降级。早期我没给模型调用设超时,结果某个供应商抽风时,请求全部堆积,线程池被打满,整个服务雪崩。后来加了超时 + 熔断 + 降级三件套,才稳住。

坑三:Agent 死循环。有一次 Agent 在两个工具之间反复调用,Token 消耗飙升。后来加了最大步数限制和循环检测,才解决。经验是:任何自主决策的循环,都必须有硬性上限。

坑四:Prompt 注入。用户输入里如果包含恶意指令,可能让模型执行不该执行的操作。我的做法是在 Prompt 组装时对用户输入做转义和隔离,同时限制 Agent 可调用的工具范围。

5.3 性能优化的几个实用技巧

  • 连接池复用:模型调用的 HTTP 客户端要用连接池,别每次新建。
  • 结果缓存:相同问题 + 相同上下文的请求可以缓存,尤其是 Embedding 这种幂等操作。
  • 批量处理:Embedding 支持批量就批量,能显著降低调用次数。
  • 预热:服务启动后对模型连接做预热,避免首批请求慢。

6. 关于这个平台后续可以怎么扩展

写到这里,核心的东西基本都覆盖了。最后分享几个我实际在做的扩展方向,给有需要的同学参考。

一个是多模型协作。现在很多场景不是单模型能搞定的,比如一个负责理解、一个负责生成、一个负责审核。平台层面可以支持“模型编排”,把多个模型串成一条流水线,每个环节用最合适的模型。

另一个是成本精细化运营。把每次调用的 Token 消耗、模型单价、租户归属都记录下来,做成实时账单。这样运营方可以清楚地知道哪个租户在烧钱,哪个功能性价比低。

还有一个是评测体系。生产环境最怕的是模型悄悄变差。我搭了一套离线评测流程,定期用固定测试集跑一遍,对比不同模型、不同 Prompt 的效果,数据驱动地做优化决策。

我个人在实际操作中的体会是:AI 应用平台的技术难点,八成不在 AI 本身,而在那些“传统”的工程问题上——隔离、限流、监控、降级。把 Spring Boot 这套企业级工程能力用扎实了,AI 能力接进来就是水到渠成的事。反过来,如果工程底座不牢,模型再强也撑不住生产环境。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进