ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring AI Alibaba停更后,Java AI应用开发的工程化选型与落地实践

Spring AI Alibaba停更后,Java AI应用开发的工程化选型与落地实践

1. 一个停更消息引发的技术选型焦虑

前几天在技术群里看到有人转了一张截图,说 Spring AI Alibaba 这个项目停更了,紧接着群里就炸了锅。有人感叹“Java 在 AI 这条路上果然走不通”,有人开始盘算着要不要转 Python,还有人直接甩出一句“Java 已死,有事烧纸”。我当时正在用这套东西做一个内部知识库的检索增强生成服务,看到消息的第一反应是:先别慌,把仓库的提交记录和 issue 区翻一遍再说。

翻完之后我发现,事情远没有标题党说得那么夸张。这个项目确实在一段时间内没有新的提交,但它的代码还在、依赖还在、已经跑通的功能也还在。更重要的是,它背后依赖的那套 AI 应用开发范式——也就是 Spring 生态里对接大模型能力的整体思路——并没有因为某一个具体项目的节奏变化而消失。真正值得聊的,不是“Java 还有没有希望”这种情绪化的命题,而是当我们在 Java 技术栈里做 AI 应用时,到底应该把哪些东西当作长期资产,哪些东西只是过渡期的脚手架。

这篇文章想做的事情很明确:把“Spring AI Alibaba 停更”这件事拆开来看,讲清楚它到底意味着什么、Java 做 AI 应用的真实能力边界在哪里、以及如果你正在或者准备用 Java 做 AI 相关开发,接下来应该怎么调整自己的技术选型和工程结构。不管你是刚接触 AI 应用开发的 Java 后端,还是已经在项目里用上了大模型能力的架构师,都能从下面这些内容里找到可以直接参考的判断依据和实操方案。

2. 停更不等于失效:先把事实和情绪分开

2.1 一个开源项目的生命周期到底怎么看

很多人看到“停更”两个字,脑子里自动翻译成“这个项目废了”。但在开源世界里,一个项目的状态其实有好几种完全不同的情况。第一种是活跃维护,有固定的提交节奏、issue 响应及时、版本按计划发布。第二种是维护模式,不再加新功能,但关键 bug 还会修,安全漏洞还会补。第三种是归档冻结,代码不再变动,但仓库保留,任何人都可以 fork 继续做。第四种才是真正意义上的废弃,连代码都找不到了。

Spring AI Alibaba 当时的状态更接近第二种和第三种之间。它的核心能力——比如对接通义系列模型、提供 Spring 风格的 ChatClient 和 EmbeddingClient 抽象、支持向量库集成——这些代码都是完整可用的。你拿它来做一个对话服务或者文档检索服务,跑起来没有任何问题。问题在于,如果它不再跟进上游 Spring AI 的接口变化,也不再加新的模型适配,那你在做长期项目时就需要考虑后续的维护成本。

这里有一个很关键的判断逻辑:你依赖的是一个项目的“能力”还是“更新”。如果你只是需要它现在已经提供的功能,那停更对你的影响很小,因为代码不会自己坏掉。但如果你需要它持续跟进新模型、新协议、新标准,那停更就是一个明确的信号,说明你需要在架构上留出替换空间。

2.2 Java 做 AI 应用的真实短板在哪里

群里那些说“Java 没希望”的人,其实大部分并没有真正用 Java 做过完整的 AI 应用。他们说的“没希望”,往往指的是“不能用 Java 训练大模型”。这个判断本身没错,但问题在于,绝大多数做 AI 应用的人根本不需要训练模型。

训练大模型是另一回事,那是需要几千张加速卡、几百人团队、几个月时间才能做的事情。而 AI 应用开发的核心工作是:调用模型接口、组织提示词、管理对话上下文、做检索增强、处理结构化输出、集成到业务系统里。这些事情的本质是工程问题,不是算法问题。而在工程问题上,Java 的优势非常明显:强类型、成熟的依赖注入、完善的事务管理、丰富的企业级集成经验、庞大的现有系统存量。

Java 真正的短板其实在另外几个地方。第一是 Python 生态里那些实验性的 AI 工具链更新太快,Java 这边跟进总是慢半拍。第二是很多模型提供方的官方 SDK 优先支持 Python,Java 版本要么没有,要么功能不全。第三是在数据处理和科学计算场景下,Python 的库确实更顺手。但这些短板并不影响 Java 作为 AI 应用“后端服务层”的定位。你完全可以用 Python 做数据处理和模型微调,用 Java 做面向业务的服务层,两者通过接口或者消息队列协作。

