资讯动态

LangChain4j:Java开发者的大模型应用开发框架实战入门

发布时间:2026/10/20 6:21:59 来源:尧图企业网站定制
能看到这个标题说明你多半已经受够了在Java项目里手写大模型调用的零碎代码。LangChain4j这个名字就是Java生态对LangChain那套思路的回应给JVM开发者一套顺手、完整、不啰嗦的LLM应用开发框架。它解决的核心问题是让Java工程师不用从HTTP请求、JSON解析、上下文拼接这些脏活开始而是把大模型接入、对话管理、工具调用、检索增强这些能力像搭积木一样组合到业务系统里。如果你正准备在公司项目里引入AI能力或者只是想在自己的Demo里快速跑通一个带记忆、能联网、能查文档的聊天机器人这篇文章就是你需要的入门路线图。我会从依赖引入讲到RAG全程带着代码也会把踩过的坑一并列出来。1. 为什么Java项目需要LangChain4j1.1 直接调HTTP接口的痛点我见过不少团队的第一版AI功能就是拿RestTemplate或者OkHttp去调模型厂商的HTTP接口。看起来也不难拼JSON、发请求、解析返回、把历史消息塞进数组再发一次。但一旦功能多起来就变得很难受。你自己维护多轮对话时要处理截断策略是保留最近十条消息还是按字符数截断还是按Token数截断。要处理系统提示词的注入方式要处理模型返回格式不稳定时的重试。更棘手的是当你要接入多个模型供应商时每家接口的消息格式、参数命名都有一点差异你的业务代码里会写出大量if-else来适配。这时候一个统一的抽象层就非常有用。LangChain4j把所有模型提供商都收敛到同一个ChatLanguageModel接口后面。业务代码只和这个接口打交道至于背后是某个云厂商API还是本地跑的一个模型完全不关心。切换供应商时只需要换一行工厂代码其余逻辑基本不用动。我自己做过一个模拟项目X需要对接多种模型做效果对比就是靠这个统一接口省下了大量时间。如果当时继续写原生HTTP调用光是参数转换就能写几百行胶水代码。1.2 LangChain4j和LangChain不是一回事很多从Python转到Java的开发者会问一个问题LangChain4j是LangChain的Java移植版吗从设计理念上确实有继承关系但它不是简单翻译。LangChain4j更注重Java生态的既有习惯大量使用接口、Builder、注解这些Java开发者熟悉的东西而不是套用Python那种偏动态的写法。举个例子在LangChain里你想让模型调用一个自定义函数需要注册工具、写描述结构、手动解析模型返回的函数调用参数。在LangChain4j里你只需要写一个带Tool注解的方法框架自动帮你完成描述生成和参数解析。这种体验非常Java也很舒服。LangChain4j还特别照顾了几种常见场景对话记忆不要求你去维护消息数组框架提供了ChatMemory接口和现成的窗口记忆实现文档问答有完整的嵌入选型、存储选型、检索链路流式输出通过StreamingChatLanguageModel抽象让你不用关心不同供应商在流式接口上的差异。对Java开发者来说LangChain4j的意义不是让你非要学一套新范式而是把你本来就要做的那些事封装成了符合Java习惯的API。2. 快速跑通第一段对话2.1 依赖引入与模型选择我以LangChain4j 1.x版本为例写这段内容。引入依赖很简单核心包一定要加然后按你选的模型服务商加一个扩展包。// 核心核心包这是必须的 implementation dev.langchain4j:langchain4j:1.0.0 // 假设你连的是Ollama本地模型 implementation dev.langchain4j:langchain4j-ollama:1.0.0 // 假设你连的是兼容OpenAI协议的服务就用这个 implementation dev.langchain4j:langchain4j-open-ai:1.0.0我现在最推荐入门阶段先用Ollama跑本地模型。原因很简单不涉及API密钥、不会有账单、断网也能联调而且latency可控非常适合先跑通逻辑。你装好Ollama之后拉一个模型例如qwen2.5:7b就可以开始写代码了。如果你要用某个云厂商的兼容OpenAI接口实际上很多家都提供这种兼容层。你可以配置baseUrl指向服务地址填入对应的密钥。这里有个关键点langchain4j-open-ai扩展包本身就是为兼容OpenAI协议设计的所以遇到任何自称兼容OpenAI协议的服务用小改一下baseUrl的方式就能连上去。2.2 一个最小的代码示例这块代码是整个框架最常见的入口。你用Builder创建一个ChatLanguageModel然后就可以开始聊天了。import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .build(); String answer model.generate(用一句话解释什么是事务); System.out.println(answer);如果走langchain4j-open-ai这条路代码长这样import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; ChatLanguageModel model OpenAiChatModel.builder() .baseUrl(https://你的模型服务地址) .apiKey(你的密钥) .modelName(模型名) .build();这里有个小技巧我实际开发中一直用把.apiKey()改成从环境变量读取不要在代码里写死。你可以这样写.apiKey(System.getenv(LLM_API_KEY))这样做的好处是代码提交到仓库里不会把密钥带走部署时也能按环境配置不同的密钥。别问我怎么知道的我见过把密钥写死在配置文件里整个仓库被脱裤的惨剧。2.3 用消息对象代替字符串拼接model.generate(String)这个接口适合验证环境真正开发时不建议这么用。因为它没有办法区分系统提示词和用户输入。你应该养成从一开始就使用消息对象的好习惯。import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.data.message.SystemMessage; ListChatMessage messages new ArrayList(); messages.add(new SystemMessage(你是一名Java架构师回答要简洁且专业。)); messages.add(new UserMessage(请问Java 21的虚拟线程适合用在哪些场景)); ChatResponse response model.generate(messages); System.out.println(response.aiMessage().text());SystemMessage负责设定人设和约束UserMessage是用户输入将来还会有AiMessage代表模型的历史回复。这种区分的价值在于当你做多轮对话时上下文就是这些消息的有序列表。框架会正确地把它们转换成各个模型服务需要的格式你不需要自己拼。我见过有人写死一个超大字符串来塞系统提示词看起来方便实际上改需求时非常痛苦。用消息对象后你可以把系统提示词拆成多个SystemMessage还可以在对话过程中动态追加灵活得多。3. 对话记忆和流式输出3.1 手动管理上下文为什么不可行你很快就需要做多轮对话。最简单粗暴的做法是把每一轮消息都存到一个List里然后完整地发给模型。ListChatMessage history new ArrayList(); history.add(new SystemMessage(你是AI助手)); history.add(new UserMessage(今天天气如何)); history.add(new AiMessage(目前没有天气查询功能)); history.add(new UserMessage(那你能做什么));这样做的缺点很明显上下文越来越长很快就撞上模型的Token上限。而且你不截断每次请求的延迟和费用都在涨。更麻烦的是如果用户的输入很短但历史消息很多模型反而容易被无关内容干扰。很多人一开始会觉得“我用一个List就能搞定”直到上线后遇到超长对话才回来改。其实LangChain4j提供了现成的轮子你不需要自己造。3.2 使用ChatMemory管理上下文ChatMemory是一个接口最常用的实现是MessageWindowChatMemory。它内部维护一个固定大小的消息窗口窗口满了就丢弃最旧的消息。import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; ChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build(); chatMemory.add(new UserMessage(我想订一张明天去北京的机票)); chatMemory.add(new AiMessage(好的我需要知道你的出发城市和期望时间。)); chatMemory.add(new UserMessage(从上海出发早上十点左右)); ChatResponse response model.generate(chatMemory.messages());maxMessages(20)的意思是最多保留20条消息也就是大约10轮对话。你认真想想这个数字背后的逻辑不是越大越好太大的窗口会稀释模型对最近内容的注意力还让每次请求的Token数直线上升。在实际项目里我通常建议根据你的业务场景和模型上下文长度来配置8到20条是一个比较稳的区间。ChatMemory的另外两个常见实现是TokenWindowChatMemory和TransientChatMemory。前者按Token数作为截断基准后者适合一次性会话场景。你自己实现一个ChatMemory接口也不难比如把历史消息存到Redis里这样多实例部署时用户的上下文就不会丢。3.3 流式输出你迟早要学聊天机器人如果是打字机效果用户会觉得对话更接近真人。这个体验差异很大我强烈建议做聊天类产品时直接用流式。LangChain4j把流式抽象成了StreamingChatLanguageModel和StreamingResponseHandler。import dev.langchain4j.model.StreamingResponseHandler; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.output.Response; StreamingChatLanguageModel streamingModel OllamaStreamingChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .build(); streamingModel.generate(给我写一首关于夏天的诗, new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { System.out.print(token); } Override public void onComplete(ResponseAiMessage response) { System.out.println(\n生成完成); } Override public void onError(Throwable error) { error.printStackTrace(); } });onNext会在每个Token到达时被调用。在Web项目里你可以把token通过WebSocket或SSE推给前端用户就能看到逐字生成的效果。注意一点流式模式下你已经拿不到一次性完整返回的对象了所以业务逻辑要围绕数据流来设计。比如需要把生成的完整内容存库时你需要自行拼装onNext里的内容或者把拼装好的字符串放在onComplete里统一处理。有一点要提醒如果某个环节是异步流式的链路里的异常处理不能偷懒。我见过在onError里只打日志不通知用户的实现结果用户那边只见对话停住没有报错体验很差。建议onError里至少把错误状态推送给调用方。4. AiServices让大模型变成可定义的接口4.1 把模型调用包装成业务方法如果只停留在model.generate()这个层面LangChain4j相比裸调HTTP接口的优势还不算大。真正让它进入实用层面的是AiServices这个特性。你定义一个接口框架帮你生成实现。你只管写业务规则底下的模型调用、上下文注入、工具分配全部由框架处理。import dev.langchain4j.service.AiServices; interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build(); String answer assistant.chat(你好介绍一下你自己);AiServices.builder()会基于接口生成一个动态代理实例。你每次调用assistant.chat()框架自动把消息放进ChatMemory带着历史调模型把结果返回还会更新记忆。你完全不需要关心消息列表是从哪来的。这种接口方式的想象力很大。你可以在接口方法上添加SystemMessage注解来指定系统提示词比如interface CustomerSupport { SystemMessage(你是一个耐心的客服回答问题时不能超过50个字) String answer(String question); }也可以让方法接收多个参数框架会帮你把它们拼进用户消息。甚至可以在参数上使用MemoryId注解为不同用户分别维护对话记忆。这在真实的SaaS系统里几乎是刚需。4.2 用AiServices做流式输出更轻松AiServices对流式的支持同样自然。你定义一个返回TokenStream的接口方法之后用链式调用处理结果。import dev.langchain4j.service.TokenStream; interface StreamingAssistant { TokenStream chat(String userMessage); } StreamingAssistant assistant AiServices.builder(StreamingAssistant.class) .streamingChatLanguageModel(streamingModel) .chatMemory(chatMemory) .build(); assistant.chat(讲一个程序员冷笑话) .onNext(token - System.out.print(token)) .onComplete(response - System.out.println(\n完成)) .onError(error - error.printStackTrace()) .start();注意这里返回的是TokenStream调用.start()才会真正发起请求。这种设计让你可以先装配各种回调再触发执行。在Web场景里尤其友好你可以从容地把回调接到SSE事件流的各个生命周期上。4.3 工具调用把模型的请求变成代码执行工具调用是当前大模型应用里最有实战价值的能力。在很多场景里模型本身不知道实时数据、没有权限执行操作但它可以在回答时告诉你“我需要调用某个函数”。LangChain4j用Tool注解把这件事做得很简单。import dev.langchain4j.agent.tool.Tool; class OrderTools { Tool(根据订单号查询订单当前状态) public String queryOrderStatus(String orderId) { // 这里可以查数据库也可以调别的服务 return 订单 orderId 已于昨天发货预计明天送达; } }然后把你写好的工具传给AiServicesAssistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderTools()) .build(); String answer assistant.chat(帮我查一下订单20240701到哪了);框架内部的工作流程是这样的先把你所有Tool注解的描述和参数结构发给模型模型根据对话内容判断要调哪个工具把参数传给工具方法拿到返回结果后再让模型基于这个结果组织最终回答。你看到的结果是模型的自然语言回复但它背后真的执行了一段Java代码。工具调用有两点值得注意。第一工具方法的描述文本很关键模型会依据描述决定要不要调用介绍得含糊会被跳过。第二工具方法返回字符串是给模型看的不是给用户看的所以你可以放心地返回“用户等级为VIP3”模型自然会组织成合适的口语化回答。我用这个能力做过一个查询系统模型可以根据用户提问自动选择查询接口。以前需要用意图识别算法实现的功能现在一个注解就搞定了少写了大量分支逻辑。5. 检索增强生成让模型知道你的私有文档5.1 RAG最小链路先解释一下RAG检索增强生成Retrieval-Augmented Generation就是让模型在回答你的问题时先去一个向量数据库里找出和问题相关的文档片段把这些片段作为参考上下文一起发给模型。为什么需要它因为模型训练时的知识是有截止日期的而且它不知道你们公司的内部资料。你不可能为了一个内部产品功能去重新训练一次模型但你可以让模型在回答时查一下你准备好的资料。LangChain4j的RAG链路主要由三部分组成文档加载、向量化存储、检索问答。下面这个例子用的是内存型向量存储适合理解和测试。import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel; EmbeddingModel embeddingModel OllamaEmbeddingModel.builder() .baseUrl(http://localhost:11434) .modelName(nomic-embed-text) .build(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); // 1. 加载并分块 Document document Document.from(LangChain4j是一个帮助Java开发者构建LLM应用的工具库……); ListTextSegment segments DocumentSplitters.recursive(500, 50).split(document); // 2. 向量化并存入 for (TextSegment segment : segments) { Embedding embedding embeddingModel.embed(segment.text()).content(); embeddingStore.add(embedding, segment); } // 3. 检索 Embedding queryEmbedding embeddingModel.embed(LangChain4j是什么?).content(); ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant(queryEmbedding, 3); for (EmbeddingMatchTextSegment match : matches) { System.out.println(匹配分数: match.score()); System.out.println(片段内容: match.embedded().text()); }在这个流程里要注意分块不是随便拿字符切。DocumentSplitters.recursive(500, 50)的意思是每块大约500个字符分块之间保留50个字符的重叠。重叠的目的是避免切片切开一个语义完整的句子导致两个相邻片段内容都不完整。匹配分数一般是一个0到1之间的数值越接近1说明越相关。不同向量模型和存储实现的分数口径不完全一样所以不要硬套一个固定阈值。5.2 向量存储选型InMemoryEmbeddingStore只适合本地开发。一旦数据量上千或者你需要多实例共享文档库就必须上真正的向量数据库。LangChain4j对很多向量数据库都有集成我用过的可以分成这么几类存储方案适合场景注意点InMemory本地调试、数据量小进程重启数据丢失PgVector项目已经有PostgreSQL不必额外引入新中间件Redis已有Redis集群、需要低延迟需要额外配置向量索引Milvus数据量大、需要高并发部署运维成本高一些我自己的建议是如果你的系统已经用了PostgreSQL直接上PgVector是最省心的。你不需要再运维一套新的数据库只是建一张带向量字段的表业务成本和故障域都最小。5.3 用AiServices和RAG结合做文档问答前面的检索示例只是演示了检索过程。想让模型真正基于检索结果回答需要把检索结果接进AiServices。LangChain4j提供了一种声明式的写法在你的接口方法上加一个Retrieval相关注解不同版本注解名可能略有差异框架会自动完成检索和注入。import dev.langchain4j.service.AiServices; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; ContentRetriever contentRetriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .build(); interface DocumentAssistant { String answer(String question); } DocumentAssistant assistant AiServices.builder(DocumentAssistant.class) .chatLanguageModel(chatModel) .contentRetriever(contentRetriever) .build(); String answer assistant.answer(LangChain4j支持哪些向量存储);maxResults(3)代表最多取三条相关片段注入提示词。这里有一个很实用的调整思路不要盲目调大maxResults片段越多模型被无关内容干扰的概率也越高同时Token花费越大。我通常从3开始效果不好才往上加很少超过5。RAG真正意义上的难点不在框架而在于数据质量。如果你喂进去的文档本身逻辑混乱、信息碎片化再好的检索算法也召回不到好内容。所以做RAG项目时我会先花时间整理文档去掉页眉页脚、表格的表头表尾、无意义的重复段落再考虑调参。6. 常见问题与排查实录6.1 依赖冲突与版本不匹配Java生态最常见的问题就是依赖冲突。LangChain4j依赖了一些常见库比如Jackson、SLF4J。如果项目里已经有这些库的老版本可能出现NoClassDefFoundError或者奇怪的序列化异常。我的排查套路是三步走。第一步用./gradlew dependencies或者Maven的dependency:tree查看完整依赖树确认冲突点。第二步把LangChain4j相关依赖的版本提级到和项目一致或者反过来统一到框架要求的版本。第三步如果实在纠缠不清直接把冲突的传递依赖在配置里排除掉显式引入你确认没问题的版本。要注意的是不同LangChain4j扩展包之间要保持版本一致。你用了langchain4j-ollama:1.0.0那就不要混一个langchain4j-open-ai:0.36.0进来跨版本混用很容易出诡异问题。6.2 模型输出带着一堆Unicode转义用某些模型服务时你会遇到返回的JSON字符串里中文全部变成\uXXXX形式直接展示给用户是没法看的。这不是LangChain4j的问题是模型服务把字符串做了Unicode转义传输。先用一个简单的System.out.println打印返回结果确认是这个现象。处理方式很直接在返回字符串上调用org.apache.commons.text.StringEscapeUtils.unescapeJava()或者自己写一个正则替换\\uXXXX的小函数。如果只是测试环境出现也可以直接忽略因为很多服务在正式生产环境会自动关闭转义。6.3 上下文超长被模型拒绝当你调大maxMessages或者传入很长的文档时可能遇到模型上下文超限的报错。不同模型的上下文长度差别很大有的8K有的128KMessageWindowChatMemory按消息条数截断并不解决“单条消息过长”的问题。这时候我的建议是用TokenWindowChatMemory按Token数控制上下文长度。此外你还可以在构造模型时设置maxTokens参数对生成部分的长度做限制。比如你只是需要简短回复把maxTokens设为256能在一定程度上避免生成阶段超限。MessageWindowChatMemory 换成: TokenWindowChatMemory.builder() .maxTokens(3000, tokenizer) .build();tokenizer需要根据模型类型选一个对应的分词器实现否则Token数统计不准截断策略就会失效。6.4 工具调用进入死循环工具调用偶尔会出现模型反复调用同一个工具、不返回最终答案的情况。最常见的原因是工具返回的结果里有模型无法理解的信息或者返回内容是空字符串。我在工具方法里都会加一个兜底如果查不到数据明确返回未查询到相关信息而不是返回空字符串或null。这个看起来不起眼的细节能大幅减少模型的反复试探。另外可以在AiServices配置里设置最大调用轮数防止异常场景下无限循环消耗Token。LangChain4j的有些版本直接支持maxIterations配置没有的话也可以自己在上层加一个计数器拦截。6.5 并发场景下的记忆串线上线后最容易踩的坑是多用户同时对话聊天记忆互相串了。如果你在全局只维护了一个ChatMemory实例所有用户的消息都会往里塞A用户的问题会被B用户的历史消息污染这在客服系统里是事故级的Bug。解决办法是用MemoryId注解把记忆按用户隔离。你在接口方法参数上标注interface CustomerSupport { String chat(MemoryId String userId, String userMessage); }框架会为每个userId维护独立的ChatMemory。用这个方案后我再没遇到过串线的投诉。7. 最后分享一点实战心得我从零开始摸LangChain4j到现在最大的感受是这个框架的设计者很清楚Java开发者需要什么。你不需要理解LangChain那些复杂的概念只要会Java照着接口签名写就能出活。不过有一点我自己也还在坚持就是不要什么东西都一股脑往AiServices里塞。有些简单场景比如一次性生成摘要直接用model.generate()就够了只有涉及到多轮、工具、RAG组合的时候才上AiServices这样代码反而更容易理解。还有一个小建议给你的接口方法、Tool方法、RAG检索器都取一个业务含义明确的名字不要图省事写成a()、b()。这些定义在Java代码里是方法但对模型来说就是上下文的一部分。名字越清晰模型越容易做出正确判断。别小看这种细节我见过把工具方法命名为fun1的项目模型召回率差得让人怀疑人生。如果你打算把LangChain4j用到生产环境我多啰嗦一句先小范围验证单机跑通一个带ChatMemory和AiServices的Demo确认依赖、模型、记忆都没有问题再考虑引入RAG和向量数据库。一步步来比一上来就铺开一大套架构稳妥得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