ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot 生产级 AI 应用平台:架构分层、Agent 编排与治理实践

2026/10/7 13:40:35 拓冰建站 浏览量
Spring Boot 生产级 AI 应用平台:架构分层、Agent 编排与治理实践 1. 为什么要在 Spring Boot 里做 AI 应用平台1.1 从“能跑通”到“能上线”之间隔着什么我最早接触 Spring AI 是在一个内部知识库项目上当时图省事直接在 Controller 里 new 了一个 ChatClient把 API Key 硬编码在代码里调通那一刻确实挺爽。但等到要上线的时候问题全来了模型调用超时没有降级策略、多轮对话的上下文存在内存里重启就丢、不同业务线要接不同模型却只能改代码重新打包、Token 消耗没有任何统计口径。这些问题单拎出来都不难但堆在一起就说明一件事——Demo 和平台之间差的不是功能是工程化。所谓生产级 AI 应用平台核心不是“能调通大模型”而是把模型调用这件事变成一项可治理、可观测、可扩展的基础能力。它要解决的具体问题包括统一接入多家模型供应商并支持热切换、管理对话会话与上下文窗口、编排 Agent 的工具调用链路、控制并发与限流、记录调用链路与成本、以及给上层业务提供稳定的 SDK 或 HTTP 接口。适合阅读这篇内容的读者是已经写过 Spring Boot 业务、现在想把 AI 能力真正落到生产环境里的后端同学也包括正在做技术选型的架构角色。1.2 技术选型的几个关键取舍选 Spring Boot 作为底座几乎是顺理成章的团队现有的鉴权、监控、配置中心、数据库连接池这些基础设施都能直接复用没必要为了 AI 单独起一套异构技术栈。真正需要斟酌的是 AI 编排层用什么。方案优势代价适用场景直接调 HTTP API无额外依赖完全可控每家模型参数格式不同重复代码多只接一家模型的小项目Spring AI官方抽象ChatClient/Embedding 统一版本迭代快部分高级特性滞后主流选择Java 团队首选LangChain4j生态丰富Agent 支持成熟与 Spring 集成需自己粘合复杂 Agent 编排自研编排层完全贴合业务维护成本高有专门平台团队时我最终选的是Spring AI 作为模型接入层 自研轻量编排层的组合。原因很实际Spring AI 把 ChatModel、EmbeddingModel、VectorStore 这些概念抽象得很干净切换模型供应商时业务代码基本不用动但它的 Agent 编排能力在早期版本里还不够灵活工具调用的重试、超时、并行策略我需要自己控制所以在它之上包了一层编排逻辑。这个组合的好处是既吃到了官方抽象的红利又保留了关键链路的控制权。2. 平台分层架构与核心模块拆解2.1 四层结构接入层、编排层、能力层、治理层一个能扛住生产流量的 AI 平台我习惯把它拆成四层每层职责单一层与层之间通过接口通信避免牵一发动全身。接入层负责对外暴露能力包括 REST 接口、SSE 流式响应、以及给内部服务用的 SDK。这一层只做参数校验、鉴权、限流不掺业务逻辑。编排层是核心负责把一次用户请求拆解成“检索上下文 → 组装 Prompt → 调用模型 → 解析工具调用 → 再调用模型”这样的链路Agent 的循环逻辑就住在这里。能力层封装具体能力对话、向量检索、工具执行、文档解析。治理层是横切关注点包括模型路由、Token 计量、链路追踪、熔断降级、敏感内容过滤。这样分层最直接的好处是当我要把某个模型从 A 换成 B 时只需要动治理层的路由配置编排层和能力层完全无感。反过来当业务要加一个新的工具比如查订单只需要在能力层注册一个 Tool编排层自动就能用上。2.2 模型路由多供应商热切换的实现思路生产环境不可能只依赖一家模型。一方面是可用性某家服务抖动时要有备选另一方面是成本简单任务用便宜的小模型复杂推理才上大模型。我的做法是在配置中心维护一张路由表ai: routing: rules: - name: simple-qa match: intent faq primary: qwen-turbo fallback: glm-4-flash - name: complex-reasoning match: intent analysis primary: qwen-max fallback: qwen-plus路由的匹配条件可以基于意图识别结果、用户等级、请求 Token 预估长度等。关键点是路由决策要在调用模型之前完成而不是等失败了再重试否则延迟会翻倍。降级策略我一般配两级主模型超时或返回错误码时切备用模型备用也失败才向上抛异常并返回兜底话术。注意路由表一定要支持运行时刷新别写死在代码里。我踩过的坑是某次线上模型限流改路由要重新发版白白多扛了二十分钟的报错。2.3 会话与上下文管理滑动窗口不是唯一解多轮对话的上下文管理是很多人第一个卡住的地方。最朴素的做法是把历史消息全带上但 Token 会线性增长成本和延迟都受不了。常见的策略是滑动窗口只保留最近 N 轮对话。但纯滑动窗口有个明显缺陷用户在第一轮说的关键信息比如“我要查的是上个月的订单”可能在第五轮就被挤掉了。我的做法是滑动窗口 摘要压缩的组合。保留最近 K 轮原文更早的历史用一个轻量模型压缩成一段摘要拼在 System Prompt 里。这样既控制了 Token又不丢关键信息。具体参数上我一般设 K6摘要触发阈值是历史消息超过 10 轮。会话数据存 Rediskey 用session:{userId}:{conversationId}设置 2 小时过期同时异步落库一份用于审计和数据分析。public ListMessage buildContext(String conversationId, String userInput) { ListMessage recent redis.getRecent(conversationId, 6); String summary redis.getSummary(conversationId); ListMessage context new ArrayList(); if (StringUtils.hasText(summary)) { context.add(new SystemMessage(历史对话摘要 summary)); } context.addAll(recent); context.add(new UserMessage(userInput)); return context; }3. Agent 编排与工具调用的落地细节3.1 Agent 循环的本质让模型决定下一步做什么很多人把 Agent 想得很玄其实剥开看就是一个循环把可用工具的描述和用户问题一起给模型模型返回“我要调用某个工具参数是这些”平台执行工具拿到结果再把结果喂回模型直到模型认为可以给出最终答案。这个循环的终止条件、最大轮次、超时控制才是工程上的难点。我实现的编排器核心逻辑大致是这样设置最大循环轮次为 5单轮工具执行超时 3 秒整体请求超时 30 秒。每轮结束后检查模型返回是否包含工具调用意图没有就直接返回文本结果。这里有个容易忽略的点——工具调用的参数要做校验和兜底模型偶尔会生成格式不对的 JSON 或者不存在的参数名直接反序列化会抛异常必须捕获后把错误信息作为工具结果返回给模型让它自己纠正。3.2 工具注册让新增能力零改动的设计工具Tool的注册我用的是注解 自动扫描的方式。定义一个AiTool注解标注在 Spring Bean 的方法上启动时扫描所有带注解的方法把方法名、描述、参数 Schema 注册到工具注册表里。Component public class OrderTools { AiTool(name queryOrder, description 根据订单号查询订单状态) public OrderResult queryOrder( ToolParam(description 订单号) String orderNo) { return orderService.query(orderNo); } }这样业务同学要加一个新工具只需要写一个方法加注解不用改编排层的任何代码。工具描述一定要写清楚因为模型就是靠这段描述来判断什么时候该调用它。我见过描述写得太模糊导致模型乱调工具的情况比如把“查询天气”写成“获取信息”模型就会在用户问订单的时候也去调它。3.3 并发与限流Agent 扛并发的真实瓶颈Agent 场景下并发压力比普通接口大得多因为一次用户请求可能触发 3 到 5 次模型调用。我做过压测单实例在 4C8G 配置下纯转发模型请求能扛住 200 QPS但一旦带上 Agent 循环实际能稳定支撑的用户并发只有 30 到 50。瓶颈不在 CPU而在模型 API 的响应延迟和连接池。应对手段有几个一是给模型调用配置独立的线程池和业务线程隔离避免慢调用拖垮整个应用二是对同一用户的请求做串行化防止上下文错乱三是在接入层做令牌桶限流按用户维度和全局维度双重限制。线程池参数上核心线程数我一般设成2 * CPU核数队列用有界队列拒绝策略用 CallerRuns 让调用方自己扛形成天然的背压。提示别用无界队列。我早期用LinkedBlockingQueue不设容量结果高峰期任务堆积内存直接飙到 OOM排查了半天才发现是队列把请求全吞了。4. 可观测性与成本治理的实操方案4.1 链路追踪一次请求到底调了几次模型AI 应用的排查难度比普通接口高因为链路是动态的模型可能调了三次工具、四次模型。没有追踪能力基本没法定位问题。我的做法是给每次用户请求生成一个 traceId贯穿整个编排过程每次模型调用和工具调用都记录一条 span包含耗时、输入 Token、输出 Token、模型名称、是否命中缓存。这些数据落到 Elasticsearch 或者直接用日志平台配合看板就能看到 P95 延迟、各模型调用占比、失败率。有一次线上反馈“回答变慢了”我看板上一看某个模型的 P99 从 2 秒涨到了 8 秒切了路由立刻恢复整个过程不到五分钟。4.2 Token 计量与成本控制Token 是要花钱的不计量就等于烧钱不自知。我在治理层做了一个 Token 计量器每次模型返回后从响应里取 usage 字段累加按用户、按业务线、按天三个维度聚合。同时设置预算告警某个业务线当天消耗超过阈值就发通知超过硬上限就自动降级到便宜模型。成本控制还有几个实用技巧一是缓存高频问题的答案用问题文本的向量做相似度匹配命中就直接返回省掉模型调用二是Prompt 精简System Prompt 别写太长我见过有人把两千字的角色设定塞进去每次调用都白花这笔钱三是按需选择模型意图识别、文本分类这类任务用小模型完全够用没必要上大模型。治理维度采集指标告警阈值建议延迟P95/P99 模型调用耗时P99 5s成本单用户日 Token 消耗超均值 3 倍可用性模型调用失败率5 分钟窗口 3%质量兜底话术触发率 5%4.3 敏感内容过滤与安全兜底平台对外提供服务输入输出的内容安全必须管起来。我的做法是在接入层做输入过滤在返回前做输出过滤中间加一层基于关键词和向量相似度的检测。输入侧主要防注入类攻击比如用户试图通过特殊 Prompt 让模型泄露 System Prompt输出侧主要防不当内容。检测命中后不是简单报错而是返回一个预设的友好话术同时记录一条审计日志。这里有个经验过滤规则要可配置、可热更新因为对抗手段在变规则库需要持续调整。我把规则存在数据库里配一个管理后台运营同学可以自己加词、调阈值不用每次都找开发。5. 常见问题排查与踩坑记录5.1 流式响应中断与 SSE 连接管理流式输出是 AI 应用的标配体验但 SSE 连接比普通 HTTP 脆弱得多。常见问题是客户端断开后服务端还在往模型拉数据白白消耗资源。解决办法是监听连接关闭事件一旦客户端断开就取消上游的模型调用。Spring 里可以通过SseEmitter的onCompletion和onTimeout回调来处理配合Disposable取消订阅。另一个坑是代理层缓冲。有些网关默认会缓冲响应体导致流式效果失效用户要等全部生成完才看到内容。部署时一定要确认网关关闭了对 SSE 路径的缓冲并设置足够长的读超时。5.2 模型返回格式不稳定的处理即使开了 JSON 模式模型偶尔还是会返回带 Markdown 代码块包裹的 JSON或者多一句“好的以下是结果”。直接解析必挂。我的处理方式是写一个健壮的解析器先尝试直接解析失败则用正则提取第一个{...}或[...]片段再解析再失败就触发一次重试重试时在 Prompt 里强调“只返回 JSON不要任何其他文字”。重试两次还失败就降级返回兜底结果并记录一条异常日志用于后续分析。5.3 常见问题速查表现象可能原因排查方向回答重复或答非所问上下文拼接错误检查会话 key 是否串号工具调用不触发工具描述不清晰优化 description 文案流式输出卡顿网关缓冲关闭代理缓冲Token 消耗异常高历史未压缩检查摘要触发逻辑偶发超时模型侧抖动看路由降级是否生效5.4 我踩过的几个真实坑第一个坑是把 API Key 写进了配置文件提交到仓库。虽然后来紧急轮换了密钥但这个教训让我养成了所有密钥走环境变量或密钥管理服务的习惯。第二个坑是没有给模型调用设超时默认超时可能是无限等待某次模型服务卡住线程池被占满整个应用雪崩。第三个坑是会话数据只存内存本地测试没问题一上多实例部署就出现“同一个用户两次请求上下文对不上”因为负载均衡打到了不同实例。这三个坑本质上都是把 Demo 思维带进了生产环境值得每个刚上手的人警惕。6. 从零搭建的最小可行路径6.1 依赖引入与基础配置如果现在让我从零搭一个最小可用的版本我会这样起步。先在pom.xml里引入 Spring AI 的 starter注意版本要和 Spring Boot 版本对齐我一般用 Spring Boot 3.2.x 配 Spring AI 1.0.x 这条线。然后配置模型连接信息全部走环境变量spring: ai: dashscope: api-key: ${AI_API_KEY} chat: options: model: qwen-plus temperature: 0.7temperature这个参数值得说一下它控制输出的随机性。做客服问答我一般设 0.3 到 0.5保证回答稳定做创意生成才调到 0.8 以上。别小看这个参数设错了要么回答死板要么胡言乱语。6.2 第一个可用的对话接口最小版本不需要 Agent先把对话跑通。写一个 Service 注入ChatClient在方法里组装消息列表调用即可。关键是把会话 ID 作为参数传进来从 Redis 取历史调用完再把新消息写回去。这一步做完你就有了一个支持多轮对话的基础服务。Service public class ChatService { private final ChatClient chatClient; private final SessionStore sessionStore; public String chat(String sessionId, String input) { ListMessage context sessionStore.load(sessionId); context.add(new UserMessage(input)); String reply chatClient.prompt() .messages(context) .call() .content(); sessionStore.append(sessionId, input, reply); return reply; } }6.3 逐步演进从对话到 Agent 到平台有了对话能力之后演进路径我建议分三步走。第一步加工具调用把业务系统里高频的查询操作注册成 Tool让模型能主动调用第二步加治理能力把 Token 计量、链路追踪、限流降级补上这一步是能不能上生产的分水岭第三步做多租户和配置化让不同业务线能各自配置模型、Prompt、工具集平台化才算成型。每一步都不要跳。我见过团队一上来就想做全功能平台结果基础对话的上下文管理都没做扎实后面全是返工。先把一条链路做透再横向扩展这个节奏最稳。6.4 部署与容量规划的一点经验部署上AI 应用对内存的要求比普通 Web 应用高因为要缓存会话、向量数据、模型客户端连接。我一般给单实例配 4G 起步JVM 堆设 2G剩下的留给堆外和系统。实例数按峰值并发除以单实例承载量来算再留 30% 余量。容器化部署时注意健康检查要包含模型连通性探测别等流量进来了才发现模型连不上。容量规划有个简单公式可以参考单实例支撑并发数 ≈ 模型平均响应时间秒的倒数 × 线程池大小 × 0.7。比如模型平均响应 2 秒线程池 50那单实例大约能扛 17 个并发。这个数字很粗糙但用来做初始估算够用了真实容量还是要靠压测校准。最后分享一个我在实际运维中体会很深的点AI 平台的稳定性不取决于你接了多少家模型而取决于你对失败路径的处理有多细致。模型超时怎么办、返回格式错了怎么办、工具执行抛异常怎么办、上下文超长了怎么办把这些边界情况一个个想清楚、测到位平台才算真正立得住。我现在的习惯是每加一个新能力先写它的失败用例再写正常用例这个顺序能帮我提前发现大部分隐患。