2.3 从“框架依赖”转向“协议依赖”的思路转变

这次停更事件给我最大的启发,是让我重新思考了 AI 应用架构里的依赖关系。以前我会很自然地引入某个框架,然后用它的抽象来写业务代码。比如用某个 ChatClient 来发请求,用某个 EmbeddingClient 来做向量化。这样做的好处是写起来快,但坏处是一旦这个框架不维护了,你的业务代码就跟它绑死了。

更稳妥的做法是把依赖层次往下压一层。不要依赖某个具体框架的客户端抽象,而是依赖模型服务本身的 HTTP 接口协议。现在主流的大模型服务基本都提供兼容 OpenAI 格式的接口,这个格式已经成了事实上的行业标准。你只要按照这个格式发 HTTP 请求,不管后面换哪个模型、哪个供应商,你的代码都不用大改。

这个思路转变带来的直接好处是:框架变成了可替换的“便利层”,而不是不可替代的“基础设施”。你可以继续用 Spring AI 或者任何其他封装来简化开发,但心里要清楚,真正让系统跑起来的是那些 HTTP 接口和 JSON 结构,不是某个特定的注解或者接口名。

3. Java AI 应用的核心能力拆解

3.1 对话服务:从接口调用到上下文管理

用 Java 做一个对话服务,最基础的部分就是发 HTTP 请求调用模型接口。这部分用 Java 的 HttpClient 或者 WebClient 都能做,没有什么技术难度。真正需要花心思的是上下文管理。

大模型本身是无状态的,它不记得你上一句说了什么。所谓的“多轮对话”,其实是每次请求时把之前的对话历史一起发过去。这就带来几个工程问题:历史消息怎么存、存多久、超长了怎么截断、不同用户的会话怎么隔离。

我在实际项目里的做法是:会话历史存在 Redis 里,用用户 ID 加会话 ID 作为 key,设置一个合理的过期时间。每次请求时取出最近 N 条消息,加上系统提示词和当前用户输入,组装成完整的消息列表发给模型。N 的取值需要根据模型的上下文窗口大小和你的业务场景来定。比如模型支持 8K 上下文,系统提示词占了 500 token,那留给历史消息的空间大概就是 7000 token 左右,按每条消息平均 100 token 算,大概能放 70 条。但实际不会放这么多,因为历史太长会让模型注意力分散,通常保留最近 10 到 20 轮就够了。

这里有一个容易踩的坑:不要把整个对话历史原封不动地一直往后传。我见过有人为了“让模型记住更多”,把几百条历史消息全塞进去,结果不仅响应变慢、成本飙升,模型的表现反而下降了。正确的做法是做一层摘要或者关键信息提取,把久远的历史压缩成一段简短的背景描述。

3.2 检索增强:向量化与相似度检索的工程实现

检索增强生成是目前 Java AI 应用里最有实用价值的方向之一。它的核心思路是:把文档切块、向量化、存进向量库,用户提问时先检索出最相关的几个文档块,把它们作为上下文一起发给模型,让模型基于这些内容来回答。

在 Java 里做这件事,关键环节有三个。第一个是文档切块。切块的大小和重叠度直接影响检索效果。切太大,一个块里混了多个主题,检索出来噪声多;切太小,信息不完整,模型拿到的上下文不够。我的经验是中文文档按 300 到 500 字一块,重叠 50 到 100 字,这样既能保证语义完整,又不会引入太多无关内容。

第二个是向量化。你可以调用模型服务提供的 embedding 接口,也可以用本地的向量化模型。调用接口的好处是简单、效果好,坏处是每次都要发网络请求,大批量文档处理时比较慢。本地模型的好处是快、免费、数据不出本地,坏处是需要额外的部署和资源。我一般会在文档量不大的时候直接用接口,文档量超过几万条时考虑本地部署。

第三个是向量检索。Java 这边可以用的向量库有 Milvus、Weaviate、Qdrant 等,它们都提供 Java 客户端。如果数据量不大,用 Redis 的向量检索功能也够用。检索时要注意相似度阈值的设置,太低会召回不相关的内容,太高会漏掉有用的信息。我通常会把阈值设在 0.7 左右,然后取 top 5 的结果,再根据实际效果微调。

