为什么要看 Java 的 LLM 框架
团队要做个工单自动分类的小工具,调用 OpenAI。一开始我手搓 HttpClient 拼 JSON,发现每加一个能力就要重写一遍请求体、解析流、管上下文。更烦的是流式输出要自己处理 SSE,错误码要自己映射,超时和重试也要自己写。做了两个功能之后,代码里一半是在和 HTTP 打交道,而不是在解决业务。后来看到 LangChain4j,它把 Chain、Memory、Tool 三件事抽象得很干净,而且和 Spring Boot 集成几乎零配置,我花半天迁过去了,后面加功能的速度快了一倍。
Chain:把模型调用串成流水线
最基础的对话,用 AiServices 接口就能声明式拿到。你定义个接口,框架在运行时生成实现,不用写实现类:
public interface Assistant {
@SystemMessage("你是工单分类助手,只输出分类标签")
String chat(String userMessage);
}
Assistant assistant = AiServices.create(Assistant.class,
OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_KEY"))
.modelName("gpt-4o-mini")
.build());
这一层就是 Chain:输入文本,输出文本,中间可选地接 Retriever、OutputParser。我们加了 JSON 输出解析器,把模型返回的字符串直接映射成 Java 对象,省掉了手写的 Gson 反序列化。后来接到一个需求:分类结果要带"置信度",我在接口里加了个返回 record 的方法,框架自动把 JSON 解析成 record,零样板。对比之前手搓的 80 行解析代码,这一下子清爽了。
Memory:让对话记得住前文
默认每次调用模型都是无状态的。要保留上下文,得挂 Memory。LangChain4j 提供了几种实现,我用的是窗口型:
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(model)
.chatMemory(memory)
.build();
我们用 MessageWindowChatMemory,窗口设 10 条,超出就丢最早的。线上实测单会话上下文常驻内存约 3.2KB,2000 并发会话占 6.4MB,可接受。但窗口型 Memory 没有长期记忆,要做用户画像得换持久化实现——框架提供了写入 Redis 或数据库的 MemoryStore,我们后来接了 Redis,把会话跨重启保留,用户隔天回来对话还能接上上下文。
Tool:让模型会调我的代码
这是最惊艳的部分。模型本身算不了实时数据,我把查询订单的方法标注成 @Tool,框架自动生成 JSON Schema 交给模型:
@Tool("根据订单号查询物流状态")
public String queryOrder(@P("订单号") String orderId) {
return orderRepo.findStatus(orderId);
}
一次工单里用户问"订单 A123 到哪了",模型自己决定调用 queryOrder,拿到结果再组织语言。我们灰度对比过:纯靠模型记忆的准确率只有 61%,接了 Tool 之后升到 94%。背后是框架把 Tool 描述塞进 system prompt,模型返回 function_call 时在本地执行再回填。注意 Tool 方法别做太重的事,模型可能频繁调用,最好加个本地缓存,我们为此加了一层 Caffeine,把订单状态缓存 30 秒,避免同一个订单一秒内被查十几次。
和 Spring Boot 集成
依赖加 langchain4j-open-ai-spring-boot-starter,配置写在 application.yml,几乎不用写 Java 配置:
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_KEY}
model-name: gpt-4o-mini
temperature: 0.2
timeout: 30s
然后直接 @Autowired 注入 Assistant。我们把它包了一层兜底:模型超时 3 秒就走规则引擎,避免 LLM 抖动拖垮工单链路。另外我们做了个简单的限流器,单实例每秒最多 50 次模型调用,超出排队,防止把 API 配额打爆导致整条链路雪崩。
一个值得说的落地细节
Tool 调用是模型"自主决策"的,意味着你不能假设它一定调。有一次我们依赖 queryOrder 的结果做后续判断,结果模型觉得用户问的是退款政策,压根没调,直接回了政策文本,下游拿到空值。后来我们在代码里做了兜底:如果关键 Tool 没被调用且问题涉及订单,就主动补一次查询。这套"模型自主加代码守门"的模式,是我们用下来最稳的写法。
小结
LangChain4j 的抽象层次刚好:Chain 管流程、Memory 管状态、Tool 管能力扩展。比自己手搓省了至少一周,而且代码可读性高,新人也能很快接手。缺点是中文文档少,有些注解的语义得读源码,另外版本迭代快,升级时要注意 API 小改。整体值得在 Java 项目里一试,尤其适合想快速把 LLM 接进现有 Spring 系统的团队。我们目前已经把它用在工单分类、订单查询、知识库问答三个场景,统一了三套原本各写各的调用封装。