ARTICLE DETAIL

资讯详情

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

Spring Boot 搭建生产级 AI 应用平台:架构设计与工程实践

Spring Boot 搭建生产级 AI 应用平台:架构设计与工程实践 1. 为什么我选择用 Spring Boot 来搭 AI 应用平台1.1 从一次真实的踩坑经历说起去年下半年我接手了一个内部 AI 工具平台的重构工作。原来的系统是用 Python 写的模型调用、业务逻辑、用户管理全揉在一个项目里代码量上来之后改一个接口要动三四个文件部署的时候还得单独维护一套 Python 环境。最要命的是团队里大部分后端同学都是 Java 技术栈出身每次排查问题都要跨语言调试效率低得让人抓狂。后来我下定决心把整个平台用 Spring Boot 重写了一遍。选它的理由很直接团队熟悉、生态成熟、工程化能力强。更重要的是Spring AI 这个项目已经把主流大模型的调用封装得相当到位不需要自己从零造轮子。重写完之后接口响应稳定了部署也简化成一条命令运维同学终于不用再问我“这个 Python 脚本到底跑在哪个虚拟环境里”了。这篇文章就是把这套平台的搭建思路、核心模块设计、踩过的坑和实操细节完整地分享出来。不管你是刚接触 AI 应用开发的 Java 后端还是想把现有 AI 能力集成到业务系统里的架构师都能从里面找到可以直接参考的东西。1.2 生产级 AI 平台到底要解决什么问题很多人一提到“AI 应用平台”第一反应就是“调个模型接口返回结果”。但真正放到生产环境里事情远没有这么简单。我总结下来一个能扛住真实业务流量的 AI 平台至少要解决下面这几类问题模型接入的统一抽象不同厂商的模型接口协议、参数命名、返回格式都不一样如果每个业务模块都直接调原始 SDK后期换模型就是灾难。对话上下文的管理多轮对话需要维护会话状态还要控制 token 消耗不能无限制地把历史消息全塞进去。Agent 能力的编排单纯的问答满足不了复杂场景需要让模型能调用工具、查数据库、执行多步推理。并发与限流模型调用是典型的慢接口一个请求可能跑十几秒高并发下线程池、超时、重试策略都得设计好。可观测性调用量、耗时、token 消耗、失败率这些指标不监控起来出了问题根本无从下手。安全与权限谁能调用哪个模型、单次请求的 token 上限、敏感内容的过滤都是必须考虑的。Spring Boot 在这几个方面都有现成的解决方案Spring AI 负责模型抽象Spring Security 管权限Micrometer 做指标采集Resilience4j 处理熔断限流。把这些组件组合起来就能搭出一个结构清晰、易于维护的平台。1.3 整体技术选型与架构分层在动手之前我先把技术栈定下来。选型的原则是优先用 Spring 官方生态减少第三方依赖带来的不确定性。层次技术选型选型理由基础框架Spring Boot 3.2.x支持虚拟线程对高并发场景友好模型接入Spring AI 1.0.x官方抽象支持多种模型提供商数据持久化MyBatis-Plus MySQL团队熟悉CRUD 效率高缓存Redis存会话上下文、限流计数器接口文档SpringDoc OpenAPI自动生成方便前端对接监控Micrometer Prometheus指标采集标准化熔断限流Resilience4j轻量配置灵活架构上我分成了四层接入层负责 HTTP 接口和参数校验编排层处理对话逻辑和 Agent 调度模型层封装具体的模型调用基础设施层提供缓存、持久化、监控等支撑。这样分层的好处是换模型只动模型层改业务逻辑只动编排层互不影响。2. 核心模块的细节拆解与实操要点2.1 模型接入层用 Spring AI 做统一抽象Spring AI 最核心的价值在于它提供了一套统一的ChatClient和ChatModel接口。不管你底层用的是哪家的模型上层业务代码调用的方式都是一样的。我实际用下来这个抽象层设计得比较合理切换模型提供商只需要改配置不用动业务代码。配置一个模型的基本写法是这样的spring: ai: openai: api-key: ${MODEL_API_KEY} base-url: ${MODEL_BASE_URL} chat: options: model: ${MODEL_NAME} temperature: 0.7 max-tokens: 2048然后在代码里注入ChatClient就能用Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个专业的技术助手) .build(); } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }这里有个细节要注意temperature这个参数不是随便设的。做代码生成、数学推理这类需要确定性的任务我一般设 0.1 到 0.3做创意文案、头脑风暴设 0.7 到 0.9。设太高会出现胡言乱语设太低回答又会很死板。max-tokens也要根据实际场景控制设太大不仅浪费钱还可能因为生成时间过长导致接口超时。提示不同模型对参数的支持程度不一样有些模型不支持temperature或者top-p配置前最好查一下对应提供商的文档避免参数被静默忽略。2.2 会话上下文管理别把历史消息一股脑塞进去多轮对话是 AI 应用的基本能力但上下文管理是个容易被忽视的坑。我见过有项目把用户所有的历史消息都拼到 prompt 里结果没聊几轮就超出模型的上下文窗口直接报错。我的做法是在 Redis 里维护每个会话的消息列表同时做两层控制第一层是消息数量限制默认保留最近 10 轮对话。第二层是token 预算控制每次组装 prompt 之前先估算 token 数超过阈值就从最早的消息开始丢弃。估算 token 有个粗略的经验公式英文大约 4 个字符 1 个 token中文大约 1.5 个字符 1 个 token。精确计算可以用对应模型的 tokenizer但生产环境里用估算值加一点余量就够了。public ListMessage buildContext(String sessionId, String newMessage) { ListMessage history redisTemplate.opsForList() .range(chat:session: sessionId, 0, -1); ListMessage context new ArrayList(); int tokenBudget 3000; int used estimateTokens(newMessage); // 从最新消息往前取直到超出预算 for (int i history.size() - 1; i 0; i--) { Message msg history.get(i); int cost estimateTokens(msg.getContent()); if (used cost tokenBudget) break; context.add(0, msg); used cost; } context.add(new UserMessage(newMessage)); return context; }会话数据我设置了 2 小时的过期时间避免 Redis 内存无限增长。如果业务需要长期保存对话记录那就异步落库到 MySQLRedis 只做热数据的缓存。2.3 Agent 编排让模型学会调用工具Agent 是这两年最火的概念之一但很多人对它的理解停留在“能自动干活的 AI”。从工程角度看Agent 的本质是让模型能够决定调用哪个工具、传什么参数、拿到结果后如何继续推理。Spring AI 提供了Tool注解来声明可调用的工具方法用起来相当顺手。Component public class WeatherTools { Tool(description 查询指定城市的实时天气) public String getWeather( ToolParam(description 城市名称如北京) String city) { // 实际调用天气 API return weatherApi.query(city); } }然后在 ChatClient 里注册这些工具ChatClient client builder .defaultTools(new WeatherTools(), new DatabaseTools()) .build();模型在推理过程中如果判断需要查天气就会自动生成工具调用请求Spring AI 负责执行并把结果回传给模型继续推理。这个循环会一直持续到模型给出最终答案。这里有几个实操经验值得分享。第一工具描述要写清楚模型是根据 description 来判断该不该调用这个工具的描述模糊会导致误调用或者该调不调。第二工具方法要幂等因为模型可能会重复调用同一个工具。第三要设置最大循环次数防止模型陷入死循环我一般设 5 次超过就强制返回当前结果。2.4 并发处理虚拟线程真的能救场模型调用是 IO 密集型操作一个请求动辄几秒到几十秒。传统的线程池模型下每个请求占一个线程并发量一上来线程池就满了后面的请求只能排队。Spring Boot 3.2 引入的虚拟线程正好解决这个问题。开启虚拟线程很简单一行配置spring: threads: virtual: enabled: true开启之后Tomcat 的请求处理会自动使用虚拟线程。虚拟线程的创建成本极低可以轻松创建几十万个IO 阻塞时会自动让出底层载体线程吞吐量提升非常明显。我实测下来同样的硬件配置开启虚拟线程后并发处理能力提升了 3 到 5 倍。不过要注意虚拟线程不适合 CPU 密集型任务如果你的业务里有大量计算逻辑还是得用平台线程池。另外使用虚拟线程时要避免使用synchronized块因为它会导致载体线程被固定失去虚拟线程的优势改用ReentrantLock更合适。2.5 限流与熔断保护自己也要保护下游模型服务再稳定也有抽风的时候限流和熔断是生产环境的必备能力。我用 Resilience4j 做了两层防护。第一层是接口级限流防止单个用户或 IP 疯狂刷接口。用令牌桶算法每个用户每秒最多 5 次请求RateLimiter rateLimiter RateLimiter.of(userLimit, RateLimiterConfig.custom() .limitForPeriod(5) .limitRefreshPeriod(Duration.ofSeconds(1)) .timeoutDuration(Duration.ZERO) .build());第二层是模型调用熔断当某个模型的失败率超过阈值时自动切断调用走降级逻辑。比如连续 10 次调用有 5 次失败就熔断 30 秒期间所有请求直接返回兜底话术避免雪崩。CircuitBreaker circuitBreaker CircuitBreaker.of(modelCall, CircuitBreakerConfig.custom() .failureRateThreshold(50) .slidingWindowSize(10) .waitDurationInOpenState(Duration.ofSeconds(30)) .build());降级策略我一般准备两套一是切换到备用模型二是返回预设的友好提示。具体用哪套看业务重要性核心业务切备用模型非核心业务直接降级。3. 完整实操流程与关键环节实现3.1 项目初始化与依赖配置先把项目骨架搭起来。我用 Spring Initializr 生成基础结构选 Spring Boot 3.2.x、Java 21、Maven 构建。核心依赖在pom.xml里配置dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M4/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-spring-boot3/artifactId version2.2.0/version /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency /dependenciesSpring AI 的版本迭代比较快写这篇文章时用的是 1.0.0-M4正式版发布后 API 可能有调整升级前记得看 release notes。3.2 数据库表结构设计平台的核心表我设计了四张用户表、会话表、消息表、模型配置表。这里重点说会话表和消息表因为它们是上下文管理的基础。CREATE TABLE chat_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL UNIQUE, user_id BIGINT NOT NULL, title VARCHAR(128), model_name VARCHAR(64), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_id (user_id) ); CREATE TABLE chat_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content TEXT, token_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_session_id (session_id) );token_count这个字段很有用可以用来统计每个会话的 token 消耗做成本核算。role字段存user、assistant、system三种值对应消息的不同角色。3.3 核心对话接口的实现对话接口是整个平台最核心的部分我把它拆成了几个步骤参数校验、限流检查、上下文组装、模型调用、结果保存、指标上报。RestController RequestMapping(/api/chat) public class ChatController { PostMapping(/completions) public SseEmitter chat(RequestBody Valid ChatRequest request) { // 1. 限流检查 if (!rateLimiter.tryAcquire(request.getUserId())) { throw new BusinessException(请求过于频繁请稍后再试); } // 2. 创建 SSE 连接支持流式返回 SseEmitter emitter new SseEmitter(60_000L); // 3. 异步处理 CompletableFuture.runAsync(() - { try { chatService.streamChat(request, emitter); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; } }这里用了 SSEServer-Sent Events做流式返回用户体验比等完整结果好很多。模型生成一个字就推一个字前端可以边收边显示。SSE 的超时时间我设了 60 秒因为有些复杂问题模型确实要跑很久。流式处理的核心代码public void streamChat(ChatRequest request, SseEmitter emitter) { ListMessage context contextManager.buildContext( request.getSessionId(), request.getMessage()); chatClient.prompt() .messages(context) .stream() .content() .subscribe( chunk - { try { emitter.send(SseEmitter.event() .data(chunk)); } catch (IOException e) { emitter.completeWithError(e); } }, emitter::completeWithError, () - { // 保存完整消息到数据库 saveMessage(request.getSessionId(), fullContent); emitter.complete(); } ); }3.4 监控指标埋点没有监控的线上系统就是裸奔。我用 Micrometer 埋了几个关键指标请求总数、请求耗时、token 消耗量、失败率。Component public class ChatMetrics { private final Counter requestCounter; private final Timer requestTimer; private final Counter tokenCounter; public ChatMetrics(MeterRegistry registry) { this.requestCounter Counter.builder(ai.chat.requests) .description(对话请求总数) .register(registry); this.requestTimer Timer.builder(ai.chat.duration) .description(对话请求耗时) .register(registry); this.tokenCounter Counter.builder(ai.chat.tokens) .description(token 消耗总量) .register(registry); } public void recordRequest(long durationMs, int tokens, boolean success) { requestCounter.increment(); requestTimer.record(durationMs, TimeUnit.MILLISECONDS); tokenCounter.increment(tokens); if (!success) { failureCounter.increment(); } } }这些指标通过/actuator/prometheus端点暴露出来Prometheus 定时抓取Grafana 做可视化。我配了几个告警规则失败率超过 10% 告警、P99 耗时超过 30 秒告警、token 消耗突增告警。有了这些出问题能第一时间发现。3.5 部署与配置管理生产环境部署我用的是 Docker Docker Compose把应用、MySQL、Redis、Prometheus 都编排在一起。关键配置通过环境变量注入敏感信息不写死在代码里。version: 3.8 services: app: build: . ports: - 8080:8080 environment: - MODEL_API_KEY${MODEL_API_KEY} - MODEL_BASE_URL${MODEL_BASE_URL} - SPRING_PROFILES_ACTIVEprod depends_on: - mysql - redis deploy: resources: limits: memory: 2GJVM 参数我调过几轮最终用的是java -XX:UseZGC -Xms1g -Xmx2g -XX:MaxMetaspaceSize256m -jar app.jarZGC 的停顿时间在毫秒级对响应时间敏感的 AI 接口很友好。堆内存设 2G 是因为要缓存一些模型配置和会话数据太小会频繁 GC太大又浪费资源。4. 常见问题与排查技巧实录4.1 模型调用超时怎么办这是最常见的问题。模型服务本身响应慢或者网络抖动都会导致超时。我的处理策略是分三层第一层是客户端超时设置Spring AI 默认的超时时间偏短我一般调到 60 秒。第二层是重试机制对超时和 5xx 错误自动重试 2 次重试间隔用指数退避。第三层是降级兜底重试都失败就返回友好提示同时记录日志告警。Retry(name modelRetry, fallbackMethod fallback) public String callModel(String prompt) { return chatClient.prompt().user(prompt).call().content(); } public String fallback(String prompt, Exception e) { log.error(模型调用失败, e); return 抱歉服务暂时不可用请稍后再试; }重试配置resilience4j: retry: instances: modelRetry: max-attempts: 3 wait-duration: 1s enable-exponential-backoff: true exponential-backoff-multiplier: 24.2 上下文丢失或错乱多轮对话里用户经常反馈“它怎么忘了刚才说的话”。排查下来通常是几个原因会话 ID 生成有问题、Redis 数据过期、并发写入覆盖。我的排查步骤是这样的先看 Redis 里对应 sessionId 的 key 存不存在不存在就是过期或没写进去存在的话看消息顺序对不对顺序乱了就是并发写入的问题。并发写入的解决办法是给同一个会话的写操作加分布式锁用 Redis 的SETNX实现锁的粒度是 sessionId。public void saveMessage(String sessionId, Message message) { String lockKey lock:session: sessionId; String lockValue UUID.randomUUID().toString(); try { Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 5, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 执行写入 doSaveMessage(sessionId, message); } } finally { // 释放锁用 Lua 脚本保证原子性 releaseLock(lockKey, lockValue); } }4.3 Token 消耗异常增长有段时间我发现 token 消耗量突然涨了好几倍查下来是某个业务方把max-tokens设成了 8192而且没做上下文裁剪。解决办法是在平台层面做硬性限制不管业务方怎么配单次请求的 token 上限不超过 4096上下文预算不超过 3000。另外建议做一个 token 消耗的日报按用户、按业务线统计发现异常能及时定位。我用的方案是每天凌晨跑一个定时任务从消息表里聚合前一天的 token 消耗生成报表发到群里。4.4 常见问题速查表问题现象可能原因排查方向解决方案接口返回 401API Key 失效或配置错误检查环境变量和配置文件更新 Key重启服务响应特别慢模型服务负载高或网络问题看监控的 P99 耗时切备用模型加超时限制回答内容重复temperature 设太低检查模型参数配置调高 temperature 到 0.7 以上上下文丢失Redis 过期或并发覆盖查 Redis key 和写入日志加分布式锁调整过期时间内存溢出会话数据堆积看堆内存和 GC 日志加过期策略限制单会话消息数流式返回中断SSE 超时或网络断开看客户端和服务端日志调大超时加心跳保活4.5 几个我踩过的坑第一个坑是虚拟线程和 synchronized 的冲突。我一开始在会话保存的方法上加了synchronized结果发现虚拟线程的优势完全没发挥出来吞吐量跟平台线程差不多。后来改成ReentrantLock才正常。这个坑很隐蔽因为代码能跑只是性能上不去不看监控根本发现不了。第二个坑是 Spring AI 的版本兼容性。有次升级到新版本发现ChatClient的 API 变了编译直接报错。后来我养成了习惯升级前先看官方 migration guide小版本升级也要跑一遍集成测试。第三个坑是模型返回的 JSON 解析失败。让模型返回结构化数据时它有时候会在 JSON 外面包一层 markdown 代码块标记直接解析就报错。解决办法是在解析前先做清洗把json 和这些标记去掉或者用更宽松的解析器。第四个坑是限流配置太严。我一开始把单用户限流设成每秒 2 次结果正常用户连续问几个问题就被限了体验很差。后来改成每秒 5 次突发允许 10 次才比较合理。限流阈值一定要根据真实用户行为来定拍脑袋设的数字往往不合适。5. 平台后续可以怎么扩展这套平台跑了大半年整体比较稳定。如果继续往下做我觉得有几个方向值得投入。多模型路由是第一个。现在平台支持配置多个模型但调用哪个是写死的。可以做一个智能路由根据问题类型、成本预算、当前各模型的负载情况动态选择。比如简单问题走便宜的小模型复杂推理走能力强的大模型能省不少成本。RAG 知识库是第二个。现在平台只能靠模型自身的知识回答接入企业私有知识库后能回答很多专业领域的问题。Spring AI 已经提供了 VectorStore 的抽象对接向量数据库不难难的是文档切分和召回策略的调优。Agent 工作流编排是第三个。现在的 Agent 还是单轮的复杂任务需要多步协作。可以引入工作流引擎把多个 Agent 串起来每个负责一个子任务最后汇总结果。这个方向工程复杂度比较高但价值也大。成本核算与配额管理是第四个。现在 token 消耗只是统计还没有跟用户或部门挂钩。可以做一个配额系统每个用户每月有固定的 token 额度用完就限流这样能有效控制成本。我在实际运维这套平台的过程中最大的体会是AI 应用的工程复杂度八成不在模型本身而在周边的工程设施。模型调用就那几行代码但要让它在生产环境稳定运行需要处理的边界情况、异常场景、性能问题比传统 CRUD 应用多得多。把监控做扎实把降级做完善把配置管理做规范这些看起来不起眼的工作才是平台能不能扛住真实流量的关键。
返回列表