3.3 结构化输出:让模型返回可解析的数据

很多业务场景需要模型返回结构化的数据,比如从一段文本里提取姓名、电话、地址,或者把自然语言转换成 SQL 查询。这时候就需要用到结构化输出能力。

最直接的做法是在提示词里明确要求模型返回 JSON 格式,然后在 Java 侧用 Jackson 或者 Gson 解析。但这样做有一个问题:模型有时候会不听话,返回的 JSON 格式不对,或者多加了说明文字,导致解析失败。

更稳妥的做法是使用模型服务提供的结构化输出功能。现在很多模型都支持指定输出格式,比如要求返回符合某个 JSON Schema 的数据。这样模型会在生成时保证格式正确,你拿到之后直接反序列化就行。如果模型不支持这个功能,那就需要在提示词里把格式要求写得非常明确,并且在解析失败时做重试或者降级处理。

我在项目里封装了一个通用的结构化输出工具类,核心逻辑是:先尝试直接解析,失败后把错误信息拼回提示词让模型重新生成,最多重试两次。这个方案在实际使用中能把解析成功率从 85% 左右提升到 99% 以上。

4. 工程化落地的关键环节

4.1 模型调用的超时、重试与降级策略

调用外部模型服务,网络抖动和服务波动是常态。如果不做任何保护,一个超时就可能让整个请求链路卡住。我在项目里对模型调用做了三层保护。

第一层是超时控制。连接超时设 5 秒,读取超时设 60 秒。读取超时之所以设这么长,是因为大模型生成一段几百字的回复确实需要几十秒,设太短会导致正常请求被误杀。但也不能不设,否则遇到服务端 hang 住的情况,线程会被一直占用。

第二层是重试。对于超时和 5xx 错误,做最多两次重试,每次间隔 1 秒。注意不要对 4xx 错误重试,那通常是请求本身有问题,重试也没用。重试时要确保请求是幂等的,对于对话生成这种场景,重试意味着可能生成两次,需要在业务层做去重或者接受这种不确定性。

第三层是降级。当模型服务完全不可用时,不能直接给用户报错。我的做法是准备一个兜底回复,比如“当前服务繁忙,请稍后再试”,同时把请求记录到队列里,等服务恢复后异步处理。对于非实时场景,比如后台的文档摘要生成,降级策略就是直接跳过,等下一轮再处理。

4.2 提示词模板的管理与版本控制

提示词是 AI 应用里最容易被忽视但又极其重要的部分。很多人把提示词硬编码在 Java 代码里,改一次就要重新编译部署,非常不方便。更好的做法是把提示词抽出来,作为配置或者模板文件管理。

我一般会在项目里建一个 prompts 目录,每个提示词一个文件,用模板引擎(比如 FreeMarker 或者 StringTemplate)来渲染变量。文件名带上版本号,比如document-summary-v2.ftl。这样修改提示词不需要改代码,只需要改文件,重启服务或者刷新配置就能生效。

更进一步的做法是把提示词也纳入版本控制,每次修改都记录变更原因和效果对比。因为提示词的改动对输出质量的影响非常大,有时候改一个词就能让效果天差地别。没有版本记录的话,出了问题很难回溯。

4.3 成本控制:token 计量与缓存策略

用大模型是要花钱的,尤其是当你的应用有一定用户量之后,token 消耗会非常可观。我在项目里做了两件事来控制成本。

第一件是 token 计量。每次调用模型后,记录输入 token 数、输出 token 数和对应的费用。这些数据按用户、按接口、按天汇总,方便分析成本构成。很多模型服务的响应里会带 token 使用信息,直接取来用就行。如果没有,就需要自己估算,中文大概一个字对应 1.5 到 2 个 token。

第二件是缓存。对于相同或者相似的请求,如果之前已经生成过结果,直接返回缓存,不再调用模型。缓存的 key 可以用请求内容的哈希值,缓存的有效期根据业务场景来定。比如 FAQ 类的问答,缓存可以设长一点;实时性要求高的场景,缓存就设短一点或者不用。我在一个内部知识库项目里加了缓存之后,模型调用量下降了大约 40%,效果非常明显。

5. 常见问题与排查实录

