起:一个看似简单的决定

我有个后端项目 firedemo,用 Spring AI 2.0-M1 对接 OpenClaw Gateway 做 AI 作业批改。用得其实很薄——就一个 ChatClient 发 HTTP 请求,连 Function Calling 都没碰。

“换成 LangChain4j 吧,Agent 编排更成熟,简历上也好看。”

换依赖而已,能有多难?

承:五连坑

坑一:Jackson 双版本打架

1
Could not initialize class tools.jackson.databind.json.JsonMapper$Builder

Spring Boot 4 用 Jackson 3(tools.jackson),LangChain4j beta1 拖了个 openai4j → Retrofit → Jackson 2.xcom.fasterxml.jackson)。虽然包名不同,但 classpath 上有两个 Jackson 就是炸。

修:升到 1.0.0-beta3——这个版本去掉了 openai4j,自带了 JDK HttpClient。但坑一刚出,坑二就来了。

坑二:BOM 覆盖 Netty 版本

langchain4j-bom:1.0.0-beta3 把 Netty 从 4.2.x 拽回到了 4.1.x。Spring Boot 4 的 HttpConnectionLiveness 类直接找不到了。

修:不用 BOM,直接声明版本。但坑一+坑二浪费了 beta3 的时间,干脆直接拉最新稳定版。

坑三:API 大换血

升到 1.13.0 之后编译不过。ChatLanguageModelChatModelStreamingChatLanguageModelStreamingChatModel。改名也就算了,ChatModel.chat(String) 返回值从 ChatResponse 变成了 String……行吧。

坑四:Invalid HTTP method(本日 MVP)

1
2
3
4
5
6
HTTP request:
- method: POST
- url: http://localhost:18789/v1/chat/completions
- body: { ... 完全正确的 OpenAI 格式 ... }

Response: Invalid HTTP method

一模一样的内容用 curl 发就 200 OK,LangChain4j 发就报错。debug 了两小时,翻 JDK 源码才搞明白:

JDK HttpClient 默认会尝试 HTTP/2 升级(h2c),但 OpenClaw Gateway(Node.js/Express)不认识这个 upgrade,直接返回 “Invalid HTTP method”。

根本不是什么业务逻辑问题,纯粹是传输层协议协商失败。

修:非流式调用改回 Spring RestClient(HTTP/1.1,curl 同款),流式调用保留 LangChain4j 的 OpenAiStreamingChatModel

坑五:读超时连环爆

RestClient 上线后报 ReadTimeoutException——Netty 默认读超时 30s,但 AI 批改要 1-3 分钟。

修:切 JdkClientHttpRequestFactory + 显式 HTTP/1.1 + readTimeout = 5 分钟

转:后端逻辑层面的两个隐患

AI 批改时间拉长之后,之前没暴露的问题全冒出来了:

问题一:claimPendingMessages 误判僵尸消息

Redis Stream 的 claimPendingMessages 每 30s 扫一次,idle > 60s 就 claim 过来重新处理。但 AI 批改要 1-3 分钟——这意味着同一个消息会被 claim 好几次,多个消费者同时跑同一批任务。

修:idle 阈值 60s → 300s(5 分钟,比最长批改时间还多 1 分钟兜底)。

问题二:分布式锁到期

tryLock(0, 120, TimeUnit.SECONDS)——批改如果超过 2 分钟,锁自动释放,下一个消费者进来拿到锁,又开始处理同一个 submission。

修:lease time 120s → 180s

问题三:FAILED 提交污染数据中心

批改失败的 submission(如 AI 返回非 JSON)会留在数据库里,被 Dashboard 的统计查询计入学生数、提交数、分数分布,平均分被拉低,学生概算出偏差。

修:countByClassIdcountNewByClassIdcountDistinctStudentsByClassIdselectStudentOverviewByClassId 四条 SQL 里加 AND status = 'COMPLETED'

合:今天的遗产

变更 文件数
Spring AI → LangChain4j 1.13.0 3
HTTP 层改为 RestClient + JDK RequestFactory 1
claimPendingMessages idle 阈值 1
分布式锁 lease time 1
防重复处理状态检查 1
Dashboard 指标查询加 status 过滤 1

一点感想

换依赖这事,表面看是改 pom.xml 加几行代码,实际是一场 Java 生态兼容性的沉浸式体验:

  • Jackson 2 vs 3 的命名空间割裂
  • JDK HttpClient 的 HTTP/2 默认升级行为
  • BOM 版本管理对传递依赖的覆盖
  • 读超时、锁超时、僵尸消息认领——三个”超时”参数互相咬合

换框架不可怕,可怕的是你不知道底层在干什么。