
这个系列写到第十六掌名字叫“履霜冰至”。《易经·坤卦》初六爻辞说“履霜坚冰至”——脚踩到霜就该预见到寒冬将至要做防寒准备了。前十五掌我们一直在拆SpringAI和阿里云百炼怎么写、怎么调、怎么联调凡是照着做过的人此刻应该都能在自己项目里感受到那股“霜气”功能能跑但越跑越乱模型调用散落在各处会话状态没人管前端接入各写各的。这时候就该考虑“立派服务”了。“立派”这个词我借的是武侠里的“开宗立派”。散兵游勇式的AI调用和真正能独立部署、独立演进、独立运营的AI服务中间差着一个完整的工程化体系。这一掌就想把“立派”这件事讲透什么时候该动手、骨架怎么搭、阿里云底座上有哪些细节绕不开以及我在实操里挨过的几记闷棍。适合正在用SpringAI Alibaba做项目、被“能跑但不好维护”折磨的团队也适合准备从Demo走向生产环境的个人开发者。1. “履霜冰至”这一掌到底在讲什么Spring AI Alibaba的立派时机1.1 从系列标题看服务化演进的必然性熟悉周易的朋友一看“履霜冰至”就明白这说的是渐进积累、见微知著。SpringAI项目我观察了很长一段时间最有意思的现象是几乎所有团队都经历着同一条曲线。最早是尝鲜期pom里引入spring-ai-alibaba-starter照着官方案例写一个ChatClient调用通义千问模型跑通一个“AI助手”接口发个朋友圈。这个阶段是快乐的因为只需要关注“模型能不能回话”。然后是膨胀期老板说“AI助手要能引用知识库”“要能边生成边打字”“要能记住用户上次聊了什么”。于是你开始往项目里加向量库、加流式响应、加会话表、加各种Service。这时候代码已经开始不舒服了但还能撑。最后是混乱期多人协作时每个人各自new一个ChatClient模型密钥写在application.yml的五个地方同一个大模型在三个服务里被重复封装。踩到霜了但大多数人还在嘴硬“能跑就行。”“履霜冰至”要说的就是当项目出现这些早期信号别骗自己还能靠追加Controller解决问题该给AI能力“立派”了。1.2 什么是“立派服务”不是重构而是给AI应用一个稳定的家很多团队一听“服务化改造”就很紧张以为是推倒重来。我理解的“立派服务”完全不是这样它更像给一个天赋不错但没人管的孩子盖一间独立的房子不用换人不用换天赋只是让他有自己的房间、自己的作息、自己的规矩。落到SpringAI Alibaba上就是三件事第一模型访问统一收口。所有对通义千问、Ollama、OpenAI风格模型的调用都通过同一个服务入口密钥集中管理模型路由统一配置不再允许业务代码里散落model 赋值。第二会话状态有归属。聊天上下文不再只存在于某个Controller的局部变量里而是有自己的存储层RDS也好、Redis也好到期清理、按用户隔离。第三接口风格对外一致。SSE流式、普通JSON、异步任务回调都是同一个接口网关对外暴露前端控件对接一次就能跑遍全平台。“立派”之后新增AI能力不再是“加一个接口”而是“往门派里添一门武功”有固定的套路和落点。1.3 哪些读者适合看这篇说实话这篇不是给完全没有接触过SpringAI的人看的入门教程。如果你还没跑通过一个最基础的AI问答接口建议先看系列前面的章节了解ChatClient的用法。这篇适合谁比如你已经在项目里集成过阿里云百炼API但发现切换模型时要改十几个地方比如你的前端同事总在问“为什么流式接口有的返回text/event-stream有的返回application/json”比如你被线上会话丢失问题折磨过用户聊到一半刷新页面上下文全没了。这些都是“霜”看完这篇你可以对照自己的项目逐条体检然后照着骨架搭一遍。2. 立派前的“霜”这些信号出现时就该动手了这一节我用“霜”来代指工程腐化信号。每条信号都是我真实见过的也是触发我做服务化改造的直接原因。2.1 信号一ChatClient和Model在各处new来new去Spring AI Alibaba的ChatClient设计得很好可以流式调用可以装配Advisor可以指定模型参数。但正因为太好用了很多团队成员会在各自的Service里直接写ChatClient chatClient ChatClient.builder(chatModel).build();每个业务类各建各的模型参数各调各的有的地方temperature设成0.3有的地方设成0.8。等到要统一加超时时间时你就要在八个文件里做同样的事改漏一个就是线上事故。这种“散养”状态在项目初期没有感知因为模型返回的都差不多。但一旦你要做统一日志、统一审计、统一限流根本没地方下手。这是最典型的“履霜”。2.2 信号二模型、密钥、Prompt全都硬编码我见过一个项目的AI模块通义千问的API Key直接写在application.yml里同时还写了三个不同的Prompt模板分布在两个Util类、一个常量类和一处Controller的注解里。换密钥时要提交代码、重新发布改Prompt时要靠“满项目搜索”。更麻烦的是多模型切换。Spring AI Alibaba本身支持通过配置切换模型供应商但如果代码里到处写死了要调dashscope-chat-model这个Bean那你永远没法做到A/B测试。立派之后这些全部收编。密钥进配置中心Prompt模板进资源文件模型选择走路由。2.3 信号三会话丢三落四流式接口只有你一个人能调通聊天类应用最怕的坑有两类一类是刷新页面后上下文消失另一类是流式输出前端永远接不住。上下文消失的本质是“无状态”你只是把用户消息当作单次请求发给了模型没有自己的会话仓储。这在早期Demo里无伤大雅但做真实业务时用户问一句“刚才那个方案你再说一遍”模型一脸茫然这个产品就废了。流式接口接不住则往往是因为没有统一协议。前端控件有的期望text/event-stream有的期望application/octet-stream的JSON块有的期望直接返回text/plain。如果你每个接口都是现签的“临时协议”联调成本会爆炸。立派的标志之一就是接口协议像合同一样稳定。2.4 信号四没有统一网关限流熔断全靠运气AI接口和普通业务接口有个显著区别模型调用的成本高、延迟高、第三方依赖强。如果请求直接从Controller打到百炼API一旦模型侧抖动你的线上就会跟着抖完全没有任何缓冲。更现实的问题是多个AI能力共用同一个模型账号时并发上限怎么控。今天百炼侧限流了哪个接口先挂完全取决于谁先倒霉。立派之后AI服务内部必须有网关层做统一鉴权、限流、熔断、灰度。3. 三层骨架把Spring AI Alibaba从Demo屋改造成宗门我习惯把“立派服务”的骨架拆成三层接入层管协议编排层管脑子模型层管供应商。每一层都有固定的职责边界没有越权行为。3.1 接入层统一REST/SSE出口前端控件直接对接接入层是面向外部世界的脸面。无论内部是通义千问还是Ollama外部的API形态必须稳定。我建议你设定两类接口一类是标准JSON问答接口给后端服务或定时任务调用POST /api/chat请求体是userId和message响应体是完整文本、引用来源、消费Token数等信息。另一类是SSE流式接口POST /api/chat/stream同样参数响应头改为text/event-stream按data:块分段推送。有个细节值得注意前端如果用的是阿里开源的AI会话前端控件它对SSE事件的解析通常有约定事件名是什么、data字段放什么。如果你接口推的格式和控件预期不一致会表现为“打字机效果失效”或者“第一条消息偶发空白”。我的经验是数据事件名统一用message每条消息体里content字段放增量文本finishReason字段放结束标记这套协议和主流开源前端控件都兼容。3.2 编排层ChatClient是大脑Advisor是经脉Spring AI Alibaba最舒服的一点是编排能力天然内建。ChatClient不只是发个请求那么简单它有一个链式编程的骨架可以挂载丰富的组件。我目前的编排层长这样RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/stream) public FluxString stream(RequestBody ChatRequest request) { return this.chatClient.prompt() .user(request.message()) .advisors(a - a.param(chatId, request.sessionId())) .stream() .content(); } }这里advisors就是“经脉”。通过它你可以把会话记忆、检索增强、提示词增强都挂进去而不污染业务代码。比如会话记忆Spring AI Alibaba的MessageChatMemoryAdvisor可以直接接一张会话表把历史消息自动拼进上下文。你什么都不用做只需要在创建ChatClient时把它装配进去Bean ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); }这段代码价值极高它意味着“立派”不再需要手搓上下文拼接逻辑。3.3 模型层多Key路由、百炼API与Spring AI Alibaba的适配模型层是接供应商的地方。Spring AI Alibaba默认支持阿里云百炼也就是dashscope同时也兼容OpenAI等协议。我的建议是把模型配置全部收敛到一个配置类里。多账号轮询是生产环境绕不过去的需求免费额度账号、付费主账号、备用账号并存主账号被限流时自动切备用。实现方式不复杂自定义一个DelegatingChatModel内部持有多个ChatModel通过CircuitBreaker或简单的计数器做切换。Component public class RoutingChatModel implements ChatModel { private final ListChatModel candidates; public RoutingChatModel( Qualifier(dashscopeChatModelPrimary) ChatModel primary, Qualifier(dashscopeChatModelBackup) ChatModel backup) { this.candidates List.of(primary, backup); } Override public ChatResponse call(Prompt prompt) { for (ChatModel model : candidates) { try { return model.call(prompt); } catch (Exception ex) { log.warn(model instance failed, switch to next, ex); } } throw new IllegalStateException(all chat models unavailable); } // stream方法类似处理 }这层还应该承担“提示词工程”的沉淀任务。把常用的Prompt片段做成数据库表或资源文件业务侧按场景ID引用而不是在代码里拼字符串。立派之后运营人员甚至可以自己调整话术。3.4 会话与上下文RDS落地、Redis缓存别让记忆只活在内存里前面提到会话记忆要落地。Spring AI Alibaba的ChatMemory接口有现成的实现但底层存储需要你自己定。我个人推荐组合方案会话的元数据放Redis历史消息的持久化放RDS。Redis负责快速读取最近20条上下文RDS负责全量历史回溯。为什么这么拆因为每次请求模型时都往数据库查全部历史消息会很慢而Redis的list结构天然适合存放按时间排序的消息序列。建表可以简单一点但要留好索引。我给过一个生产可用的结构CREATE TABLE ai_chat_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content MEDIUMTEXT NOT NULL, tokens INT DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_session_time (session_id, created_at), KEY idx_user_time (user_id, created_at) );字段类型上有个容易踩的坑消息内容千万别用VARCHAR(255)大模型一句话轻松超过这个长度。我用的是MEDIUMTEXT能存16MB足够聊天场景使用。content存JSON时同理别用太小的长度限制。4. 阿里云底座里那些“立派”必备的细节服务化改造不只是写代码还要处理一堆基础设施细节。阿里云相关的配置我单独拎出来讲因为这些坑和数据无关、和代码无关纯粹是“无人替你踩过”的暗礁。4.1 Maven私服与镜像先从拉依赖不卡壳开始这个细节听起来很低级但在国内网络环境下Maven仓库速度直接影响整个团队的开发体验。Spring AI Alibaba的很多依赖包括spring-ai-alibaba-starter如果直接从中央仓库拉断断续续的让人想砸电脑。我的~/.m2/settings.xml里常年配着阿里云仓库镜像mirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors注意mirrorOf要写成central如果你图省事写*会把本应走私有仓库的依赖也拦过去反而搞出诡异的依赖解析问题。这个我踩过当时排查了很久才发现是镜像拦截了私有仓库。4.2 RDS连接池与建表会话数据要能睡得安稳生产环境不建议用本机MySQL直接买阿里云RDS基础版也能抗住绝大多数场景。Spring Boot连接RDS时连接池参数要比默认配置更保守一点。我现在的配置是这样的spring: datasource: url: jdbc:mysql://your-rds-instance.mysql.rds.aliyuncs.com:3306/ai_service username: ai_service password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000connection-timeout设成3秒是为了避免模型高峰时请求全堵在拿连接上max-lifetime设成30分钟是为了避开MySQL服务器端的连接空闲回收。这两项是RDS场景下的标准优化不调的话业务低谷时容易出现Communications link failure高峰时又容易连接耗尽。4.3 OSS存储桶与SSL证书静态资源和域名安全AI服务经常要存取知识库文件。小项目放本地磁盘没问题但一旦涉及多节点部署本地文件就成灾难。阿里云OSS的存储桶很适合放Prompt模板附件、知识库原始文档。说一个我注意到的细节OSS的访问凭证要区分“写入权限”和“读取权限”。业务服务器只需要上传文件时用有写权限的RAM账号前端读取时走CDN加公开读而不是把所有文件的读写密钥都暴露给前端。否则你就是把一个无限流量成本的炸弹交给了浏览器。SSL证书方面阿里云每年都有免费证书额度但免费证书有效期短通常三个月或一年到期续期很容易被忽略。我的做法是写一个自动化提醒任务在证书到期前30天往钉钉群发消息。续期流程在阿里云控制台可以一键完成但如果你对接的是自己的域名建议把证书部署到负载均衡SLB或CDN上而不是在每台ECS上手动放证书文件。4.4 ECS frp没有公网IP也能把服务露出来经常有朋友在本地或内网环境跑SpringAI服务想给远端的同事、客户预览但公司没有公网IP。阿里云ECS frp是目前很实用的一套组合在一台有公网IP的阿里云ECS上部署frps作为服务端内网机器跑frpc把本地SpringAI端口映射到ECS的公网端口。frpc最小配置长这样[common] server_addr your-ecs-ip server_port 7000 token your-token [spring-ai] type tcp local_ip 127.0.0.1 local_port 8080 remote_port 8080跑起来之后其他人访问http://your-ecs-ip:8080就能直接打到内网服务。这个方案适合演示和联调不推荐长期承载生产流量性能和安全性都比不上SLB私网互通。另外frp本身的web管理面板端口也要在ECS安全组里放行不然就会出现“frp进程活着但面板访问不了”的诡异情况后面我会专门讲。4.5 容器化与镜像源老CentOS怎么办如果你还在CentOS 6.10、7这类老系统上跑Java应用第一件事是把yum源换成阿里云的镜像源不然装个依赖等半小时是很正常的。CentOS 6.10的官方源早已停止维护不换源你连yum install都跑不顺。不过我更建议一步到位用Docker打包你的SpringAI服务。构建镜像时基础镜像用带阿里云镜像源加速的版本Maven构建阶段依赖阿里云仓库运行阶段只保留JRE。这样不管你的宿主机是CentOS还是别的发行版交付形态完全一致后面扩机器就是一条命令的事。5. 踩坑实录立派路上我挨过的五记闷棍服务化改造不是照着架构图做完就完事真实运行的坑多到能写一本册子。这五条是我最有代表性的遭遇每条都伴随一次凌晨的排查。5.1 坑一SSE流式输出中文乱码现象是前端控件收到的事件流里中文内容时有时无有时干脆变成问号。排查链路如下先怀疑编码问题检查ServerSentEvent输出时是否设置了text/event-stream;charsetUTF-8。Spring MVC默认的字符集有可能是ISO-8859-1如果没有显式指定中文就会乱。如果你用了WebFlux还要确认MediaType.TEXT_EVENT_STREAM是否带UTF_8参数。我最终的解决方案是在配置类里显式指定消息转换器Bean public HttpMessageConverterString responseBodyConverter() { return new StringHttpMessageConverter(StandardCharsets.UTF_8); }同时在前端用EventSource或fetch的ReadableStream做解码时固定TextDecoder的编码为utf-8。两头都锁死乱码彻底消失。5.2 坑二RDS连接池被打满sessions表挤爆上线第一个月某天突然收到RDS的连接数告警。看监控连接池20个连接全部活跃数据库CPU飙升。再看慢日志全是INSERT INTO ai_chat_message和那条查询最近消息的SQL。根因有两层。第一层是没做会话消息的批量写入前端每次SSE推送完整个响应后端就把整段聊天记录拆成好几条SQL逐条插入。一次聊天涉及20轮对话就产生几十次数据库往返。第二层是“最近消息”查询没有覆盖索引随着表数据量增长扫表越来越慢。修复方案写入合并成批量batchInsert一次事务写若干条查询语句强制走(session_id, created_at)联合索引再加一个定时任务把超过30天的历史会话归档到冷表。优化后同样的并发量数据库负载降了80%以上。5.3 坑三多Key轮询被限流百炼返回429我把主账号和备用账号都配进路由模型后自测感觉没问题但上线后依然偶发报错。看日志发现备用账号被限流了。问题出在轮询逻辑太“诚实”总是先打主账号主账号失败后才切备用。备用账号本身配额低所有流量在峰值期都堆给它它当然先挂。正确做法是“预热加权轮询”。主账号权重80%备用账号权重20%平时就让备用账号承担小流量那它不会因为从0突然拉满而触发限流。同时Spring AI Alibaba的调用要加退避重试百炼返回429时不要立即重试而是等Retry-After指定的秒数。5.4 坑四frp web面板能开但访问不了我自己也踩过frpc配置里加了web_protocol http和web_port 7001进程也起来了ECS安全组也放了7001端口但在浏览器里访问就是不显示面板。排查发现是frp在运行时把面板信息打印在了进程日志里显示的监听地址是0.0.0.0:7001但我在ECS安全组里放行的是TCP协议而frp默认的web面板走的是HTTP协议。等等这里的关键在于你既要在安全组放行端口还要在frps配置里正确设置bind_addr和dashboard_addr让面板监听在能被公网访问的网卡上。另一个隐藏坑是如果你的ECS实例有多个网卡或走了安全组白名单还要确认没有在防火墙层面只允许内网IP访问。最后把frps的配置改成[common] bind_addr 0.0.0.0 bind_port 7000 dashboard_addr 0.0.0.0 dashboard_port 7001 dashboard_user admin dashboard_pwd your-strong-password再把安全组里7000和7001两个端口都放行问题解决。5.5 坑五上下文越长越贵Token预算失控会话记忆顾问生效后能记住上下文了但新问题来了对话轮数越多拼进模型的上下文越长Token费用节节攀升响应变慢甚至触发模型的最大长度限制。我理想中的方案是“滑动窗口压缩摘要”双轨制。滑动窗口保证最近5轮对话全量保留超过5轮的部分用一个摘要模型把老内容总结成一两句话。这样既保留关键信息又控制上下文体积。实现时我写了一个自定义Advisor在做上下文拼装前先对历史消息做裁剪。public class SlidingWindowAdvisor implements Advisor { Override public ChatResponse call(AdvisorChain chain, AdvisorRequest request) { // 在组装消息列表前只保留最近N条和一条summary消息 return chain.next(request); } }这个Advisor在MessageChatMemoryAdvisor之前执行能显著减少发送给模型的Token数。实测同一组会话费用下降了约35%响应时间也快了不少。6. 立派之后的日子运维与迭代的几条心得服务立起来了真正的考验才开始。最后分享几条我们在上线稳定期沉淀的心得每条都是真金白银换来的。6.1 灰度与切流先拿10%流量祭刀不要把新服务一次性切全量。即使你做了完整测试也无法保证生产环境的真实对话形态不踩雷。我的做法是按用户维度灰度比如先放10%的用户流量到新AI服务观察错误率和对话平均耗时稳定24小时后再逐步提升比例。灰度期间还要盯一个容易被忽视的指标模型内容拒绝率。同样一段用户输入旧服务可能正常回复新服务因为Prompt模板改动可能触发安全策略导致回复空白。百分比对比是发现这类问题的最快方式。6.2 可观测日志、埋点、链路追踪AI服务的可观测比普通服务更依赖结构化日志。我给每个对话都生成一个traceId从接入层开始就一直透传包括百炼API的响应时间、Token消耗、模型名、是否走降级分支。排查问题时一个traceId能串起全部链路。另一个技巧是给“Token消耗”专门建一张统计表按天、按用户、按模型维度聚合。这个数据既是成本账单也是性能优化的依据。阿里云RDS完全能扛住这种轻量聚合查询。6.3 成本视角模型调用和基础设施都要记账最后说说账单。AI服务的特点是成本随使用量线性增长不像传统服务有明确的固定成本。我每月盘点账单重点看三块百炼模型调用的Token费用、RDS存储和连接数费用、OSS流量费用。有时候一个小改动就能省下可观的费用。比如把不需要引用知识库的简单问答走更便宜的模型比如把超过30天的会话归档到冷存储比如给OSS开生命周期规则自动把过期文件转到低频访问层。这些不直接影响功能但能让你的服务在业务量增长时依然有利润空间而不是做得越多亏得越多。立派服务从来不是一次性的改造工程它更像一种持续的组织方式。踩过这些坑、补上这些细节之后你会发现SpringAI Alibaba的项目不再是“一堆能跑的接口”而是一个能稳定迭代、能控制成本、能放心交给团队维护的真正意义上的服务。下一步这个“门派”就可以考虑多模型混编、私有化知识库、甚至对外提供AI能力开放平台了。