Spring Boot生产级AI应用平台架构设计与实践 做 AI 应用最容易死在从 Demo 到生产这最后一段路上。Demo 阶段直接调模型接口就完事了但一旦要考虑多模型切换、流式输出、用户并发、Token 成本、内容安全、监控告警整套逻辑就必须变成一个真正有边界、有治理、能演进的产品。今年我完整做了一版“基于 Spring Boot 构建的生产级 AI 应用平台”从模型网关到 Agent 编排从监控体系到第三方开放接口算是把 Spring Boot 的生态能力在 AI 场景里重新梳理了一遍。这篇文章就讲讲我的整体架构、关键实现和几个真正踩过的坑给打算用 Java 技术栈做 AI 中台、AI 底座或者正在做 AI 项目交付的团队作参考。先说明一下背景。当时的需求是给公司内部多套业务系统提供统一的 AI 能力包括智能问答、Agent 自动化、知识库检索还要开放一小部分能力给外部合作方。这意味着我交付的不只是一个聊天机器人而是一个可以承载多个 AI 应用的平台。选型时团队内部也有争论有人建议用 Python 的 FastAPI 快速起服务模型生态也确实更好接。但最终我们还是选择了 Spring Boot 3事实证明这个决策在生产落地阶段帮我们省下了大量治理成本。1. 平台定位与整体架构设计1.1 为什么选 Spring Boot 而不是 Python 框架先把这个争论聊透。不否认 Python 在模型侧的生态价值但我们的平台不是模型训练平台而是模型应用平台。它要解决的是接入、编排、权限、计量、可观测这些事情而这些问题恰好是 Java 服务端技术栈最成熟的领域。用 Spring Boot 有几个很现实的好处体系完整Spring Security 做鉴权、Spring Actuator 做监控、Spring Validation 做参数校验、MyBatis 做持久化全部都是现成的不需要像 Python 项目那样到处拼第三方库。团队门槛低团队里 Java 工程师多Go 和 Python 的高级工程师不好招用 Spring Boot 至少不会因为没人写而卡住迭代。可观测性成熟Micrometer 的指标体系、日志框架、ELK 接入、SkyWalking 这类 APM 组件在 Java 生态里的成熟度是 Python 生态短期内追不上的。部署运维一致公司已经有完善的 Jenkins K8s 配置中心体系用 Spring Boot 可以零成本接入不必给运维团队添麻烦。那 Python 的角色是什么我保留了一个独立的模型侧服务专门处理语音转写、图片理解这类对 Python 库依赖很重的需求主体业务编排、接口暴露、数据流转全走 Java。这个混合部署的做法实测下来很稳。1.2 平台分层与模块划分平台采用 Maven 多模块架构模块边界一开始就定清楚避免了后面业务交错导致的循环依赖。ai-platform-common 通用工具、统一返回体、异常定义 ai-platform-gateway 模型网关统一接入 OpenAI 及国产大模型 ai-platform-agent Agent 编排引擎、工具注册、多模型协作 ai-platform-conversation 会话管理、Prompt 模板管理 ai-platform-knowledge 知识库切片、向量化、检索 ai-platform-openapi 第三方开放接口、API Key 管理 ai-platform-admin 运营后台、数据统计、内容审核模块间的依赖方向是单向的openapi 和 admin 都依赖 agentagent 依赖 gatewaygateway 是底层的模型出口不反向依赖任何上层模块。这样只要模型网关的接口稳定上层随便怎么迭代都不会影响模型接入。平台的总体流程是业务系统或外部合作方通过 API 发起请求 → 接入层完成鉴权和参数校验 → Agent 引擎解析意图并调度工具 → 模型网关调用底层大模型 → 结果回流并做内容安全过滤 → 流式或非流式返回。2. 模型接入层让上层应用不关心底层模型2.1 模型网关的抽象设计模型网关是整个平台的技术底座设计核心是“上层应用永远不感知具体模型”。不能因为某一天想把主力模型从国产模型换成 Claude就把所有业务代码翻一遍。我定义了一个统一的 ChatGateway 接口所有模型适配器都实现它public interface ChatGateway { ChatResponse chat(ChatRequest request); void stream(ChatRequest request, SseEmitter emitter); ChatResponse chatWithTools(ChatRequest request, ListToolDefinition tools); ChatResponse chatWithContext(ChatRequest request, ListContextMessage contexts); }请求体里只放业务相关的字段比如消息列表、温度、最大 Token 数而模型类型、API Key、Base URL 这些统统放到注册中心配置里通过 ModelRoute 策略动态选择。spring: ai: gateway: routes: - name: default-chat provider: openai-style base-url: ${LLM_BASE_URL} api-key: ${LLM_API_KEY} model: ${LLM_MODEL_NAME} temperature: 0.7 max-tokens: 2048 enabled: true这里有个细节很多平台类项目会把模型参数硬编码到某个 Service 里后面换模型时要动业务代码。为了避免这个问题我在网关层加了一个 ProviderAdapter 注册表每个提供商包一个适配器启动时通过配置自动装配。换模型时改配置就行业务代码一行不用动。2.2 流式出参的 SSE 工程化大模型应用的流式返回是刚需用户不愿意等五六秒再看一整段答案。Spring Boot 里最直接的做法是用 SseEmitter把流式响应当成异步任务往外推。GetMapping(value /v1/chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(RequestParam String prompt, RequestHeader(value X-Trace-Id, required false) String traceId) { SseEmitter emitter new SseEmitter(0L); CompletableFuture.runAsync(() - { try { ChatGateway gateway modelRouter.route(); gateway.stream(new ChatRequest(prompt), emitter); } catch (Exception e) { emitter.completeWithError(e); } }, aiTaskExecutor); return emitter; }注意 SseEmitter 构造函数里的超时参数我直接传 0L表示不主动超时。因为大模型流式输出的时长非常不稳定如果设置 30 秒超时用户问题稍微复杂一点就会断流体验很差。真正的时间控制要放在网关层针对每次模型调用设置读取超时同时通过队列把并发排队控制在合理范围。断连处理是另一个容易踩坑的点。浏览器或者客户端可能中途关闭SseEmitter 会抛出 IOException需要在回调里优雅释放资源不能让它把线程池里的工作线程拖死。我封装了一个 HeartbeatSseEmitter每 10 秒发送一个注释行既保持连接活跃又能及时发现异常断开的连接。2.3 超时、重试与限流生产级的门槛模型接口不是公司内部服务它的超时、抖动、限流都是常态。生产环境必须把这三件事做成体系。超时拆成两种连接超时和读取超时。连接超时我统一设置 3 秒读超时根据场景分档——普通问答 60 秒开放接口 120 秒。所有超时参数都放配置中心不能用常量写死。重试策略模型调用失败时直接重试并不安全因为大模型计费是实打实的。我用指数退避重试只在网络层异常或 5xx 错误时重试业务侧异常直接向上抛。另外记录每一次重试前后的 response方便后续复盘成本损耗。限流是必须的AI 应用的 Token 成本比普通接口高出几个数量级如果没有限流恶意刷接口能直接刷爆预算。我在网关层接入了基于 Redis Lua 的滑动窗口限流按用户维度控制每秒请求数也按模型维度控制每分钟 Token 消耗量超过阈值直接返回 429并在响应头里带上 Retry-After 信息。3. Agent 编排引擎与多模型协作3.1 ReAct 循环的运行机制Agent 引擎是平台上最复杂的模块。所谓 Agent简单理解就是一个“能使用工具的对话机器人”它会根据用户目标决定调哪个工具、按什么顺序调、以及怎么处理工具返回的结果。我采用的是 ReAct 循环核心流程如下接收用户请求加载对话上下文和已注册工具列表。组装系统指令交给模型期望模型返回工具调用参数或最终答案。如果返回的是工具调用请求校验参数并执行对应 Tool Bean。把工具执行结果作为观察内容回填给模型。重复执行直到模型输出最终答案或达到最大步数上限。public AgentResult execute(AgentRequest request) { AgentContext ctx new AgentContext(request); int maxSteps 8; while (ctx.getStepCount() maxSteps) { String instruction promptBuilder.build(ctx); LLMResponse resp chatGateway.chatWithTools(instruction, ctx.getTools()); ToolCall call toolCallParser.parse(resp); if (call null) { return ctx.finish(resp.getContent()); } Object toolResult toolRegistry.invoke(call); ctx.appendObservation(call, toolResult); ctx.incrementStep(); } return ctx.finish(达到最大迭代步数请简化问题或调整指令); }最大步数为什么设 8因为经过压测步数太低复杂任务完不成步数太高容易陷入工具调用死循环8 步足够覆盖绝大多数业务场景同时能把单次请求的时长和 Token 消耗控制住。这个值我建议按业务实际压测结果定不要盲目抄。3.2 Function Calling 的注册与安全边界工具注册机制就是 Spring Bean 的自动扫描机制核心是把一个普通方法变成模型可感知、可调用的 Function。实现方式是用自定义注解AgentTool启动时扫描所有带这个注解的 Bean自动生成 OpenAPI 风格的工具描述信息。AgentTool(name query_order, description 根据订单号查询订单状态和金额) public ToolResult queryOrder(ToolParam(description 订单号格式如 SO20250101) String orderNo) { OrderVO order orderService.queryByNo(orderNo); return ToolResult.success(order); }工具注册后会生成 JSON Schema随系统提示一起发送给模型。这里有一个非常重要的安全边界工具的执行权限不能由模型自己决定必须由平台执行上下文控制。比如模型可能根据对话历史判断调用“删除订单”工具但实际用户权限并不是管理员那平台就应当在工具校验环节拦截并返回“无权限执行”的结果而不是真的去调删除接口。我在 ToolRegistry.invoke 前加了一个 PermissionCheckInterceptor根据当前请求用户上下文校验工具级别的 ACL这一步能挡住绝大多数越权风险。3.3 多 AI 协作的编排策略多 AI 协作不是多个模型跑一遍取结果而是一种有计划的分工。我目前支持三种编排模式。串行编排适合有强依赖的任务流比如先调用信息抽取模型识别用户提到的城市再把城市信息传给天气查询工具最后让生成模型润色回答。并行编排适合互相独立的任务比如同时让两个不同模型针对同一份代码做 Review再汇总评分可以显著降低响应时延。路由编排前置一个轻量模型做意图识别按识别结果路由到不同专用模型比如敏感问题路由到审核模型普通问题路由到知识模型。实现上我用 CompletableFuture 来做并行编排同时控制并行度不超过 3。原因是大模型调用本身消耗带宽和 CPU并行度开太大会把线程池打满反而拖垮整体吞吐。3.4 并发控制AI Agent 怎么扛住生产流量这是搜索热词里被问得最多的一个问题。AI Agent 和普通接口不一样普通接口是计算几毫秒返回Agent 一个任务可能长达几十秒。如果按照普通接口的思路直接用 Tomcat 线程池扛所有请求几十个慢任务就能把线程池打穿。我的方案是三层流量治理第一层入口信号量限流。Agent 执行入口处放一个 Semaphore控制全局同时执行的 Agent 数量。这个数值根据下游模型服务的配额和压测结果来定我这边压测出单机同时运行 40 个 Agent 任务时 QPS 和延迟曲线最优于是把信号量设为 40。第二层模型网关令牌桶。架构上和业务无耦合在网关层按模型维度做 Token 维度限流防止某个 Agent 循环调用工具消耗大量 Token。第三层任务排队与优先级。超出信号量范围的请求进入阻塞队列按用户等级分配优先级。这个机制保证了 VIP 用户的请求不会排在大量免费请求的后面。实测效果单机 4C8G 的实例在压测下能稳定支撑 60 QPS 的普通问答Agent 类的长任务并发控制在 40整体没有出现过线程池拒绝和模型限流失控的情况。4. 会话持久化、Prompt 管理与 RAG 落地4.1 会话与消息的表结构设计平台一旦暴露给多个业务方会话数据的表结构就必须好好设计。我用了 conversation 和 message 两张表下面是精简后的结构CREATE TABLE conversation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, app_key VARCHAR(64) NOT NULL, agent_id VARCHAR(64) NOT NULL, title VARCHAR(200), status TINYINT DEFAULT 1, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL ); CREATE TABLE message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_id BIGINT NOT NULL, role VARCHAR(16) NOT NULL, content TEXT NOT NULL, model_name VARCHAR(64), tokens INT DEFAULT 0, latency_ms INT DEFAULT 0, trace_id VARCHAR(64), create_time DATETIME NOT NULL );message 表里的 model_name、tokens、latency_ms 是全链路成本核算的原始数据。我建议所有真实项目都要把这三个字段设计进去否则后面算模型月度消耗时只能拍脑袋。会话上下文不会全量塞进每次请求而是通过一个上下文摘要字段定期滚动压缩避免对话十几轮以后 Token 消耗爆炸。4.2 Prompt 模板的版本化管理Prompt 模板是运营属性非常强的资源不能硬编码在代码里。我把每个 Prompt 模板存成数据库记录包含模板编码、内容、参数 schema、版本号、启停状态。每次修改生成新版本线上运行中的会话继续用旧版本新会话用最新版本。模板变量解析用简单的${变量名}占位符避免引入太重的模板引擎。但变量替换后要做一次长度校验防止用户输入把整个 Prompt 撑爆绕过模型的输出限制。Prompt 管理的另一个要点是评测。每个模板可以绑定一组评测用例发布前离线跑一遍对比新旧版本的输出质量。这说起来简单真正落地时很多人会忽略等到线上效果退化再排查就晚了。4.3 知识库检索增强的工程化切入点RAG 是生产级 AI 平台避免不了的能力。我实现了一套兼顾工程落地和检索效果的简化流程文档上传后拆分成切片切片大小按 embedding 模型支持的 Token 上限调整。切片写入向量数据库同时把文本原文冗余存一份 MySQL。检索时先用关键词粗筛再用向量相似度精排取 TopN。把检索结果拼入 Prompt 上下文时严格控制拼接总量不超过模型输入窗口的 50%避免因为上下文过长导致模型截断。向量数据库我选了支持 MySQL 协议风格的方案运维上可以和现有数据库体系统一管理。在 Spring Boot 里的集成就是一个基于 JdbcTemplate 的向量检索模块支持余弦相似度查询实测几百万条向量数据下的召回耗时在百毫秒级够用。第 4 步有个容易忽略的坑检索结果如果超过上下文窗口不是简单截断而是要按相关性从高到低丢弃。否则模型可能因为关键信息被截掉而答非所问。5. 可观测性、安全合规与第三方开放接口5.1 Actuator Spring Boot Admin 的监控体系生产级平台必须有完整的监控体系没有监控的 AI 平台就是盲飞。我基于 Spring Boot Actuator 暴露指标再用 Spring Boot Admin 做可视化面板。但除了系统级指标我维护了一套更关键的 AI 业务指标指标名类型说明ai.llm.requests.totalCounter模型请求总数ai.llm.tokens.inputCounter输入 Token 总消耗ai.llm.tokens.outputCounter输出 Token 总消耗ai.agent.stepsHistogramAgent 执行步数分布ai.agent.latency.secondsHistogramAgent 任务耗时分布ai.tool.error.totalCounter工具调用错误数ai.quota.reject.totalCounter配额拒绝次数这些指标用 Micrometer 注册Prometheus 采集Grafana 展示。特别要关注ai.agent.steps这个指标如果某天分布整体偏高说明 Agent 陷入了不必要的工具循环得第一时间检查 Prompt 模板。5.2 日志链路追踪与 Token 成本核算AI 应用的排障非常依赖链路追踪。一次用户请求可能要经历鉴权、RAG 检索、模型调用、工具执行、内容审核好几跳没有 traceId 根本没法定位问题。我在网关过滤器里生成 traceId写入 MDC然后在 JSON 日志里统一输出。所有模型调用日志都会带上这次请求的 traceId、模型名、Token 数、耗时和错误信息。这样从用户反馈到日志定位只需要一条命令。Token 成本核算依赖第 4.1 节的 message 表。我写了定时任务每天汇总每个 app_key 的 Token 消耗再结合模型单价生成成本报表。上线第一周就看到有两个内部系统的调用量异常及时给它们加了配额—如果等到月底账单出来才发现成本损失就大了。5.3 面向第三方的 Open API 应该如何设计被问“Spring Boot 对外提供的接口应该放在哪里”时我的回答是单独拆一个 openapi 模块不让第三方接口和内部业务接口混用。混在一起最直接的后果是安全策略没法差异化内部接口用的鉴权模型也不适合给外部系统用。Open API 设计关键点API Key 机制每个合作方发独立的 Key活期可吊销比共用账号体系安全得多。签名校验除 API Key 外请求体带上签名和时间戳防止请求被篡改和重放。独立配额每个 app_key 有独立的 QPS 和 Token 配额。独立限流维度在模型网关层按 app_key 而不是按用户维度限流。外部接口的数据格式也做了规范化统一返回code/message/data结构并且错误码分了模型侧错误、业务侧错误、配额侧错误三大类让第三方对接方处理起来更顺畅。5.4 内容安全过滤链路是平台的底线这一点必须放在特别重要的位置。生产级 AI 平台的合规要求比 Demo 高得多一定要在输入和输出两侧都设置内容安全过滤。我实现的是一条双通道过滤链路输入侧过滤用户在请求进入 Agent 前先过一遍敏感内容检测命中直接拦截并返回提示。输出侧过滤模型生成的答案在返回用户前再做一轮合规审核发现问题时回滚为预置的兜底回复同时记录审计日志。双向审核输入和输出都记录了完整的审核结果方便后续追溯。这里的关键不是把某个敏感词库做得很全而是建好审核管道。敏感词库会持续更新所以我把审核逻辑做成了责任链模式各类审核模块可以独立扩展。只要新增一个审核器就能在不动主流程的情况下接入新的规则。6. 部署配置、性能调优与压测实录6.1 多环境配置与配置中心AI 应用平台的配置项比普通 Web 项目多得多除了正常的数据源和 Redis还有模型路由、Prompt 模板、审核规则、配额策略。全部塞进 application.yml 会让配置文件失控。我用 Nacos 做配置中心所有环境共享一份配置模板用环境变量和配置集做差异覆盖。关键敏感配置全部走环境变量注入不落配置文件里。这里有个原则我个人强烈建议API Key 和 Secret 绝对不写进代码仓库哪怕是私有仓库不然泄露风险和审计麻烦都会找上门。6.2 容器化部署与优雅停机部署用 Dockerfile 多阶段构建Java 版本 17运行时镜像采用 JRE 精简版本。平滑下线这块Spring Boot 3 内置了优雅停机配置server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s但实际踩到的坑是即使 Spring 容器优雅停机了如果线程池里还有大模型请求在等响应JVM 也不会立即退出而是等超时。所以我把模型网关的线程池单独配置了 shutdown 钩子先停止接收新请求再等待存量任务最多 30 秒超时直接中断。配合 K8s 的 preStop hook 和 readiness 探针可以实现滚动发布时基本无感知。6.3 压测数据与 JVM 参数调优压测是在 4C8G 的容器里做的写出来给读者一个经验参考而不是一个承诺。纯问答场景8 并发TP99 约 1.8 秒单实例 QPS 峰值约 80。Agent 工具调用场景单实例同时支撑 40 个 Agent 任务任务平均耗时 12 秒暂无明显资源瓶颈。压力测试后发现瓶颈在线程池和下游模型接口不在 CPU。JVM 参数没有乱堆主要是设置了合理的堆大小和 GC 策略。对于这种长时间运行的在线服务我偏向用 G1 收集器并控制最大堆 4Gjava -Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis100 -jar ai-platform.jar另外一个容易被忽略的点是DNS 解析超时。模型网关每次请求都要解析模型服务域名压测时出现过 DNS 查询拖慢请求的情况后来在 JVM 参数里加上-Dsun.net.client.defaultConnectTimeout3000并配合本地 DNS 缓存问题解决。7. 踩坑实录与问题排查速查表7.1 高频问题速查表把实战中的高频问题整理成速查表建议读者直接收藏问题现象根本原因解决方案流式接口偶发中断SseEmitter 超时设置过短设置 0L 并配心跳线程Agent 循环调用工具工具返回结果格式模型解读不了结果统一封装成结构化 JSON模型返回 JSON 频繁解析失败模型输出不稳定开启 JSON Mode外加正则兜底解析压测时大量 Connection Reset连接池数量不足调大 HttpClient 连接池并复用某个用户耗尽所有 Token 配额配额按用户维度限流失效加全局 Token 配额管理日志里大量超时告警线程池现在排队严重改信号量限流避免排队堆积审核拦截误伤正常请求词库规则太宽泛分层词库精确词 组合词规则配置变更不生效未配置 Nacos 监听刷新加 RefreshScope 并验证7.2 几条值得长期遵守的实战经验最后分享几条真正让我少走弯路的经验。第一所有模型调用必须记录 Token 消耗即使现在不计费也要记录。这不仅是成本问题更重要的是可以基于 Token 数据做配额分配和异常检测。我接手过一个项目因为没有这个数据根本无法回答“哪个业务方消耗了最多资源”这个基本问题。第二Agent 工具在测试环境必须做 Mock。直接连真实模型开发工具时你永远在跟模型的不确定性搏斗根本区分不清到底是工具写错了还是模型没理解。Mock 模型返回固定 JSON 后开发调试效率提升了至少 50%。第三给所有模型调用加“审计日志”记录输入输出的摘要、traceId、命中审核规则的情况。这是线上排查问题时最值钱的数据。有一次用户反馈回答内容不对靠审计日志定位到是 Prompt 模板里一个变量引用错误半小时内就修复并补发了新版本。第四新模型接入时不要直接替换生产流量先在灰度路由里跑一周对比旧模型的响应质量和 Token 消耗。我遇到过新模型在同一 Prompt 下 Token 消耗比旧模型高 40% 的情况如果没有灰度对比成本悄无声息就上去了。做一个生产级 AI 应用平台本质上不是写好一个接口就行而是把模型能力、业务编排、安全合规、成本控制这些环节都变成工程体系里可管理的一部分。Spring Boot 的成熟生态帮我把这些基础设施问题都兜住了我只需要专注在 AI 场景的差异化逻辑上。这套架构从上线到现在跑了大半年稳定性和可扩展性都经住了验证后续再往里加新的模型服务或者新的业务场景时你会发现当初多花时间设计好的边界才是真正省时间的开始。