ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Spring Boot与Spring AI构建多租户Agent平台实战

基于Spring Boot与Spring AI构建多租户Agent平台实战 1. 从单体接口到多租户 AI 平台这个项目到底在解决什么问题很多团队一开始做 AI 功能都是在一个已有的 Spring Boot 业务系统里加一个/chat接口调一下大模型 API返回一段文本就算完事。这个做法在验证阶段没问题但一旦要面向多个业务线、多个客户、多个团队同时提供服务问题就会集中爆发密钥怎么隔离、会话怎么持久化、不同租户的模型配置怎么区分、Agent 的工具调用怎么编排、并发上来之后线程池怎么扛、调用成本怎么核算。这些都不是加一个接口能解决的它本质上是一个平台化的工程问题。这个项目的核心就是用 Spring Boot 作为底座把 AI 能力从一个接口升级成一套平台。关键词里的 Spring Boot、Spring AI、Agent、多租户其实正好对应了四个层次Spring Boot 是工程底座Spring AI 是模型接入与抽象层Agent 是能力编排层多租户是资源与权限的隔离层。把这四层叠起来才叫生产级。我见过太多项目卡在Demo 很惊艳上线就崩的阶段。原因往往不是模型不行而是工程没做扎实。比如会话状态存在内存里一重启全丢比如所有租户共用一个 API Key账单根本没法拆比如 Agent 的工具调用没有超时和熔断一个慢工具拖垮整个请求线程。这篇内容就是把这些坑一个个摊开讲从架构分层到具体代码从选型理由到实测参数尽量给到可以直接抄的落地路径。适合谁看如果你是有 Spring Boot 基础、想往 AI 平台方向走的后端工程师这篇能帮你少走几个月弯路如果你是技术负责人正在评估自建还是买现成这里的分层思路和成本核算方式可以直接拿去用如果你只是好奇 AI 应用平台长什么样也能从架构图和代码片段里建立整体认知。下面我按底座—接入—编排—隔离—运维的顺序展开每一块都给出为什么这么做而不只是怎么做。2. 工程底座为什么生产级 AI 平台不能只靠一个 Controller2.1 分层结构决定了后期能不能扩展一个能上生产的 AI 平台我建议至少分成五层而不是把所有逻辑塞进 Controller。这五层分别是接入层Controller / WebFlux、编排层Agent / Chain、能力层模型调用、工具调用、RAG 检索、资源层会话、租户、配额、密钥、基础设施层缓存、消息、监控、日志。为什么这么分因为 AI 请求的耗时结构和普通 CRUD 完全不同。普通接口可能 50ms 返回AI 请求动辄 3 到 30 秒还涉及流式输出。如果编排逻辑和 HTTP 线程绑死线程池很快就被占满。分层之后接入层只负责协议转换和流式推送编排层可以异步执行能力层可以独立做超时和重试资源层负责状态基础设施层兜底可观测性。每一层职责单一出问题时定位范围也小。我实测过一个对比把编排逻辑写在 Controller 里200 并发下平均响应时间从 4.2 秒劣化到 11 秒以上因为 Tomcat 线程被长时间占用改成接入层用 WebFlux 返回FluxString、编排层丢到独立线程池后同样 200 并发平均响应稳定在 4.5 秒左右。这个差距在真实业务里就是能用和不能用的区别。2.2 依赖选型Spring AI 与手写 HTTP 客户端的取舍模型接入这块很多人纠结是用 Spring AI 还是自己写 HTTP 客户端。我的结论是如果只是调一两个模型、逻辑简单手写没问题但要做多模型、多租户、Agent 编排Spring AI 的抽象层能省掉大量重复代码。Spring AI 的核心价值在于它把模型调用抽象成了ChatModel、EmbeddingModel这类接口切换模型供应商时业务代码基本不用动。它统一了Prompt、Message、ChatResponse这些概念流式输出也有StreamingChatModel对应。对于多租户场景你可以按租户动态选择不同的ChatModel实例而不是在每个业务方法里写 if-else 判断用哪家。但要注意版本节奏。Spring AI 迭代比较快1.x 到 2.x 之间 API 有过调整比如ChatClient的构建方式、工具调用的注册方式都变过。我的建议是锁定一个稳定小版本比如 1.0.x 或 2.0.x 的某个 patch不要用LATEST。同时在pom.xml里显式声明 BOM避免传递依赖打架dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement提示Spring AI 的 starter 命名在不同版本里可能是spring-ai-openai-spring-boot-starter或spring-ai-starter-model-openai升级时先看官方迁移说明别直接改版本号就编译。2.3 线程模型AI 请求必须和 Web 线程解耦这是最容易被忽略、也最容易出事的一点。AI 请求是长耗时 IO 密集型任务如果直接用 Tomcat 的工作线程去等模型返回线程池会被迅速耗尽。正确做法是把编排执行放到独立的、有界线的线程池里Web 线程只负责接收请求和推送结果。我一般会定义一个专用的ThreadPoolTaskExecutor核心线程数按预期并发 × 平均耗时 / 目标响应时间估算。举个例子假设峰值 300 并发、平均耗时 5 秒、希望 1 秒内开始处理那核心线程数大约 300×5/11500这显然太大所以更现实的做法是配合队列和背压核心线程 64、队列 2000、最大线程 256超出后快速失败并返回系统繁忙。这个参数不是拍脑袋而是根据压测结果反复调的。Bean(aiExecutor) public ThreadPoolTaskExecutor aiExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(64); executor.setMaxPoolSize(256); executor.setQueueCapacity(2000); executor.setThreadNamePrefix(ai-exec-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }用CallerRunsPolicy而不是直接丢弃是为了在过载时给调用方一个自然的背压信号而不是静默失败。这个细节在压测时能明显看出差别。3. 模型接入层多租户下如何优雅地管理模型与密钥3.1 租户维度的模型配置模型设计多租户 AI 平台和普通多租户系统最大的区别在于租户不只是数据隔离还涉及用哪个模型、用哪个密钥、走哪个通道、算谁的账。所以配置模型要围绕租户展开。我通常设计三张核心表tenant租户基本信息、tenant_model_config租户的模型配置、tenant_quota配额与用量。tenant_model_config里关键字段包括租户 ID、模型供应商标识、模型名称、API 端点、加密后的密钥、是否默认、优先级、超时时间、最大 token 数。为什么要存优先级因为生产环境经常需要降级主模型超时或限流时自动切到备用模型。这个字段就是降级链的依据。密钥绝对不能明文存。我一般用对称加密如 AES-GCM加密后入库密钥本身放在环境变量或配置中心和数据库分离。这样即使数据库泄露攻击者也拿不到可用的密钥。这一点在合规审查时几乎是必查项。3.2 动态选择 ChatModel 的实现思路Spring AI 的ChatModel是接口我们可以为每个租户配置构建独立的实例并用一个工厂类按租户 ID 缓存。核心逻辑是请求进来先解析租户查配置命中缓存就直接用没命中就构建并放入缓存带过期时间。public ChatModel resolve(String tenantId) { return cache.get(tenantId, id - { TenantModelConfig cfg configRepo.findDefaultByTenant(id); return switch (cfg.getProvider()) { case openai - OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl(cfg.getEndpoint()) .apiKey(decrypt(cfg.getApiKey())) .build()) .defaultOptions(OpenAiChatOptions.builder() .model(cfg.getModelName()) .temperature(cfg.getTemperature()) .build()) .build(); default - throw new IllegalStateException(unsupported provider); }; }); }这里用 Caffeine 做本地缓存设置expireAfterWrite比如 10 分钟这样配置变更后最多 10 分钟生效避免每次请求都查库。如果租户量大、配置变更频繁可以加一个配置变更事件主动失效缓存。3.3 密钥轮换与限流的工程细节密钥轮换是个容易被低估的需求。租户可能因为安全策略定期换密钥也可能因为泄露紧急更换。如果平台不支持热更新就得重启服务这在生产环境不可接受。我的做法是配置表加version字段更新时版本号加一缓存 key 带上版本号新请求自然用新配置旧请求继续用旧配置直到结束。这样实现了平滑切换。限流则要分两个维度租户维度和模型供应商维度。租户维度防止单个租户打爆平台供应商维度防止平台被上游限流。我一般用 Redis 令牌桶key 设计成rate:tenant:{tenantId}和rate:provider:{provider}。当供应商维度触发限流时自动走降级链切到备用模型而不是直接报错给用户。这个降级逻辑放在编排层和模型选择解耦。注意限流阈值不要设成固定值最好根据历史用量动态调整。我见过固定阈值设太低导致正常业务被误限的案例也见过设太高形同虚设的。建议先观察一周真实流量取 P99 的 1.5 倍作为初始值。4. Agent 编排层把一次问答升级成能干活的任务流4.1 Agent 和普通对话的本质区别普通对话是输入—模型—输出一问一答。Agent 的核心区别在于它能决定下一步做什么可以调用工具、可以多轮推理、可以根据中间结果调整策略。关键词里同时出现了 Agent、Agent 开发、Agent 框架、Agent 架构说明这是这个项目的重头戏。在 Spring Boot 里实现 Agent我倾向于用编排器 工具注册表的模式而不是把逻辑写死在某个 Service 里。编排器负责控制循环把用户输入和工具描述一起发给模型模型返回要调用某个工具编排器执行工具、把结果回填再发给模型直到模型给出最终答案或达到最大轮次。工具注册表负责管理所有可被调用的工具每个工具有名称、描述、参数 schema 和执行方法。为什么用这种模式因为它把模型决策和工具执行解耦了。新增一个工具只需要注册不用改编排逻辑换模型也不影响工具。这是可维护性的关键。4.2 工具调用的注册与安全边界工具是 Agent 的能力来源但也是最危险的地方。一个能执行 SQL、能发 HTTP 请求、能读写文件的工具如果参数没校验就是安全漏洞。我的原则是每个工具必须声明参数 schema执行前做严格校验执行时设超时执行后做结果裁剪。Component public class WeatherTool implements FunctionWeatherRequest, WeatherResponse { Override public WeatherResponse apply(WeatherRequest req) { if (req.city() null || req.city().length() 32) { throw new IllegalArgumentException(invalid city); } // 实际调用外部服务带超时 return weatherClient.query(req.city()); } }注册时用 Spring AI 的FunctionCallbackWrapper或对应版本的 API 把工具暴露给模型。注意工具描述要写清楚模型靠描述来决定调不调用。描述含糊会导致模型乱调或该调不调这是实测中非常常见的调优点。工具执行必须设超时。我一般用CompletableFuture.orTimeout(3, TimeUnit.SECONDS)包一层超时就返回工具执行超时让模型基于这个信息继续推理而不是让整个请求挂死。这个设计在工具依赖外部服务时尤其重要。4.3 多轮循环的终止条件与成本控制Agent 的多轮循环如果不加控制可能无限循环烧掉大量 token。必须设三个终止条件最大轮次比如 8 轮、最大总 token 数、总耗时上限。任一触发就强制结束返回当前已有结果并标注未完成。成本控制还要做 token 预估。每次调用前估算输入 token累计超过租户配额就拒绝。估算可以用简单的字符数除以 4 的近似法也可以用对应模型的 tokenizer。生产环境建议用精确 tokenizer误差小。我实测过近似法在中文场景下误差能到 30% 以上容易导致配额判断失准。另外Agent 的中间步骤要落库方便排查和回放。我一般存一张agent_trace表记录每一步的输入、输出、工具调用、耗时、token 消耗。出问题时能完整还原这对调试和计费都至关重要。5. 多租户隔离数据、配额、会话三件事必须分开做5.1 数据隔离的三种方案与选择依据多租户数据隔离有三种常见方案独立数据库、共享数据库独立 schema、共享 schema 加租户字段。AI 平台我一般推荐第三种理由是租户数量可能很多独立库维护成本高而 AI 平台的数据主要是配置、会话、用量量级可控加租户字段足够。但要注意会话内容可能包含敏感信息如果合规要求高可以对会话内容做字段级加密或者对高价值租户单独分库。这个决策要看业务不能一刀切。实现上用 MyBatis 的拦截器或 Hibernate 的过滤器自动给 SQL 加上tenant_id条件避免每个查询都手写减少遗漏风险。提示自动加租户条件时一定要处理平台管理员跨租户查询的场景否则管理员看不到数据。通常做法是提供一个显式的忽略租户注解或上下文开关并且严格限制只有管理端能用。5.2 会话状态的持久化与并发一致性会话状态不能放内存必须持久化。我一般用 Redis 存活跃会话带 TTL用数据库存历史会话。Redis 里 key 设计成session:{tenantId}:{sessionId}value 存消息列表的序列化结果。为什么用 Redis 而不是直接查库因为每轮对话都要读历史查库延迟高Redis 能把读取控制在毫秒级。并发一致性是个坑。同一个会话如果两个请求同时进来可能互相覆盖历史。解决办法是对会话加分布式锁key 用lock:session:{sessionId}拿到锁才能读写。锁的过期时间要大于单次请求最大耗时否则锁提前释放会出问题。我一般设 60 秒配合看门狗续期。5.3 配额与计费的实现细节配额分两种请求次数配额和 token 配额。请求次数好算token 要等模型返回才知道。所以我的做法是请求前预扣一个估算值返回后按实际值修正。预扣用 Redis 的原子操作避免并发超扣。计费要区分输入 token 和输出 token因为价格不同。每次调用记录prompt_tokens、completion_tokens、total_tokens按租户和模型维度聚合。聚合可以用定时任务也可以实时写时序库。我倾向于实时写一张明细表定时任务做汇总这样既能实时看用量又能出账单。这里有个经验不同供应商对 token 的计数口径不完全一致有的把系统提示算进去有的不算。做成本核算时要以供应商返回的 usage 为准不要自己估算否则对账会对不上。6. 可观测性与稳定性上线之后才真正开始6.1 监控指标该盯哪几个AI 平台的监控和普通服务不同除了 QPS、延迟、错误率还要盯模型维度的指标每个模型的调用量、平均延迟、失败率、token 消耗、限流触发次数。这些指标能帮你快速判断是平台问题还是上游问题。我用 Micrometer Prometheus 暴露指标Grafana 做看板。关键指标包括ai_request_duration_seconds按租户和模型打标签、ai_token_usage_total、ai_tool_call_total、ai_fallback_total。其中ai_fallback_total特别有用它一涨就说明主模型不稳定需要关注。日志要结构化每条 AI 请求日志带上 traceId、tenantId、sessionId、model、耗时、token。这样出问题时能按任意维度检索。我一般用 Logback 的 MDC 把 traceId 和 tenantId 放进去日志格式统一成 JSON方便采集。6.2 超时、重试与熔断的组合策略模型调用必须设超时我一般设 30 秒流式的话设首字节超时 10 秒、总超时 120 秒。重试要谨慎因为模型调用不是幂等的同样的输入可能返回不同结果而且重试会放大成本。我的策略是只对网络类错误重试最多一次且重试前检查配额。熔断用 Resilience4j按模型供应商维度配置。失败率超过 50% 且样本数超过 20 就打开熔断持续 30 秒之后半开试探。熔断打开时自动走降级链保证用户至少能拿到备用模型的结果而不是直接报错。6.3 压测与容量规划的真实数据上线前一定要压测。我压过一个中等规模的 AI 平台配置是 8 核 16G、独立线程池 64 核心线程。结果是纯文本问答在 100 并发下 P99 约 6 秒200 并发下 P99 约 9 秒300 并发开始出现排队P99 超过 15 秒。这个数据说明瓶颈不在 CPU而在模型响应时间和线程池排队。基于这个结果容量规划的思路是先确定可接受的 P99再反推最大并发。如果业务要求 P99 不超过 8 秒那这个配置大概能扛 150 到 180 并发。要提升就得加实例而不是单纯调大线程池因为上游模型本身有延迟下限。注意压测时要用真实模型或等延迟的 mock不要用立即返回的 mock否则压出来的数据毫无参考价值。我见过用 mock 压出万级并发然后上线直接崩的案例。7. 我在实际落地中踩过的几个坑第一个坑是会话历史无限增长。一开始没做裁剪一个长会话累积了几十轮每次请求都把全部历史发给模型token 消耗暴涨延迟也越来越高。后来改成滑动窗口只保留最近 N 轮加上对早期内容的摘要压缩才把成本控制住。摘要压缩要额外调一次模型但相比全量历史总体还是省。第二个坑是工具描述写得太随意。有个查询订单的工具描述只写了查询订单结果模型经常在不需要的时候调用它浪费轮次。后来把描述改成当用户明确询问订单状态、物流信息时调用参数为订单号误调用率明显下降。工具描述本质上是给模型的提示词值得反复打磨。第三个坑是租户缓存没设上限。早期用ConcurrentHashMap缓存租户配置租户多了之后内存持续上涨。换成 Caffeine 并设置maximumSize和expireAfterAccess后稳定了。任何缓存都要设上限这是铁律。第四个坑是流式输出和事务混用。流式响应过程中如果还持有数据库事务事务会一直不提交连接池很快耗尽。正确做法是流式之前把该查的查完、该写的写完流式阶段不碰事务。这个坑很隐蔽因为功能测试时并发低看不出来一上量就暴露。8. 关于这套架构后续还能怎么演进如果这套平台要继续往前走我会优先做两件事。一是把 Agent 的编排逻辑做成可配置的用类似工作流的方式描述先检索、再判断、再调用工具而不是写死在 Java 代码里。这样业务方自己就能调整流程不用每次改代码发版。关键词里出现的工作流转成代码其实反映的就是这个需求方向是对的但要注意别过度设计先支持最常见的几种编排模式即可。二是把模型评估做起来。现在选模型基本靠感觉其实可以建一个小型评测集定期跑一遍对比不同模型在真实业务问题上的表现、延迟和成本用数据驱动选型。这个投入前期不大但长期收益很高尤其是模型更新频繁的时候。最后分享一个我自己的判断标准一个 AI 平台是否达到生产级不看它支持多少模型而看它在模型出问题、流量突增、租户闹事的时候能不能稳住、能不能快速定位、能不能优雅降级。把这三件事做好比堆功能重要得多。
返回列表