搞Java的兄弟们,这两年是不是有点尴尬?身边搞Python的同事天天拿LangChain写智能体、搞RAG,咱Javaer只能眼巴巴看着,想抄作业都没法抄。直到我遇到LangChain4j,这个专为Java生态设计的LLM应用框架,才算是彻底解了馋。这篇文章我用一个真实的入门项目带大家走一遍,从环境搭建到对话、流式输出、Tool Calling、RAG全套摸下来,保证你照着敲就能跑通,咱Java人也能把大模型玩得明明白白。
1. 项目定位与核心价值:为什么Java生态偏偏需要它
1.1 从痛点说起:Java开发者做LLM应用的尴尬
我先说个扎心的事实。现在市面上的LLM开发框架,九成以上都是Python的天下。Python那边有完整的生态,工具链一拉一大把,文档多到看不完。而Java这边呢?不是没有SDK,但大多数都是各家大模型厂商自己做的那套客户端,调个接口还行,真要做Agent、做记忆管理、做RAG(检索增强生成),你就得从零开始拼轮子。
我最早做的一个智能客服项目就吃过这个亏。当时要在现有的Spring Boot系统里接一个大模型接口,本身调用倒是不难,难的是几个硬骨头:
- 多轮对话的记忆管理,得自己维护会话历史数组,每次请求手动拼进去
- 想让模型调用我们内部系统的库存查询接口,得自己写函数调用的JSON Schema解析
- 要把企业内部的文档库喂给模型做问答,没有现成的切分、向量化、检索链路
这些活本身不复杂,但杂、碎、容易出错,尤其一旦系统里跑着几百万流量的核心业务,你绝对不敢用手写拼接的方式上生产。当时我就在想,如果有个框架能把LLM开发里的这些“脏活累活”都抽象掉,只暴露干净的接口给工程师,那就太好了。
1.2 LangChain4j到底解决了什么问题
LangChain4j这个项目诞生的初衷,就是给Java开发者提供一套和Python生态对标的能力。它把LLM应用开发中会遇到的高频场景统统做成了模块化组件,你不需要关心底层的HTTP调用、JSON协议、消息历史的维护逻辑,只要面向统一的抽象接口写业务就行。
它的几个核心设计理念,我实际用下来觉得非常对路:
- 模块化:核心是
langchain4j基础包,然后按功能拆成langchain4j-open-ai、langchain4j-ollama、langchain4j-embeddings等一堆扩展包,你用到哪个就引哪个,不会把一堆用不着的依赖塞进你的Spring Boot工程。 - 接口统一:不管底层接的是哪家大模型,你写的代码几乎是一样的。今天想从A厂切到B厂,只换一个依赖和几行配置就行,业务代码不用动。这一点对国内团队太重要了,因为模型厂商的动向谁说得准。
- Java生态友好:它天生和Spring Boot、Quarkus这些Java框架配合得很好。你可以用
@Bean管理模型对象,可以在Service层里优雅地调用Agent能力,做的工程化方案能直接嵌进现有的企业级系统,而不是为了一个新项目单独开一套Python微服务。
毫不夸张地说,LangChain4j就是当前Java生态里做LLM应用最接近“正规军”的方案,没有之一。对于业务系统以Java为主、又迫切想接入AI能力的团队来说,它几乎是性价比最高的选择。
1.3 你将从这个项目里得到什么
这个入门项目不是简单的“调通一个接口”就完事,我把它设计成了一条完整的“学习——实战——避坑”链路,你跟着走一遍,基本就能摸清LangChain4j的套路:
- 环境准备和Maven依赖怎么配
- 用十几行代码跑通第一个对话Demo
- 搞懂ChatLanguageModel、Message这一组核心抽象到底是干嘛的
- 实现“打字机”一样的流式输出,这个是聊天机器人必备体验
- 用AiServices优雅地让模型按Java对象格式返回结构化数据
- 亲手实现Tool Calling(函数调用),让模型可以真正操作你的Java方法
- 跑通一个极简RAG流程,让模型基于你自己的文档回答问题
每一项我都会给出可直接复制的代码、关键参数的取舍说明,以及我实际踩过的一些坑。有些坑网上文档里根本不会写,但你早晚会遇上。
2. 环境准备与第一个对话Demo:先把水烧开
2.1 兜底配置:JDK和Maven
正式开始之前,先把地基打好。LangChain4j要求JDK 8以上,但我建议你直接用JDK 17。倒不是因为JDK 8跑不起来,而是后面你要用到一些语言特性,以及Spring Boot 3.x的生态,JDK 17会让你省很多事。Maven就用3.8以上版本,这应该不是什么大问题。
为了方便演示,我新建了一个Maven工程,目录结构就是标准的src/main/java。整个项目用到的依赖就三个核心加一个可选:
<properties> <langchain4j.version>1.0.0-beta1</langchain4j.version> </properties> <dependencies> <!-- LangChain4j 核心 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 接入兼容OpenAI协议的API厂商 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 本地模型可选方案 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 日志,建议带上 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.6</version> </dependency> </properties>这里有个版本选择的问题得提醒你。LangChain4j的版本迭代非常快,API变化也比较激进。如果你用的版本和我这里不同,个别方法名可能对不上。遇到这种情况不要慌,去对应版本文档里查一下换个叫法就行。
2.2 模型接入的两种路径
接下来最关键的一步:让LangChain4j能连上大模型。我建议新手走两条路都试一遍,因为它们的侧重点完全不同。
路径一:接在线大模型API
这是最快出效果的方式。现在国内能买到的大模型API,基本都提供OpenAI兼容协议。LangChain4j里专门有个langchain4j-open-ai模块,实际上就是走OpenAI的协议格式,所以只要是兼容OpenAI格式的API,都能用这个模块接。
代码长这样:
ChatLanguageModel model = OpenAiChatModel.builder() .baseUrl("https://你的API服务商地址/v1") .apiKey("你的API Key") .modelName("你选定的模型名称") .temperature(0.7) .logRequests(true) .logResponses(true) .build();这里baseUrl是API服务商给你的访问地址,modelName是你购买的模型标识,temperature控制生成随机性,数值越大回答越发散,做客服类场景建议调低到0.2左右,免得模型太“话痨”。logRequests和logResponses建议开发期保持true,能直接在日志里看到完整的请求和响应体,定位问题很方便,生产环境记得关掉。
路径二:接本地模型(Ollama)
本地模型的好处是免费、数据不出内网、离线可用,适合对数据隐私敏感的团队。我用的Ollama工具,它是目前搭建本地模型服务最简单的方式之一,下载安装后命令行执行ollama run qwen2.5:7b这样一条命令就能拉模型并起服务。
Java这边连接Ollama就更简单了,不需要API Key:
ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .temperature(0.7) .build();注意modelName必须和你Ollama里已经拉取的名字一致。我第一次用的时候不小心MultiModelName写成qwen2.5没带:7b,结果启动直接报模型不存在,排查了半天。这个细节记一下,能省你五分钟。
2.3 十几行代码跑通第一个对话
搞定模型对象之后,跟大模型聊天就非常简单了,核心就一行代码。
public class ChatDemo { public static void main(String[] args) { ChatLanguageModel model = OpenAiChatModel.builder() .baseUrl("https://你的API服务商地址/v1") .apiKey("你的API Key") .modelName("你选定的模型名称") .build(); String answer = model.generate("用一句话解释什么是大语言模型"); System.out.println(answer); } }就这么朴素。model.generate("你的问题")传一个字符串进去,模型就把回答当字符串返回给你。
我让人印象深的是,这个简单的Demo背后,LangChain4j已经把HTTP连接、超时重试、请求格式封装、响应解析全给你处理完了。你在自己项目里写的话,这些代码少说也得两三百行,还不一定写得稳。
跑通这段代码,你就已经完成了从Java工程师到LLM应用开发者的“惊险一跳”。接下来要做的,是理解这背后的组件关系,然后往里面加真正的业务逻辑。
3. 核心抽象拆解:为什么说LangChain4j的设计“干净”
3.1 ChatLanguageModel:所有大模型入口的统一接口
在LangChain4j里,ChatLanguageModel是最高频的接口,可以把它理解成“所有大模型入口的门面”。不管是OpenAI、Ollama还是其他厂商的模型,对外表现都是ChatLanguageModel这个接口,提供了多个generate重载方法。
这个设计思路非常像JDBC——Java操作数据库时,不管底下是MySQL还是PostgreSQL,你都通过Connection、Statement这些标准接口操作,换数据库只换驱动。LangChain4j把同一套哲学搬到了LLM领域。你写的Service层代码面向ChatLanguageModel编程,将来从一家模型厂商切到另一家,只要换@Bean里配置的那个实现类就行,业务代码纹丝不动。
除了generate(String)这个最简单的方法,它还有接收ChatMessage列表的方法,这才是多轮对话的正确打开方式:
List<ChatMessage> messages = new ArrayList<>(); messages.add(SystemMessage.from("你是一个专业的Java技术顾问。")); messages.add(UserMessage.from("帮我推荐一个Java的ORM框架")); messages.add(AiMessage.from("如果你需要轻量级,可以考虑Jdbi;如果追求生态完整,MyBatis和Spring Data JPA都可以。")); messages.add(UserMessage.from("那MyBatis和Spring Data JPA我该怎么选?")); ChatResponse response = model.generate(messages);看到这里,你应该嗅到了关键点:LangChain4j并不是让你把记忆当成字符串拼来拼去,而是用结构化的消息对象维护上下文。
3.2 Message体系:系统、用户、AI、工具四类消息的角色分工
ChatMessage是一个顶层接口,下面有四个实现类,对应LLM世界里四类基础消息。我列个表,你一眼就能看明白:
| 消息类型 | 类名 | 作用 |
|---|---|---|
| 系统消息 | SystemMessage | 给模型设定角色行为规则,比如“你是客服”“你只回答技术问题” |
| 用户消息 | UserMessage | 用户输入的问题或指令 |
| 模型应答 | AiMessage | 模型的回复,在多轮中作为历史上下文回传 |
| 工具消息 | ToolMessage | 工具调用后的结果回传(做Tool Calling时会用到) |
日常开发中,90%的场景就是这三种消息的组合:开头放一条系统消息设定人设,后面交错放用户消息和模型应答。特别要注意的是,SystemMessage虽然在API调用中只是一条普通的消息记录,但它对模型的行为约束能力非常强。你完全不prompt它,模型也能答,但加上一句角色定义之后,回答质量会有肉眼可见的区别。我做过对比,同样问“给我一个Java单例模式的写法”,不加系统消息时模型可能给了五六个变体让人眼花缭乱,加了“你是一位精通Java并发编程的专家,回答要简洁、给出代码和说明”之后,输出直接干净利落,这属于免费的工程优化。
3.3 AiServices:不再手工拼消息,让框架管记忆
手工管理ChatMessage列表在小Demo里没啥问题,可一旦业务复杂起来,你就要开始处理会话过期、消息数量超限、历史裁剪……这时候再用List就非常痛苦。
LangChain4j为此提供了AiServices这个高层的门面。它允许你做一个接口,然后把模型能力直接绑定到方法上。我举个例子你感受一下:
interface Assistant { String chat(String userMessage); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); String answer = assistant.chat("李白是哪位诗人?");你没有看错,你只管定义一个Java接口,不需要写实现类,AiServices会在底层自动生成一个动态代理,把.chat()方法调用接住,自动从chatMemory里取出历史消息,和本次用户输入拼成完整的ChatMessage列表,发给模型,再把模型回答存回记忆里。
这里MessageWindowChatMemory.withMaxMessages(20)的含义是:最多保留最近20条消息。超过20条,最老的自动被丢掉。这是防止Token爆掉的一种朴素但实用的策略,适合大多数轻量级对话场景。
我强烈建议你从第一步Demo之后就直接切到AiServices,因为它才是写实际业务代码的正确姿势。
4. 进阶实操:流式输出、结构化输出与Tool Calling
4.1 流式输出:别再让用户干等那几秒
如果你做的只是内部的命令行小工具,同步等待模型返回没问题。但只要是面向用户的界面,全等返回再渲染的体验只能用“灾难”形容——一段长回答往往要等5秒甚至更久才一次性冒出来,用户早就以为系统挂了。
解决方案是流式输出,也就是让模型生成一个字就推一个字过来,前端像打字机一样把字逐个打出来。LangChain4j里对应的接口叫StreamingChatLanguageModel:
StreamingChatLanguageModel streamingModel = OpenAiStreamingChatModel.builder() .baseUrl("https://你的API服务商地址/v1") .apiKey("你的API Key") .modelName("你选定的模型名称") .build(); streamingModel.generate("请写一篇关于Java内存模型的短文", new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { System.out.print(token); // 收到增量token,立即输出 } @Override public void onComplete(ChatResponse response) { System.out.println("\n完成"); } @Override public void onError(Throwable error) { error.printStackTrace(); } });onNext方法就是流式推送的核心回调。注意这里拿到的token是一个个片段,可能是一个词,一个汉字,甚至半个词,你只管把它原样追加到页面上就行。如果你用的是Spring Boot做Web应用,想把这套能力暴露给前端的话,标准做法是用Spring WebFlux或SSE(Server-Sent Events),把onNext里的token通过SSE通道实时推给浏览器。
我不建议在这里直接上WebSocket,SSE对“服务端单向推送文本”这个场景的契合度是最高的,实现也简单很多。
4.2 结构化输出:让模型把JSON“吐”成Java对象
大模型问答最讨厌的一个场景是:你跟它说“帮我解析一下这封邮件里的订单号、金额、客户名称”,它倒是乖乖回答了,但回答格式可能是散文,可能是乱码JSON,也可能是一个带markdown代码块标记的JSON。
为了让它稳定输出结构化数据,LangChain4j提供了一套非常优雅的方案:你定义一个Java类,再定义一个接口方法标注返回值,模型就会按照你的Java类型约束输出。看代码:
record Person(String name, int age, String city) { } interface PersonExtractor { @SystemMessage("你是一个信息提取助手,请从用户输入中提取字段并返回。") Person extractPerson(String text); } PersonExtractor extractor = AiServices.create(PersonExtractor.class, model); Person person = extractor.extractPerson("张三今年28岁,住在杭州"); // 输出:Person[name=张三, age=28, city=杭州]我头一回用这个功能的时候整个人是震撼的。它背后的原理是:LangChain4j把你定义的Person类型(record或class)转换成JSON Schema,放到prompt里让模型生成对应结构的JSON,然后再用Jackson把JSON反序列化回Java对象。整个过程无缝衔接,你完全不用手工解析。
这个方法简直是为信息抽取、表单自动填充、数据清洗等场景量身定做的。比起让模型“输出JSON然后你解析”,这个方案能把解析失败率压到极低。毕竟模型不需要猜你要什么格式,Schema已经给它了。
4.3 Tool Calling:让模型调用你的Java方法
如果说结构化输出是让人舒服,那Tool Calling就是整个LangChain4j里最让我觉得“值回票价”的功能。简单说,它能让模型自己决定“我需要调用某个Java方法”,然后框架帮你把参数传进去,执行完方法,再把结果回传给模型。
我项目里一个真实的例子:让大模型帮用户查询订单状态。模型本身不知道订单数据,但它可以调用查询方法。定义工具非常简单,就在接口里加一个带@Tool注解的default方法(或用@Tool标记的实现方法):
interface OrderAssistant { String chat(String userMessage); @Tool("根据订单ID查询订单状态") default String queryOrderStatus(@ToolParam("订单ID") String orderId) { // 这里实际去查数据库 return "订单" + orderId + "当前状态为:已发货,预计明天送达"; } }然后构建时把它构建进去:
OrderAssistant assistant = AiServices.builder(OrderAssistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .tools(new OrderTool()) // 如果工具逻辑在另一个类,也可以传入工具对象 .build();之后用户发一句“查一下订单12345678的物流”,模型的回复就不是空话,而是真的调用queryOrderStatus("12345678"),拿到结果之后,再基于这个结果组织语言回复用户。
这个能力背后的机制是迭代式的多轮调用:模型先返回一个“我要调用queryOrderStatus工具”的意图,里面带着参数,框架把ToolMessage回传给模型,再让模型基于工具结果生成最终回复。
用的时候我建议你务必要注意这几点:
@Tool的描述文字要写清楚“这个方法是干什么的”,因为模型是看着这段描述决定要不要调用的。描述越具体,模型调用越准。你写成“查询订单状态”和“根据订单ID查询订单状态,返回当前物流节点和预计送达时间”,后者被正确调用的概率高得多。- 参数用
@ToolParam注明含义,因为模型需要通过语义理解去生成参数值。 - 工具方法不要抛非受检异常,建议捕住并返回一个错误描述字符串。因为模型可能会根据“查询失败”的返回继续追问或转人工,总比你日志里打了个异常栈、用户界面却卡死了强。
4.4 提示词模板:告别字符串拼接
写业务系统的时候,谁都会碰到“prompt里有动态变化的部分”的场景。没见过世面的做法是字符串拼接,见过世面的做法是用模板。
LangChain4j内置了PromptTemplate,使用方式和MyBatis的SQL语句有点像。如果你在SQL里写过<where><if>,你会发现这套方式异常亲切:
PromptTemplate template = PromptTemplate.from(""" 你是{{companyName}}的客服助手。 用户问题是:{{question}} 请用不超过{{wordLimit}}个字回答。 """); Map<String, Object> variables = new HashMap<>(); variables.put("companyName", "某某科技"); variables.put("question", "你们的退款政策是什么"); variables.put("wordLimit", 50); Prompt prompt = template.apply(variables); String response = model.generate(prompt.text());模板变量用双大括号括起来,apply方法传入一个变量Map就行。把提示词从代码里抽离出来放到配置文件或数据库,是很适合团队协作的习惯——运营同学能直接调话术,不用提代码。
5. 再往深走一步:极简RAG流程
5.1 RAG到底解决什么问题
大模型训练数据有截止日期,你公司的内部制度、产品文档、特殊业务知识,模型一概不知道。RAG(检索增强生成)的思路是:用户提问时,先从你自己的知识库里检索出相关的片段,拼到prompt里给模型参考,让模型基于这些资料回答。
LangChain4j对RAG的支持是组合式的,把下面这条链路串起来就行:
加载文档 -> 切分 -> 向量化(Embedding) -> 存入向量库 -> 检索 -> 拼装prompt -> 生成回答我建议你第一次做RAG时不要一上来就追求复杂架构,先用最简单的“内存向量存储”跑通流程,理解每一环在干什么,再考虑换真正的向量数据库。
5.2 快速跑通一个内存版RAG
这里我用一个简化的例子,演示怎么把一段关于产品介绍的文本变成可检索的知识。
首先准备一个分割器和嵌入模型。LangChain4j通过TextSegment来表示文本片段,通过EmbeddingModel来做文本向量化:
// 1. 加载文档(这里直接用字符串模拟) String content = "某某智能音箱支持语音控制、蓝牙连接、定时提醒,电池续航12小时..."; // 2. 切分成片段(通常按字数设置窗口,带重叠) DocumentSplitter splitter = DocumentSplitters.recursive(300, 30); List<TextSegment> segments = splitter.split(content);然后是向量化和入库:
EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .baseUrl("https://你的API服务商地址/v1") .apiKey("你的API Key") .modelName("你选定的Embedding模型") .build(); EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); for (TextSegment segment : segments) { Embedding embedding = embeddingModel.embed(segment).content(); store.add(embedding, segment); }到了检索环节,LangChain4j提供了一个特别实用的EmbeddingStoreContentRetriever,它帮你把“向量查询”“相似度排序”“取Top K片段”这些操作全部封装好:
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) // 检索时返回几个最相关片段 .minScore(0.6) // 相似度低于0.6的片段直接丢弃 .build();最后把它接进AiServices,整个RAG链路就通了:
Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String answer = assistant.chat("这个音箱支持蓝牙吗?"); // 模型会先检索相关文本片段,再基于片段内容回答minScore这个阈值是我比较看重的参数。它的单位是相似度分数,不同Embedding模型产出的分数范围不太一样。你在测试阶段可以先设低一点比如0.5,看看检索出来的片段相关内容对不对,然后逐步调高;如果设太高,可能什么都检索不到,模型只能无奈地说“我不知道”。
5.3 RAG落地前必须要想清楚的坑
- 小文档用内存向量库没问题,但企业级知识库规模上了几十万片段,就得换真正能持久化的向量数据库了。LangChain4j有对应的模块,接入方式和上面几乎一样,替换实现类即可。
- 切分大小很重要。切得太碎,语义不完整;切得太大,token开销高、检索精度下降。我自己的经验是,200~500个字符一个片段,重叠30~50字符,属于比较稳妥的起点。
- Embedding和对话模型是两个独立的模型配置,不要误以为同一个模型对象可以干两件事。对话用
ChatLanguageModel,向量化用EmbeddingModel,它们是两条独立的线。
6. 新手必看的常见问题与排查实录
6.1 常见报错速查表
这组内容全部是我自己和身边同事实际踩过的坑,整理成表给你:
| 现象 | 大概率原因 | 解决思路 |
|---|---|---|
| 调用时报401认证失败 | API Key配置错误或未授权该模型 | 检查apiKey是否复制完整,检查模型名是否有该模型访问权限 |
| 报404 Not Found | baseUrl或modelName配错 | 确认服务商地址格式(一般以/v1结尾),确认模型名准确 |
| 报模型不存在Model Not Exist | Ollama未拉取对应模型 | 先执行ollama list,确保本地模型Tag完整匹配 |
| 返回内容乱码或截断 | 上下文长度超限、编码问题 | 调低maxMessages,减少历史消息数量;确保服务端返回UTF-8 |
| Tool Calling一直没触发 | 模型本身不支持工具调用,或工具描述太模糊 | 换成支持Tool Calling的模型,优化@Tool描述文案 |
| 结构化输出报反序列化错误 | 模型生成的JSON和Java类型不匹配,或字段命名不一致 | 给record字段加清晰说明,必要时用@Description注解字段含义 |
6.2 我最想单独拎出来说的三个坑
坑一:日志里的明文Key泄漏问题
开发环境为了排查问题,我开了logRequests(true),结果有次顺手把日志贴到群里,API Key全场可见。这要是在生产环境后果不堪设想。配置项本身没问题,我的建议是:开发期开、生产期关;如果真的要定位生产问题,优先用网关注入Header或者临时开30秒再关掉。
坑二:上下文长度爆掉,对话“失忆”
我用MessageWindowChatMemory.withMaxMessages(20)的时候,以为20条消息肯定够了,结果实战中发现:20条消息在Token层面可能相当于两千个Token,如果模型上下文窗口只有8K(比如一些轻量模型),一旦用户上传长文档或连续追问,老消息还是会被丢掉——但如果你用的是另一个窗口更大的模型,这个数量可以放宽。关键点是:不要只按“条数”估算,要按“Token”估算。主流做法是给每轮对话估算Token消耗,用窗口大小除以每轮平均消耗得出一个安全的保留条数。
坑三:改了代码还是走旧逻辑
LangChain4j的Builder模式有个特点,对象构建完就不可变了。你如果调整了temperature参数,没重启进程、只把新配置写在main方法里跑了个新实例,那没问题;但如果你在Spring Boot里用了单例Bean,改了配置却没重启,尝试效果还是旧的。改配置后务必确认Bean确实重新创建了。
6.3 我的排查方法论
做LLM应用的调试,和传统Java调试有点不一样。传统问题我可以断点、一步步看变量,LLM应用的问题很多时候是“提示词不对”或“模型这个场景下就是蠢”,很难静态分析。
我的排查顺序固定是这样:
- 先看日志。开启
logRequests和logResponses,看清发给模型的原始prompt长什么样、模型返回的原始响应长什么样。这一步能过滤掉70%的问题。 - 复现简化。把业务逻辑剥掉,用最少的代码直接调
ChatLanguageModel,如果简单代码能复现,说明问题在模型或配置;如果不能复现,说明问题在你的业务封装层。 - 查模型能力边界。有些模型不支持Tool Calling、有些Embedding模型不支持中文、有些长文本模型在长对话后性能下降明显。遇到怪问题,先确认是不是模型本身的能力问题。
- 查版本差异。LangChain4j更新频繁,如果代码和网上教程对不上,很可能版本变了。优先看你自己依赖的版本的文档或源码。
7. 结尾:说点个人心得
我在把LangChain4j引入到实际项目之前,其实犹豫过很久。Java生态做AI确实没有Python那边热闹,社区资料也少,遇到问题主要靠翻源码。但真正用了几个月之后,我反而觉得LangChain4j这种“先标准化、再扩展”的思路特别对Java工程师的胃口——你不必成为一个提示词工程大师,也能用干净的Java代码把LLM嵌进企业系统里。
最后再分享一个实用小技巧:新手阶段不要一上来就追新版本,选一个稳定的发布版,把核心接口源码下载下来,花一晚上把ChatLanguageModel、AiServices、ChatMemory这几个关键类的源码读一遍。很多网上搜不到答案的疑难杂症,看一遍源码心里就有底了。搞懂抽象背后的设计意图,比背会一百个API示例都管用。
祝各位Java同仁玩得开心,早日把大模型真正用进自己的业务里。