5.1 模型返回内容不完整或截断怎么办

这个问题通常有两个原因。第一个是 max tokens 设置得太小,模型生成到一半就被截断了。解决方法是根据业务需要调大 max tokens,但要注意模型的上下文窗口上限,输入加输出的总 token 数不能超过这个上限。第二个原因是网络传输中断,尤其是在流式输出的场景下。这时候需要在客户端做好拼接和校验,发现内容不完整时重新请求。

5.2 向量检索召回率低怎么优化

召回率低一般从三个方向排查。首先是切块策略,检查块的大小和重叠度是否合理,可以尝试不同的参数组合看效果。其次是 embedding 模型的选择,不同模型对中文语义的理解能力差异很大,可以换一个模型试试。最后是检索参数,调整相似度阈值和 top K 的值,有时候放宽阈值能召回更多相关内容。

5.3 并发请求下响应变慢的排查思路

并发变慢通常不是模型本身的问题,而是你的服务端有瓶颈。按这个顺序排查:先看线程池是不是太小,模型调用是阻塞操作,需要足够的线程来支撑并发;再看连接池配置,HTTP 连接复用没做好会导致频繁建连;然后看 Redis 或者数据库有没有慢查询;最后看模型服务端是不是有速率限制,触发了限流会导致请求排队。

问题现象可能原因排查方法解决方向
返回内容截断max tokens 太小检查请求参数调大 max tokens
检索结果不相关切块太大或阈值太低查看召回内容调整切块和阈值
并发时响应慢线程池或连接池不足查看线程和连接状态扩容线程池和连接池
解析 JSON 失败模型输出格式不稳定打印原始返回内容使用结构化输出或加重试
成本增长过快无缓存或历史消息过长统计 token 消耗加缓存、压缩历史

5.4 几个我踩过的坑和对应的解法

第一个坑是在循环里调用模型。有一次我写了一个批量处理文档的逻辑,在 for 循环里逐条调用模型做摘要,结果几千个文档跑了几个小时,还触发了限流。后来改成批量提交加异步处理,速度提升了十几倍。

第二个坑是忽略字符编码。模型返回的中文内容在某些情况下会出现乱码,排查后发现是 HTTP 响应解析时没有指定 UTF-8 编码。这个问题在 Windows 环境下特别容易出现,加上编码指定后就正常了。

第三个坑是提示词里带了太多示例。为了让模型理解任务,我在提示词里放了十几个示例,结果输入 token 暴涨,成本翻了好几倍,效果却没有明显提升。后来精简到三个典型示例,效果基本持平,成本降了一半以上。

6. 技术选型的长期判断

回到最开始那个问题:Java 还有希望吗?我的判断是,Java 在 AI 应用开发这个层面的希望从来就没有消失过,只是很多人把“AI 开发”和“模型训练”混为一谈了。模型训练确实是 Python 的天下,但 AI 应用开发是一个工程问题,而工程问题恰恰是 Java 最擅长的领域。

Spring AI Alibaba 的停更,与其说是一个技术路线的失败,不如说是一个提醒:不要把业务架构建立在某一个具体框架的持续维护上。你应该依赖的是模型服务的接口协议、是向量检索的数学原理、是工程化的超时重试降级策略,这些东西不会因为某个项目的停更而失效。

如果你现在正在用 Java 做 AI 应用,我的建议是:保持现有的框架封装,但把核心的模型调用逻辑抽出来,做成不依赖特定框架的独立模块。这样即使将来换框架,你的业务代码也不用动。如果你还在犹豫要不要用 Java 做 AI 应用,我的建议是:先想清楚你的场景是工程问题还是算法问题,如果是工程问题,Java 依然是那个最稳妥的选择。

最后分享一个我在实际项目里验证过的做法:把模型调用封装成一个独立的内部服务,对外提供 HTTP 接口,业务系统通过这个接口来使用 AI 能力。这样做的好处是,模型相关的依赖、配置、密钥都集中在一个地方管理,业务系统不需要关心用的是哪个框架、哪个模型。将来要换模型或者换框架,只需要改这一个服务,所有业务系统都不用动。这个架构思路在 Spring AI Alibaba 停更的消息出来之后,让我少了很多焦虑,因为我知道我的业务代码跟那个框架之间隔了一层,换掉它只是改一个服务的实现而已。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进