第一次在Java项目里接入大模型,我选了LangChain4j + Qwen
第一次在 Java 项目里接入大模型时,我其实挺抗拒的。网上一搜教程,铺天盖地都是 Python,我一个写 Java 后端的,总不能为了一个对话接口就把整套技术栈推翻重来。后来同事甩给我一个库,说你自己研究下。那个库就是 LangChain4j——Java 生态里把大模型能力封装得最顺手的一个框架,搭配通义千问(Qwen)的 OpenAI 兼容接口,从新建工程到第一次跑通问答,实测确实用不了五分钟。
这篇不搞源码分析、不整理论推导,就讲三件事:怎么把 Qwen 最快接进 Java 工程,接到之后做多轮对话、流式输出、结构化返回时怎么扩展,以及那些文档里不会写但你八成会踩的坑。全程用 Maven + JDK 17,跟着敲一遍就能跑起来。适合谁看?后端 Java 开发、想在现有系统里加 AI 能力的项目负责人,以及死活不想碰 Python 的 Spring 玩家。准备好 IDEA,我们直接开搞。
1. 先搞清楚 LangChain4j 到底帮你省了什么
1.1 它是为 Java 开发者准备的 LLM 集成层
接触过 Spring Boot 的朋友都知道,它把繁琐的配置封装成了“自动配置 + Starter”,让 Java 开发者不用再操心 Bean 管理那一堆破事。LangChain4j 做的事情本质类似:它在 HTTP 客户端之上做了一层模型抽象,把大模型的接入、消息组装、上下文管理、输出解析全部收敛成几个简单接口。
你只需要拿到一个ChatLanguageModel对象,调用generate("你好")就能拿到模型回复。至于底层是 HTTP 调用还是流式解析、消息格式怎么拼、Token 怎么计数,框架全部接管了。之前我用原生 HTTP 调 OpenAI 兼容接口,光 JSON 序列化就写了一百多行,换成 LangChain4j 之后这段代码缩到了不到十行。
这个库还提供了一层类似 Feign 的接口抽象叫AiService,后面会专门讲。它可以让你像定义 Mapper 接口一样定义“AI 方法”,方法注解里写提示词,参数用占位符映射,返回类型直接绑定对象。这套设计是 LangChain4j 区别于其他 Java AI 框架的核心竞争力,也是我坚持用它而不是手搓 HTTP 调用的主要原因。
1.2 为什么模型选 Qwen 而不是 ChatGPT 或别的
选模型是个现实问题。ChatGPT 官方接口在国内访问和支付都有门槛,大部分团队不会把核心业务押在一个不稳定链路上。Qwen 的优势在于:模型服务在国内,调用延迟低且稳定;接口完全兼容 OpenAI 协议格式,意味着 LangChain4j 里所有基于 OpenAI 协议的封装都能直接复用;对中文场景的理解能力和指令跟随在我实际测试里并不输给同体量的国外模型。
Qwen 系列大致分三档:qwen-turbo适合测试和低成本高频调用,qwen-plus是性价比和效果均衡的主流选择,qwen-max适合复杂推理和高质量生成。入门直接用qwen-plus或者qwen-turbo就够了,跑通了再根据场景切换模型名,一行配置的事。
1.3 了解两个核心抽象:ChatLanguageModel 与 ChatMessage
ChatLanguageModel是同步对话模型的入口,负责接收消息、调用大模型、返回文本。它背后有多个实现:对接 OpenAI 系用OpenAiChatModel,对接本地模型可以用 Ollama 的实现,还有针对各种云厂商的实现。因为它们都实现同一个接口,业务代码里切换底模型不需要改动上层逻辑。
ChatMessage则定义了消息结构,常见的有SystemMessage(系统提示词)、UserMessage(用户输入)、AiMessage(模型回复)。多轮对话就是维护一个消息列表,把历史消息轮流塞给模型。理解这两点,后面的代码就顺理成章了。
2. 动手前的准备:JDK、依赖和密钥
2.1 JDK 版本与构建工具选择
LangChain4j 新版本要求 JDK 8 及以上,但我在实际使用中强烈建议直接用 JDK 17 或 21。原因很朴素:新版本框架的编译字节码、依赖的底层库(比如 Jackson、gRPC 相关组件)对 JDK 8 的兼容性已经越来越差,跑着跑着冒出个UnsupportedClassVersionError很扫兴。而且 JDK 17 的虚拟线程、密封类、模式匹配这些特性,在做流式处理时写起来顺手得多。
构建工具我用 Maven,因为它在国内公司存量项目里占有率最高,而且 Maven 的依赖管理对新手更直观。Gradle 也能跑,只是依赖坐标和仓库配置略有差异,不影响核心代码。
2.2 引入 langchain4j 相关依赖
Maven 中央仓库有现成的 BOM,可以帮助统一版本管理。在pom.xml里先引入 BOM,再引入两个核心模块:langchain4j和langchain4j-open-ai。注意,Qwen 因为兼容 OpenAI 协议,所以走的是open-ai模块,不需要单独的国产模型模块。
<properties> <langchain4j.version>0.35.0</langchain4j.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>${langchain4j.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> </dependency> </dependencies>版本号建议去 Maven 中央仓库查一下最新发布版,我写的0.35.0是我在用的稳定版本,你用更新的版本问题也不大,但注意个别 API 在小版本间有微调,遇到编译报错优先看官方升级说明。
提示:如果你的工程用的是 JDK 8,建议找 0.31 之前的版本;0.32 之后部分模块开始要求 JDK 11+,别盲目升版本。
2.3 获取 API Key 和 Base URL
去模型服务商控制台注册账号,创建一个 API-KEY。这一步没什么技术含量,但有个细节要注意:开通模型服务后,会看到两个地址,一个是原生 DashScope 格式,一个是 OpenAI 兼容格式。LangChain4j 必须用 OpenAI 兼容格式的地址,也就是形如https://xxx.com/compatible-mode/v1这样的 URL。
我见过不止一个朋友把这俩搞混,原生格式填进去之后通通报 404。记住一点:compatible-mode是全小写,路径末尾必须带/v1,这个后缀不能丢。调用模型时不需要单独指定“模型服务商”,把 API Key 和 Base URL 配置正确后,modelName直接传qwen-plus即可。
3. 五分钟跑通第一版问答
3.1 最小可运行代码
新建一个 Java 类,直接写一个main方法,这是整个流程成本最低的验证方式。代码量少到不好意思:
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; public class QwenQuickStart { public static void main(String[] args) { ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("sk-your-api-key") .modelName("qwen-plus") .baseUrl("https://your-service.com/compatible-mode/v1") .build(); String answer = model.generate("你好,请用一句话介绍你自己"); System.out.println(answer); } }这段代码干了几件事:构建一个指向 Qwen 服务的模型对象,设置模型名和密钥,然后调用generate方法直接传入用户消息,拿到纯文本答复。OpenAiChatModel.builder()里面其实还有很多参数,比如temperature(温度)、maxTokens(最大输出长度)、timeout(超时时间),入门阶段用默认值就行。
跑起来之后,控制台会打印模型的回复内容。如果看到一串通顺的中文,恭喜,你已经完成了从零到一的全过程,耗时确实不会超过五分钟。
3.2 把密钥从代码里剥出去
上面的代码能跑,但直接把 API Key 写在代码里是给自己埋雷。项目一旦推送到 Git 仓库,哪怕后期删掉,密钥也会留在提交历史里。正确做法是从环境变量读取:
String apiKey = System.getenv("QWEN_API_KEY"); ChatLanguageModel model = OpenAiChatModel.builder() .apiKey(apiKey) .modelName("qwen-plus") .baseUrl(System.getenv().getOrDefault("QWEN_BASE_URL", "https://your-service.com/compatible-mode/v1")) .build();在 IDEA 的 Run Configuration 里配置环境变量,本地测试方便,部署到服务器时用运维平台注入。这个习惯建议从第一行代码就养成,别等泄漏了再后悔。另外,任何模型服务商的对话内容都会经过云端,不要在生产环境传敏感隐私数据。
3.3 验证连通性与基础排错
第一次运行如果没看到预期输出,按优先级排查三个点:第一,API Key 是否正确,注意控制台创建后只在弹窗里完整显示一次,复制时不要漏字符;第二,Base URL 是否正确,重点确认有没有/v1路径;第三,网络是否能访问模型服务的域名。
日志里出现401 Unauthorized,基本就是密钥无效或服务未开通;出现404,十有八九是 Base URL 路径不对;如果是超时,先看模型名填没填对,qwen-plus这种大小写敏感,写错也会导致服务端无法路由。把这些记下来,后面能少走很多弯路。
4. 从一次性问答进入到多轮对话
4.1 为什么单次调用走不了多轮对话
大模型本身是无状态的。你第一次调用generate("你好"),它并不知道你第二次说“刚才我讲了什么”指的是什么。要让它“记住”上下文,就必须把历史消息一块儿发给服务端,由模型阅读完整对话记录后再生成回复。这个机制既是记忆的基础,也是 Token 消耗的来源。
LangChain4j 里做多轮对话最朴素的方式,就是自己维护一个List<ChatMessage>,每次调用前把历史消息全部追加进去。比如第一轮是“你好”,第二轮调用时就要传“你好”和模型的回复,再加上“刚才我说了什么”。消息多了以后代码会变得很啰嗦,而且你得手动处理消息裁剪,不让列表无限膨胀。
4.2 用 MessageWindowChatMemory 自动管理历史
LangChain4j 提供了现成的内存窗口,叫做MessageWindowChatMemory。它内部维护消息列表,超过设定的窗口大小会自动丢弃最早的消息,避免请求体无限变大。这个“窗口”就是能保留的最近消息数,我项目里常用 10 到 20,具体看模型上下文长度和业务需要。
配合AiServices使用效果最好。先定义一个接口:
interface Assistant { String chat(String userMessage); }然后绑定模型和内存窗口,得到一个代理对象。之后每次调用assistant.chat("你好"),框架都会自动携带历史消息,并且把新回复也追加进内存。这一步之后,多轮对话在业务代码里看起来就像调用普通方法,跟说句话一样简单。
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(20); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build(); String reply = assistant.chat("我叫老周,记住这个名字"); assistant.chat("我叫什么?");第二个问题模型能准确答出来。这个模式还有另一个收益:Assistant是个普通 Java 接口,可以像 Spring Bean 一样管理、注入、替换实现,测试的时候也容易 mock。
4.3 流式输出:把“等待”变成“逐字显示”
同步接口有个用户体验问题:模型回答越长,前端等待越久,用户盯着转圈图标很容易流失。流式输出让回复内容像打字机一样一个字一个字蹦出来,体感上快很多,也更接近大模型产品该有的交互方式。
LangChain4j 提供了StreamingChatLanguageModel接口,OpenAI 兼容系列有对应实现。用法跟同步版本很像,但你需要传入一个流式响应处理器,在回调里接收增量内容:
StreamingChatLanguageModel streamingModel = OpenAiStreamingChatModel.builder() .apiKey(System.getenv("QWEN_API_KEY")) .modelName("qwen-plus") .baseUrl("https://your-service.com/compatible-mode/v1") .build(); streamingModel.chat("给我讲一个程序员冷笑话") .onPartialResponse(System.out::print) .onComplete(ignored -> System.out.println("\n---END---")) .start();onPartialResponse每收到一小段文本就触发一次,onComplete在全部生成完毕后触发。实际项目里通常把这些文本片段推给 WebSocket 或 SSE,让前端按顺序渲染。这套组合改造成本不高,但对话体验质的提升立竿见影。
5. 用 AiService 把 AI 能力变成普通接口
5.1 AiService:比抽象更抽象的一步
AiService是 LangChain4j 最有特色的设计。它允许你定义纯 Java 接口,在方法上写提示词注解,框架运行时自动生成实现。这个思路对长期维护的 Java 项目特别友好:AI 调用的粒度、提示词内容、参数映射全部集中在一个接口文件里,审查和修改都一目了然。
它的注解体系支持占位符。最常用的写法是@UserMessage,里面可以写完整提示词,也可以用{{it}}代表方法入参整体,或者用{{名字}}引用命名参数。这比把提示词拼在业务代码里干净多了。
5.2 带参数的定义方式
看一个带参数的例子。假设你要做一个旅游推荐助手,用户输入目的地和预算,模型给出建议:
interface TravelAssistant { @UserMessage("你是资深旅游规划师。用户想去{{destination}},预算{{budget}}元,请推荐三个景点并说明理由。") String recommend(String destination, int budget); }对应调用方:
TravelAssistant assistant = AiServices.builder(TravelAssistant.class) .chatLanguageModel(model) .build(); String advice = assistant.recommend("杭州", 1000);注意到没有?这里的提示词和 Java 方法参数混合得恰到好处,读代码的人想不明白都难。这就是我推荐用 AiService 的核心原因——提示词工程不再游离在代码之外,而是跟方法签名绑定在一起,变成了“可读的业务规则”。
5.3 结构化输出:让模型直接返回业务对象
大多数项目要的不是一段散文,而是能落库的结构化数据。LangChain4j 的AiService支持把返回值直接定义成 POJO,框架会在内部引导模型输出 JSON 并完成反序列化。
比如让模型把一句话拆成“意图 + 实体”:
record IntentResult(String intent, Map<String, String> entities) {} interface NlpService { @UserMessage("解析用户指令,返回JSON:意图为以下之一[查天气, 设提醒, 查日历],实体为关键信息。输入:{{it}}") IntentResult parse(String userInput); }框架会努力把模型输出转成IntentResult对象。如果模型偶尔输出不规范 JSON,可能抛解析异常,实际使用中我会在方法定义时加一句“只输出JSON,不要解释文字”,能显著提升成功率。结构化输出是把 AI 能力嵌入业务系统最实用的一招,强烈建议优先掌握。
6. 常见问题速查与避坑实录
6.1 高频报错与排查对照表
跑通之后你一定会修改参数、切换模型,然后遇到各种报错。我把这段时间高频踩到的问题整理成一张表,方便你按图索骥:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
401 Unauthorized | API Key 错误或未开通模型服务 | 控制台重新生成 Key,确认复制完整 |
404 Not Found | Base URL 路径不对,缺/v1 | 检查是否 OpenAI 兼容地址,补全路径 |
| 连接超时 | 网络问题或模型名错误 | 确认服务商域名可达,确认模型名大小写 |
InvalidParameter | 参数超出范围 | 检查maxTokens、temperature取值范围 |
| JSON 解析失败 | 模型输出了多余文字 | 提示词强调“仅输出JSON”,或用框架的 OutputParser |
| 内存溢出/请求过长 | 消息窗口设置过大 | 调小MessageWindowChatMemory窗口数 |
这六个问题覆盖了我 90% 的线上求助场景。尤其是 Base URL 和模型名写错这两类,报错信息容易误导人,比如模型名错了有时报的是 404 而不是“模型不存在”,排查的时候路径和模型名要一起检查。
6.2 成本控制:别让 Token 悄悄烧钱
很多入门朋友忽略 Token 消耗。多轮对话每次调用的 Token 数是累加的,历史越长成本越高。MessageWindowChatMemory 虽然会裁剪消息,但你设置的窗口是 20,意味着每次调用都要携带最近 20 轮内容,一轮对话几千 Token 很正常。
我们的做法分两步:一是调小窗口,实测量发现 6 到 8 就够撑住大部分对话场景;二是判断业务场景是否需要“记忆”,像客服工单摘要这类一次性任务,完全不需要带历史,直接每次新建模型调用,成本能省一半以上。maxTokens也要设个合理上限,默认值偏大,模型一旦话痨起来单次输出上千 Token,费用蹭蹭涨。
6.3 参数的调优经验
temperature是影响生成风格最重要的参数,取值 0 到 2,越低越稳定,越高越发散。做信息抽取、分类、结构化输出,我建议直接设 0.1;做聊天、写作创意内容,设 0.8 左右比较好用。这个参数不用背理论,改一改跑几条文本,感受一下输出差异,记忆比什么教程都牢固。
maxTokens控制单次回复上限,如果你发现答案经常被截断,不要盲目调大,先看是不是提示词太长挤占了输出空间。反之如果业务只需要几行摘要,把maxTokens压到 500,既能提升响应速度,也能降低服务端计算成本。
6.4 生产环境落地前要做的三件事
第一个是加超时和重试。大模型服务有概率抖动,生产环境必须配置合理的 HTTP 超时(比如 60 秒)和重试机制(建议最多重试两次)。第二个是加熔断。当服务连续报错,避免业务线程全部卡在模型调用上,用本地熔断器或服务端限流兜底。第三个是加审计。记录每次调用的请求参数和响应摘要,既方便排查问题,也能统计成本。
这三点没有在入门代码里体现,但只要你准备把 AI 功能上线,就必须补上。我见过一个项目上线第一天因为模型服务限流导致接口雪崩,当时就是少了熔断这道保险,教训非常深刻。
最后分享两个实用技巧
我自己写代码时有个习惯,永远先定义接口、跑通最小闭环,再填充细节。LangChain4j 这套接口抽象也很适合这种开发节奏。第二个技巧:调试提示词时,先用一个公开的聊天页面测试提示词效果,再把稳定后的提示词固化到@UserMessage注解里,能省很多次线上调试的麻烦。
另外,如果你的项目已经用 Spring Boot,可以考虑 LangChain4j 的 Spring Boot Starter,它可以把模型、AiService、聊天记忆全部注册成 Bean,让你的代码更贴近常规 Spring 风格。不管用什么姿势接入,框架的作用只是把路铺平,真正能跑多远,还是取决于你对自己业务场景的理解。这个库我还会继续用,下一步准备往 RAG 方向试,等有成果了再写一篇分享。