
搞了大半年我终于把一套基于 Spring Boot 3 的 AI 应用平台推进了生产环境现在它每天承载几十万次大模型调用。严格来说它不只是一个“调大模型接口”的后端服务而是一个真正能落地的业务系统要接不同厂商的大模型、要支持智能体调用工具、要做流式回答、要管会话记忆还得有一整套监控和容错机制。这篇文章就从我自己的实际经历出发讲清楚怎么设计这样一个平台以及落地过程中那些躲不开的坑。如果你正在用 Java 生态承接大模型能力或者想把 AI 功能整合进现有的 Spring Boot 系统这篇内容应该能给你不少参考。我会把架构设计、核心模块、生产监控、实战代码、故障排查一次性说透内容偏实战不堆概念。有些东西我在网上找遍了也很少有人讲透比如 AI Agent 工具调用在 Java 里怎么做、流式接口被 Nginx 缓存后为什么前端半天不出字。希望这篇能帮你少走几个月的弯路。1. 先把平台边界划清楚整体架构与设计思路1.1 这个 AI 应用平台到底解决什么问题在真实的业务场景里AI 能力从来不是单独一个聊天框而是散落在智能客服、知识问答、内容生成、数据分析、内部辅助工具等各个地方。如果每个业务系统都直接对着云厂商 SDK 调接口后续模型升级、多厂商切换、成本核算、权限审计都会变成一团乱麻。我们做这个平台的核心目标就是把大模型能力抽象成一套统一服务业务方只需要传入会话上下文和业务参数就能拿到结构化的回答、工具调用结果或者流式推送。平台本身要负责的公共逻辑非常多模型协议适配、模型路由、上下文管理、限流熔断、安全审计、日志监控、成本统计。这些逻辑如果散落到各个业务代码里几乎不可能做到生产和治理。所以第一件事就是把“AI 能力”和“业务流程”切开。业务方只依赖平台的 SDK 或 OpenAPI平台内部再通过模型接入层对接不同的大模型供应商。这样做还有一个额外好处升级模型的时候业务方可能完全无感知。这块我踩过的第一个坑就是“过度设计”。一开始我还想自己做一套模型编排 DSL后来发现团队里没人愿意学。最后砍到只剩三层统一接入 API、会话与 Agent 编排、模型供应商适配。不要一开始就追求大而全先把一条链路跑通再逐步加能力这个平台才会真正长成可维护的样子。1.2 技术选型与分层结构我们选型如下Spring Boot 3.x Java 17 WebFlux Redis PostgreSQLpgvector RabbitMQ监控用 Spring Boot Admin Prometheus Grafana。选择 WebFlux 的原因很直接大模型接口本质上是长连接和流式响应每个请求都可能持续几十秒同步 Servlet 模型在这种场景下会占用大量线程而 Reactor 响应式模型能更高效地处理这种动态吞吐。不过要说明如果你的团队完全不熟悉响应式编程用 Spring MVC SSE Java 21 虚拟线程也能做后面我会讲两种方案的取舍。从整体分层来看我大致把平台拆成四个层次接入层REST API、SSE、WebSocket给前端和业务系统调用。编排层会话管理、Agent 编排、工具调用、RAG 检索流程。模型层OpenAI 兼容协议、云厂商 SDK、私有化模型服务统一做适配。基础设施层Redis、PostgreSQL/pgvector、MQ、Actuator、Prometheus、Grafana。这个分层的好处是责任清晰。接入层只负责协议和鉴权编排层只关心业务逻辑模型层不感知上游业务基础设施层提供公共能力。任何一层出问题都能在日志和监控里快速定位。尤其是模型层如果某个云厂商模型短时不可用不应该让整个平台跟着抖动这一层必须做隔离和降级。1.3 为什么不是 FastAPI 而是 Spring Boot很多人问我大模型开源生态基本都在 Python 里为什么不直接用 FastAPI这个问题我觉得要分开看。FastAPI 写原型确实快Python 的 AI 库也更顺手但生产级系统要考虑监控、配置中心、网关、权限、用户体系、交易链路这些东西在多数公司里都是 Java 生态。让 AI 平台独立在 Python 体系里运维、审计、成本核算全都得另起炉灶这是很大的隐性成本。Spring Boot 3 的优势在于它已经是一个非常成熟的微服务底座。大模型调用本身并不复杂复杂的是把它和业务系统衔接起来。我们用 Java 封装 OpenAI 协议、云厂商 SDK、工具调用已经跑得很稳定。当然遇到训练脚本、模型评测、数据处理这类偏算法的任务我仍然建议用 Python 单独做服务不要硬塞进 Java。技术选型从来不是“哪个语言更高级”而是“哪个语言更合适”。我的结论是业务型 AI 平台用 Spring Boot 更稳算法型工具链继续留 Python两者通过服务化接口协作是性价比最高的方案。2. 核心功能模块设计与关键实现2.1 大模型统一接入层SPI 加策略模式接入层是所有业务方调用 AI 能力的唯一入口也是整个平台最容易做乱的地方。我的做法是做三层抽象统一请求模型、协议适配器、模型路由。统一请求模型不关心底层是 OpenAI 还是通义千问只关心角色、消息、温度、top_p、max_tokens 这类通用参数。每个模型供应商实现同一个 ChatClient 接口内部完成协议转换、鉴权签名和超时设置。public interface ChatClient { FluxChatChunk chat(ChatRequest request); } Component public class OpenAiChatClient implements ChatClient { private final WebClient webClient; public OpenAiChatClient(WebClient.Builder builder) { this.webClient builder.baseUrl(https://api.openai.com/v1).build(); } Override public FluxChatChunk chat(ChatRequest request) { return webClient.post() .uri(/chat/completions) .header(Authorization, Bearer apiKey) .bodyValue(buildPayload(request)) .retrieve() .bodyToFlux(SseChunkDto.class) .map(this::toChatChunk); } }为什么用 WebClient 而不是 RestTemplate因为 WebClient 是非阻塞的可以配合 WebFlux 的响应式流特别适合处理模型接口返回的 SSE 流。如果你在 Spring MVC 里用 RestTemplate 读流很容易把业务线程阻塞住并发一高就出事。模型路由是在接入层之上再包一层。每个请求需要指定 modelName路由服务会从配置中心读取路由表决定把请求发给哪个供应商。多 AI 协作其实就是在这个层实现的比如主回答走模型 A摘要和标题走模型 B二者可以并行调用也可以在编排层串行协作。路由表大致像下面这样模型标识供应商场景权重备用策略primary-chat云厂商A对话主模型80%切换到云厂商Bcheap-summary开源本地模型摘要100%切换成A的最小型号embedding-zh向量模型知识库100%公共向量服务这套设计让后端在模型升级或厂商切换时非常从容。比如某个模型因为政策或者成本原因不能继续用了只需要改配置和路由表业务系统不用动一行代码。2.2 流式响应SSE 与响应式背压AI 对话最友好的协议就是 SSE也就是 Server-Sent Events。前端用 EventSource 或者 fetch 读流后端一条条往下推。Spring MVC 可以直接返回 SseEmitterWebFlux 则可以返回 FluxServerSentEvent 。我最终选了后者因为响应式流能天然处理终止、异常和背压这是 SseEmitter 完全做不到的。PostMapping(value /api/chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString streamChat(RequestBody ChatRequest request) { return chatOrchestrator.chat(request) .map(chunk - ServerSentEvent.builder(chunk.getContent()) .event(message) .build()) .onErrorResume(ex - Flux.just( ServerSentEvent.builder([ERROR] ex.getMessage()) .event(error) .build() )); }这里有几个关键细节。第一不要在大模型响应流上做全局缓存否则首字延迟会非常难看。模型返回的第一个 token 应该尽快透传给前端。第二超时时间要分两层看连接空闲超时不能太长但读超时一定要放大到 60 秒以上因为模型生成 500 个 token 可能真的需要 20 秒。第三前端断开连接时一定要把下游 Model 调用也取消掉否则后台还在继续生成内容白白浪费 token 费用。关于线程模型如果你的技术栈还是 Spring MVC可以考虑用 SseEmitter 配合虚拟线程。Java 21 的虚拟线程让每个请求不再独占一个平台线程能大幅提升并发能力。但 WebFlux 的流式处理能力还是更完整尤其当你要做多个模型的并行流式合并时Reactor 的组合子真的能省不少代码。2.3 AI Agent 与工具调用要让 AI 平台真正“干活”不能只是聊天得让它能调用业务工具。比如查订单、查库存、发工单、计算指标。现代大模型基本都支持 function call平台要做的是把业务工具注册成 JSON Schema让模型在需要时返回结构化调用参数平台再去执行并把结果回传给模型。我用一个 AiTool 接口来管理所有工具public interface AiTool { String name(); String description(); String parametersJsonSchema(); String execute(String argumentsJson); }每个工具都是一个 Spring Bean实现这个接口后自动注册到工具表里。Agent 编排层把这些工具列表连同系统提示词一起发给模型。模型如果觉得需要查天气会返回类似这样的调用{ name: query_order, arguments: {\orderId\: \20250110001\} }平台收到后再执行 queryOrderTool.execute(arguments)把结果塞回消息列表里继续调用模型直到模型给出最终答案。这个“工具调用循环”必须设置最大轮数比如 5 轮否则模型可能陷入死循环费用会一直涨。int round 0; while (round maxRounds) { ChatResponse response chatClient.chat(currentMessages); if (response.hasToolCall()) { for (ToolCall call : response.toolCalls()) { AiTool tool toolRegistry.get(call.name()); String result tool.execute(call.arguments()); currentMessages.add(new ToolMessage(call.name(), result)); } round; continue; } break; }多 Agent 协作其实也可以在这一层实现。每个 Agent 本质上是一套独立的系统提示词、工具集合、模型配置。平台可以维护一个 AgentSession记录当前 Agent 的状态、调用轮数、临时变量。某些场景下需要两个 Agent 协作比如一个负责生成 SQL另一个负责校验 SQL 安全性再返回给用户。这个机制并不神秘拆开看就是多轮函数调用加会话管理。2.4 会话记忆与 RAG 知识库生产级 AI 平台绕不开两个问题如何管理长对话的上下文以及如何让模型回答“自己私有知识库里的内容”。先说上下文。所有消息都丢给模型既不现实也贵。一个很常用的策略是 Token 预算加消息裁剪新消息永远保留越早的消息越可能被压缩成摘要中间省略的消息必要时从向量库召回。我们用的是 PostgreSQL 加 pgvector。刚开始不需要上 Elasticsearch业务量到了再换也来得及。上传的知识文档在离线任务里切块、向量化、写入 knowledge_chunks 表。检索时先给用户问题生成向量再在 pgvector 里做相似度查询SELECT content, chapter, page FROM knowledge_chunks ORDER BY embedding $1 LIMIT 5;这里是余弦距离操作符。我一般会再加一个相似度阈值比如只保留距离小于 0.25 的片段避免把不相关内容硬塞给模型。RAG 这块最常见的错误是检索结果不加过滤直接全量注入结果模型被无关信息干扰回答质量反而下降。好的做法是先做粗排再做轻量重排只把相关性最高的 3 到 5 个片段拼接进系统提示词。会话记忆的持久化我放到 Redis 缓存里并设置过期时间。超过一定轮数的旧消息异步写入 PostgreSQL生成摘要存进“记忆表”。下次用户再来先读取摘要和最近 N 条消息组合成上下文。这样做既能控制成本也能让长对话在重启后不丢失用户体感会好很多。3. 生产级落地稳定性、监控与可观测性3.1 用 Actuator 和 Spring Boot Admin 做应用体检做 AI 平台和做普通 CRUD 最大的不同是外部依赖变成了一个大模型它不稳定、延迟高、错误类型多。所以监控不能只看 CPU 和内存还要盯着模型调用本身。Spring Boot Admin 用来做应用状态总览很直观能直接看到实例列表、健康状态、线程数、内存使用、环境变量适合运维同事快速判断“服务是不是挂了”。我更依赖的是 Micrometer 加 Prometheus。暴露/actuator/prometheus端点后可以让采集器定期抓取。除了常规指标我格外关注四类 AI 业务指标指标类型含义ai_chat_requests_totalCounter总请求数按模型和接口拆分ai_chat_tokens_totalCounter输入输出 token 总数用于成本核算ai_chat_first_token_latencyTimer首 token 延迟ai_chat_stream_completed_totalCounter流式完成次数区分成功和失败这些指标看起来简单但要在代码里认认真真埋点。用 Micrometer 的注解或者编程式 API 都能做关键是不要漏掉模型名和供应商标签。否则一旦某个模型出问题你很难一眼定位到是哪家供应商导致整体成功率下降。3.2 全链路日志与 TraceId 设计AI 问题排查起来特别痛苦因为一条回答可能要经历用户请求、网关、平台、模型供应商、工具调用好几个环节。如果日志里没有统一的 TraceId出了问题只能靠时间“对表”非常低效。我们的方案是在入口 WebFilter 里生成或透传 TraceId放到 MDC 上下文里所有日志都自动带上。Component public class TraceIdFilter implements WebFilter { Override public MonoVoid filter(ServerWebExchange exchange, WebFilterChain chain) { String traceId exchange.getRequest().getHeaders() .getFirst(X-Trace-Id); if (traceId null || traceId.isBlank()) { traceId UUID.randomUUID().toString(); } return chain.filter(exchange) .contextWrite(ctx - ctx.put(traceId, traceId)); } }每条日志里除了 TraceId我还会额外记录 userId、sessionId、modelName、round 等信息。AI 调用日志单独落到一张表或者独立的日志文件内容包括请求摘要、响应摘要、token 用量、耗时、错误码。这样无论是排查用户投诉还是做安全审计都有据可查。不要只记“成功/失败”那样根本没法定位是模型拒绝回答还是工具执行出错。3.3 限流、熔断与重试策略大模型接口的成本和延迟都远高于普通数据库操作所以平台层必须做多维度限流。我们用的是 Redis 配合令牌桶按用户、按接口、按模型供应商分别限流。比如每个普通用户每分钟最多 20 次请求每个模型供应商每分钟最多 5000 次请求。一旦超限直接返回 429 并提示稍后重试。熔断我用 Resilience4j。某个模型供应商连续失败率达到阈值时直接打开熔断器后续请求在短时间内不再打到这个供应商而是走备用模型或者降级文案。配置大致如下resilience4j.circuitbreaker: instances: llmChat: registerHealthIndicator: true slidingWindowSize: 20 minimumNumberOfCalls: 10 failureRateThreshold: 40 waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 3失败重试要特别谨慎。大模型接口不是天然幂等的同样一条消息发两次可能收两次费用。我的原则是只有连接超时、HTTP 408、HTTP 429、HTTP 5xx 这类“可能还没被处理”的错误才重试像 400 参数错误、401 鉴权失败、模型返回内容审核失败坚决不重试。重试次数控制在 1 到 2 次并且用指数退避加随机抖动防止瞬间打爆供应商。3.4 异步化与资源隔离WebFlux 是非阻塞的但项目里不可能所有组件都是响应式的数据库访问、消息发送如果用了同步阻塞实现同样会卡住 EventLoop。我的做法是把这些阻塞操作放到独立的弹性线程池里用publishOn切换调度器。另外针对不同模型供应商做了线程池隔离。比如 A 模型延迟变高最多拖垮 A 的线程池不会把 B 模型的流量也堵住。在线程池选择上我踩过使用无界队列导致内存暴涨的坑。后来统一使用有界队列并设置 CallerRunsPolicy 或者自定义的降级策略。当线程池满了直接返回“当前模型服务繁忙请稍后重试”而不是把任务无限堆积在内存里等待。生产环境最怕的不是报错而是慢到像死机一样的长时间卡顿。宁可快速失败也不能拖垮整个平台。4. 从 0 到 1 实战搭一个能用的智能问答模块4.1 工程骨架与关键依赖真正动手的时候我建议按 Module 拆分但不要一开始就拆成十几个服务。合理的方式是单应用内部分包等团队规模大了再拆出去。一个最小可运行的生产级平台至少需要这些依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependencyapplication.yml里我一般会做下面几件事打开所有 Actuator 端点但只暴露到内网或网关给 Redis 配好连接池给 WebClient 设置全局连接超时和读超时。模板配置大概是spring: data: redis: host: ${REDIS_HOST} timeout: 3s management: endpoints: web: exposure: include: health,info,prometheus,metrics endpoint: health: show-details: always4.2 写一个带普通上下文管理的 SSE 接口这里给一个最小实现思路。Controller 只需要接收业务参数转给 ServiceService 里做会话读取、拼装消息、调用模型。为了演示我把上下文管理简化成 Redis 里存最近六条消息Service public class ChatOrchestrator { private final StringRedisTemplate redisTemplate; private final ChatClient chatClient; public FluxChatChunk chat(ChatRequest request) { String sessionKey session: request.getSessionId(); ListChatMessage history loadHistory(sessionKey, request.getContent()); return chatClient.chat(new ChatRequest(history)) .doOnNext(chunk - saveHistory(sessionKey, request.getContent(), chunk)); } }注意这里doOnNext里不能做阻塞 IO否则会严重影响性能。正确做法是每完成一轮后单独异步保存历史或者在流结束后一次性保存。实际生产里我们还会把“首次响应时间”和“完整响应时间”分别打点上报方便分析模型供应商的真实质量。4.3 接知识库用 pgvector 做一次向量检索知识库问答的核心是三步把用户问题向量化、在向量库里找相似片段、把片段拼进提示词。假设已经有一个EmbeddingClient接口服务层就可以这样写public String buildContext(String question) { float[] vector embeddingClient.embed(question); ListKnowledgeChunk chunks jdbcClient.sql( SELECT content FROM knowledge_chunks ORDER BY embedding ?::vector LIMIT 4 ) .param(vector) .query(KnowledgeChunk.class) .list(); return chunks.stream() .map(chunk - 【资料】 chunk.content()) .collect(Collectors.joining(\n)); }拼接提示词时一定要明确告诉模型如果在资料里找不到答案不要编造直接说不知道。否则模型很容易把检索到的无关片段当成真实知识生成一本正经的胡说八道。另外知识库切片的大小也会影响效果我一般控制在 300 到 500 字一个块太短语义不完整太长会超过模型上下文窗口。4.4 Nginx 与前端对接的注意点SSE 接口上线后最多人踩的坑是 Nginx 默认开启了缓冲。Nginx 会把后端返回的流缓存一段时间再发给前端导致前端看起来“半天不出字”最后一个字一个字往外蹦。解决办法是在对应的 location 里关掉缓冲location /api/chat/stream { proxy_pass http://backend; proxy_buffering off; proxy_cache off; proxy_read_timeout 120s; proxy_http_version 1.1; }前端如果用 EventSource只能接收 GET 请求所以我通常把流式接口拆成 GET 和 POST 两个入口。POST 流使用 fetch ReadableStream 读取更灵活。另外SSE 事件里不要塞太多无关字段数据尽量精简否则网络传输本身也会拉高首字延迟。我习惯只传两个字段event 和 datadata 里面就是一个纯文本增量。5. 常见问题与排查技巧实录5.1 首字延迟过高怎么定位有一阵我们线上首字延迟从 0.5 秒飙升到 3 秒排查后发现不是模型变慢而是平台在收到模型第一个 token 之前竟然先做了一次同步的数据库查询和一次 Redis 写入。这虽然只有几百毫秒但在用户感知里非常明显。解决方式很直接把“会话初始化”“埋点写入”全部改成异步请求进入后第一件事就是建立到模型供应商的连接。大模型流的首字延迟是产品体验的生命线任何阻塞操作都不要放在这条关键路径上。5.2 数据库连接池被拖垮WebFlux 下如果用了同步 JDBC 并且连接池较小高并发时很容易把 HikariCP 的连接耗尽。表象是接口超时、日志里出现“Connection is not available, request timed out”。这个问题的本质是线程切换导致的连接占用时间变长。我用两个手段解决一是把涉及 JDBC 的操作换成了 R2DBC 响应式驱动二是如果暂时无法换驱动就把数据库操作放到独立线程池并大幅调高连接池上限同时用信号量做线程隔离。5.3 Nginx 缓存导致前端卡顿前面提过 Nginx 缓冲的问题这里再强调一下现象本地联调一切正常部署到测试环境后前端要等十几秒才能看到第一段文字。这个现象百分之九十是 Nginxproxy_buffering on导致的。修改配置后记得nginx -s reload不要重启整个 Nginx以免影响其他流量。如果你用 Spring Cloud Gateway 或者 Kong同样要检查是否开启了响应缓冲网关层对 SSE 默认是不友好的。5.4 模型供应商限流如何优雅降级云厂商大模型经常有限流尤其是账号 QPS 配额不高的时候。我们线上出现过某个模型返回 429导致一堆请求失败。我的方案是做一个供应商级别的“故障标记”当连续三个 429/5xx 时路由层自动把请求切到备用模型并在响应里增加一个model_used字段方便前端标记“当前客服由备用模型服务”。降级文案要提前准备好不要等到失败发生时现场拼一句冷冰冰的“系统繁忙”。很多时候用户能接受延迟但不能接受莫名其妙的失败。5.5 安全审计与内容合规AI 应用平台的内容合规是不可回避的环节。我们在接入层增加异步内容审核服务对模型的输入输出做敏感词过滤和人工抽检。对于用户上传的文档在进入知识库之前必须经过审核不通过的自动丢弃。模型生成的答案也会逐条记录到审计日志异常内容可以溯源。企业场景下这些措施不只是为了合规更是为了防止模型被恶意利用打骚扰广告、生成违规内容。安全能力最好内置在平台里而不是等出了问题再补。6. 收尾我对生产级 AI 平台的理解6.1 一个最重要的工程评判标准我在这个项目里最大的体会是所谓的“生产级 AI 应用平台”核心不在于模型多聪明而在于你出了问题时能不能快速止血、能不能把模型能力变成可控的业务资产。最初我们也追逐过最新模型后来发现稳定可靠、可观测、可回滚才是平台最要紧的底线。模型可以换、可以升级但一块能够随意优雅降级的基础设施才是真正值得沉淀的东西。6.2 几个可以马上用起来的小经验最后分享几个成本很低但回报极高的工程习惯第一所有大模型调用先走公司内部的统一 API Key 管理不要散落在业务代码里第二把 token 消耗按部门和业务线做周报AI 平台如果不控制成本月底账单会让人很意外第三所有 Agent 工具调用必须加超时和并发上限因为工具有可能被模型疯狂调用第四响应流结束后一定要记录完整耗时、token 量、错误码这是后续做模型评测和调度的数据基础。在做这个平台之前我以为难点是模型接入后来才发现真正的难点是稳定、成本、日志、降级这些“不性感”的部分。希望这篇总结能让你在做技术选型和落地的时候更有底气也欢迎你在实操中遇到具体问题再回来交流踩过的坑才是最有价值的生产力。