很多人以为升级 Spring Boot,只是改几个版本号。
真正升级过一次大版本的人都知道:版本号只是开始,架构清算才是正戏。
Git 仓库: langchain4j-spring-agent/langchain4j-spring-ai
最近,我把自己的 AI 工程脚手架做了一次跨度很大的升级:
- Spring Boot 升级到 4.0.2
- Spring Cloud 升级到 2025.1.1
- Spring AI 升级到 1.1.3
- LangChain4j 升级到 1.12.2
- MCP 升级到 0.17.2
本来我以为,这会是一场"改依赖、修编译、跑测试"的常规升级。
结果一动手,我就发现:
这根本不是一次升级。
这是一场针对整个 AI 工程结构的"大体检"。
因为一旦主框架跨代,你迟早都要面对这些问题:
- 哪些模块已经沦为历史包袱?
- 哪些服务还值得继续作为主线维护?
- 旧的设计是不是已经不适合现在的 AI 工程演进了?
- 文档是不是早就跟代码脱节了?
- 你到底是在维护一个"能持续进化的工程",还是一堆功能堆出来的 demo?
于是这次我没有停留在"把项目升到 Boot 4"这一步。
我顺手把整个仓库做了一轮 模块收敛 + 主线重构 + 文档重写 + 前端同步修正。
一句话总结这次升级:
升级 Spring Boot 4.0.2,只是导火索。真正发生的,是整个 AI 项目开始从"试验场"走向"主线工程"。
一、为什么说 Spring Boot 4.0.2 这次升级,不是"改版本号"那么简单?
很多人看到升级日志,第一反应是:
"不就是把
spring-boot.version改成4.0.2吗?"
如果只是一个小 demo,这么说没毛病。
但如果你维护的是一个多模块 AI 工程,里面同时有:
- 对话服务
- RAG 检索
- MCP 工具服务
- 安全权限模块
- 日志模块
- 前端多套 UI
- 技能 Agent
- 简历 RAG
- 智能分割流程
那升级的影响,就绝不只是 pom.xml 里那一行版本号。
这次我最明显的感受有 3 个。
1)Starter 体系真的变了
尤其是和 Spring Boot 生态深绑定的组件,比如 MyBatis-Plus,这次就不是原来那套惯性写法了。
我最终统一切换到了 Boot 4 对应的 starter。
这件事表面看只是依赖替换,实际上影响的是一整套东西:
- 数据源配置
- 自动装配行为
- 多模块依赖一致性
- 后续新模块的基线写法
也就是说,升级不只是"修能跑",而是要重新确定:
以后整个工程应该按哪套基线继续演进。
2)很多旧模块必须重新判断生死
以前做项目时,很容易出现这种情况:
- 先搞一个
chat - 然后做一个
chat-v1 - 再做一个
chat-v2 - 安全模块也会有历史版本
- 某些 RAG 分块实验也单独留成模块
刚开始看,这像是在"保留探索痕迹"。
但升级时你会突然发现:
这些历史模块会持续制造认知噪音。
比如:
- 新人不知道该看哪个模块
- 文档不知道该写哪个模块
- 前端不知道应该对接哪个版本
- 你自己后面都不想维护多个平行宇宙
于是这次我决定不再模糊处理,而是直接做结构收敛。
3)最危险的不是代码报错,而是文档早就错了
这次还有一个非常深的感受:
代码报错不可怕,可怕的是代码已经变了,README 还在讲旧世界。
代码错了你很快能发现。
文档错了,通常要等别人被误导之后你才意识到它的杀伤力。
所以这次我同步重写了:
- 根目录
README.md - 后端聚合工程
README.md TECH_WHITEPAPER.md
因为升级完成后,如果文档不重写,那这次升级就只完成了一半。
二、这次升级里,我做的最重要决定不是"加模块",而是"删模块"
很多人做架构升级,会本能地想:
"先别删,先兼容着,留着以后再说。"
但真实情况往往是:
你今天不删,明天它就会继续制造维护成本。
这次我非常明确地把项目主线重新收敛了。
我保留下来并继续强化的主线模块
langchain4j-spring-ai-chat-v2langchain4j-spring-ai-security-v1langchain4j-spring-ai-seg-flowlangchain4j-spring-ai-skillslangchain4j-spring-ai-resume-raglangchain4j-spring-ai-swagger-mcp
我不再把它们当主线维护的模块
langchain4j-spring-ai-chatlangchain4j-spring-ai-chat-v1- 旧
langchain4j-spring-ai-security langchain4j-spring-ai-rag-chunk
这一步其实非常关键。
因为一旦主线不明确,后面所有事情都会变得混乱:
- 文档很难写
- 代码 review 很难统一标准
- 新功能不知道放哪
- 分支管理会越来越乱
所以这次我给自己定了一个原则:
升级窗口,就是做架构减法的最佳窗口。
这句话我非常建议做项目的人记住。
三、这次升级后,项目里最值得说的两个新增主线:Skills Agent 和 Resume RAG
很多项目升级后,只会留下一个"版本升级完成"的提交记录。
但我这次不想只做"升级成功",我更想让项目升级之后 更有方向感。
所以这次我重点扶正了两个模块。
1. Skills Agent:终于把"技能目录"做成一个真正能跑的服务了
这是这次升级里我最满意的变化之一。
过去很多人做 skills / prompt / agent 能力时,容易停留在:
- 读取本地文件
- 简单拼 prompt
- 调一下模型
- 打印个结果
这种做法能验证想法,但很难长期演进。
所以这次我把它正式做成了一个 独立 Skills Agent 服务。
它现在已经不仅仅是"会读 skill 文件",而是具备完整的服务能力:
health/info/refresh- 同步对话
chat - 流式对话
chat/stream - 会话创建、列表、归档
- 历史消息分页回放
- 模型管理
- 会话级
modelCode - Redis 记忆窗口
- 摘要压缩
- MySQL 落库
也就是说,现在的 Skills 已经具备这种能力:
不只是"读技能",而是"围绕技能做完整对话闭环"。
这意味着它可以往更多方向继续演进:
- 本地技能助手
- 命令型 Agent
- 团队共享技能平台
- 与 Swagger MCP / 外部工具服务联动
- 个人工作流编排
更重要的是,我还顺手给它配了一套新前端:
langchain4j-spring-ai-ui-skills
这个前端现在已经支持:
- 概览页
- 会话管理
- 历史消息查看
- 模型管理
- 同步 / 流式对话
从"能调用"到"能管理",它已经从一个实验能力变成了一个正式主线。
一句话总结:
Skills Agent 的真正价值,不在于能对话,而在于它已经具备了工程化落地的骨架。
2. Resume RAG:终于不再只谈"通用 RAG",而是做成了业务化 RAG
另一个我这次重点新增和强化的模块,是:
langchain4j-spring-ai-resume-rag
我越来越觉得,AI 工程里最容易陷入的一种空转是:
天天讲 RAG、讲向量、讲知识库,但始终没有一个真正贴业务的闭环场景。
所以这次我直接把一个比较真实的业务域拉出来做:简历 RAG。
这个模块现在已经打通了完整链路:
- 简历文件上传
- 文本内容抽取
- LLM 语义切窗
- Elasticsearch 主文档写入
- Qdrant 向量写入
- RustFS 文件存储
- 下载 / 分页 / 检索 / 向量详情查看
这就和"做一个通用聊天页"完全不是一个层次的事了。
因为它真正落到一个更贴近业务的链路里:
文件 → 解析 → 结构化 → 切窗 → 向量化 → 检索 → 返回结果
这类模块的价值是非常高的,因为它更容易让项目往真实场景里落:
- 人才库检索
- 候选人画像分析
- 简历向量召回
- JD 与简历匹配
- 招聘侧 AI 能力中台
如果说 Skills Agent 是这次升级里"Agent 主线"的代表,
那 Resume RAG 就是"业务化 RAG 主线"的代表。
四、升级过程中,那些看起来像小 bug 的问题,其实都在暴露架构问题
这次升级里我还修了一些表面很小的问题,但我后来发现,很多小问题其实都在提醒我:
真正该修的,往往不是表象,而是状态设计和工程边界。
1. Skills 对话页的历史消息为什么会"消失又出现"?
我在做 ui-skills 时,遇到一个很典型的问题:
- 第一次提问,大模型正常回答
- 第二次提问时,第一次 AI 的回答在页面里不见了
- 刷新页面后,它又从历史记录里出来了
这个现象非常迷惑。
但顺着查下去,问题本质就很清楚了:
- 流式输出时,AI 回复先存在临时状态里
- 临时状态没有及时落到正式消息数组
- 下一次发送前又触发了一次历史覆盖
- 后端历史与前端内存状态的同步时机没处理好
最后修复的核心思路是:
- 流式结束后立刻把 AI 内容写进正式消息列表
- 不要在下一次发送前随手整体覆盖本地消息
- 切换会话、清理流式状态、加载历史这些动作要拆清楚
这个问题让我再次确认了一件事:
前端很多"看起来是显示问题"的 bug,本质都是状态模型设计问题。
2. rustfs-data 运行时目录被 git 管住了,分支切换直接卡死
我后来还遇到了一个很典型的工程化问题:
resume-rag 的 RustFS 运行时目录被提交进了 git,结果导致切分支时直接报错:
- 本地有改动
- checkout 会覆盖这些文件
- Git 阻止切换分支
这种问题表面不大,但它非常能说明工程边界是否清楚。
因为 docker/rustfs-data/ 这种目录,本质上属于:
- 运行时数据
- 容器挂载数据
- 本地缓存 / 元信息
它天然就不应该进入版本管理。
后面我把它补进了 .gitignore,这件事虽然小,但它其实提醒了我一个很重要的点:
真正成熟的工程,不只是代码干净,运行时边界也必须干净。
五、这次升级后,我终于觉得这个项目不像"功能堆砌仓库"了
这是我这次升级后的最大感受。
以前这个项目虽然功能不少,但会有一种很明显的感觉:
- 方向很多
- 模块很多
- 试验也很多
- 但主线感不够强
升级之后,这种感觉明显变了。
现在整个项目最值得继续投入的主线已经很清楚:
chat-v2skillsresume-ragswagger-mcpseg-flowsecurity-v1
把这些模块放在一起看,会发现这个仓库已经能拼出一个相当完整的企业 AI 工程底座:
现在这套工程能覆盖什么能力?
- 多轮聊天
- 流式输出
- Redis / MySQL 上下文链路
- 检索增强
- Swagger 转 MCP Tool
- Elasticsearch MCP 检索工具
- Skills Agent 技能服务
- Resume RAG 业务化检索
- 智能分割
- JWT / RBAC 安全治理
- 日志与测试脚本
换句话说,现在它不再像"做过很多功能的仓库",而更像:
一个边界更清晰、主线更明确、后续更容易继续扩展的 AI 工程框架。
六、为什么我说这次升级最大的收获,不是"升成功了",而是"终于敢做减法了"
做工程时间长了会发现一个特别真实的现象:
加东西,永远很容易。
删东西,永远最难。
因为删除一个模块,意味着你要承认:
- 它已经完成历史使命
- 它不值得继续当主线维护
- 它会干扰后续判断
- 它继续存在的成本,已经高于它的价值
这件事比改代码难多了。
但一旦你做了,收益也特别明显:
- 文档更容易统一
- 新人更容易理解
- 模块边界更清晰
- 分支策略更容易管理
- 下一次升级成本更低
所以如果要我用一句话总结这次升级最大的收获,那我会说:
真正让工程变强的,不是你加了多少能力,而是你有没有勇气趁升级时把不该留下的东西清掉。
这是我这次最深的体会。
七、如果你也准备升级到 Spring Boot 4,我给你 5 条很实在的建议
建议 1:不要把升级理解成"改依赖版本"
版本号只是入口。
真正要重看的,是模块边界、依赖基线、运行时边界和文档口径。
建议 2:趁升级窗口做模块收敛
平时你舍不得删的东西,升级时反而最适合删。
因为升级本身就是一次天然的"全局检查"。
建议 3:一定要同步重写 README 和白皮书
否则你最后得到的,只是一个"代码升级了、文档过时了"的半成品工程。
建议 4:明确哪些目录属于源码,哪些目录属于运行时
像日志、缓存、对象存储、docker 挂载目录,一定要尽早 ignore。
建议 5:把"主线模块"写清楚
这是给后来的人看的,也是给未来的自己看的。
你今天不明确主线,半年后你自己都未必看得懂当时为什么这样拆。
八、最后:这次升级表面在升框架,本质上是在重建项目秩序
回头看这次升级,我觉得最有意思的地方在于:
表面上,我只是把项目升级到了 Spring Boot 4.0.2 。
但本质上,我其实做的是一件更重要的事:
借一次大版本升级,重新建立了整个 AI 工程的主线秩序。
现在这个项目里,哪些模块是主线、哪些模块只是历史、哪些能力值得继续投入,终于比以前清楚了很多。
我最终得到的,不只是一个"能编译通过的新版本",而是一个更像样的 AI 工程底座:
- 更少的历史包袱
- 更清晰的模块边界
- 更明确的能力主线
- 更贴近业务的 Skills 与 Resume RAG 能力
- 更一致的文档体系
如果你现在也在维护一个 AI 项目,或者正准备把老项目升级到新基线,我特别建议你别只盯着版本号。
你真正该问自己的问题是:
这次升级,我只是把依赖升上去了,还是顺手把整个项目真正整理了一遍?
很多时候,真正拉开差距的,不是你升级得多快。
而是你有没有借这次机会,把架构做得更干净。