
做 Java 后端久了看到这两年大模型应用开发几乎被 Python 生态“包圆”心里多多少少有点不是滋味。团队里最常用的技术栈明明是 Java想接个大模型能力却总要先搭一套 Python 服务维护成本直接翻倍。LangChain4j 就是为打破这个局面而生的 Java 版大模型应用开发框架它把模型对话、多轮记忆、工具调用、知识库检索这些能力抽象成了 Java 开发者熟悉的接口。这篇教程会从一个最小可运行的项目讲起逐步带你实现带记忆的助手、会调用外部工具的业务代理以及一个最简知识库问答功能。适合刚想用 Java 接入大模型的读者也适合那些已经在用 Spring Boot、但对大模型应用开发还比较陌生的同学。1. 基础认知LangChain4j 是什么、到底解决了什么问题1.1 Java 生态为什么需要自己的 LLM 框架先聊一个最现实的问题既然 Python 那边已经有很成熟的 LLM 应用框架为什么 Java 这边还要折腾一个原因很简单——Java 项目要接入大模型时痛点不是“能不能接”而是“接得痛不痛”。你当然可以直接用 HTTP 客户端调用模型接口但很快就会发现要自己处理的事情越来越多对话历史怎么维护、工具调用的请求格式怎么拼、模型返回的 JSON 怎么保证能解析、向量检索怎么和业务数据打通……这些琐碎工作每个项目都要重复一遍。LangChain4j 把这些公共问题收敛成了框架能力同时保持了 Java 生态的直觉。它不要求你放弃 Spring也不要求你写一堆胶水代码。你可以把它理解成 JDBC 之于数据库JDBC 为不同数据库提供了统一访问接口LangChain4j 则为不同模型服务商和不同应用组件提供了一层通用抽象。你写的业务代码不用绑死某一家厂商后面要换模型时通常只需要改配置。1.2 核心模块与设计思路拆解LangChain4j 的项目结构值得花两分钟了解一下因为装依赖的时候就用得上。核心模块是最底层的接口和抽象ChatLanguageModel定义了聊天模型的调用方式EmbeddingModel定义了向量模型的能力ToolSpecification描述了工具调用的元信息。这套抽象是所有上层功能的基础。默认实现模块则提供了开箱即用的工具类比如消息窗口记忆、内置的提示词渲染器、内存版向量存储等新手做原型时基本不需要再引其他库。对 Spring Boot 用户来说最友好的是官方提供的 starter 模块。它可以把模型客户端、嵌入模型、向量存储等自动配置成 Spring Bean你只需要在配置文件里写清楚模型地址、密钥和模型名代码里直接注入接口就能用。另外针对不同模型服务商的适配模块是按需引入的你用了哪家的模型就引哪家的扩展包但这种做法会带来一个小小的权衡如果写死在代码里后续换服务商就要动代码。所以我在下面的实战示例中会采用“通过配置指向服务商地址”的通用做法尽量让你和具体厂商解耦。1.3 什么场景该用它什么场景别硬上LangChain4j 适合的场景非常明确企业内部知识库问答、工单自动分类、客服助手、流程自动化中需要“让模型理解指令并调用现有系统”的场景。这些场景的共同点是对实时性要求没那么极端但对开发效率和可维护性要求高。用框架能把核心逻辑收敛在很小的代码量里后续接新的模型或调整检索方案都相对平滑。但也有不该硬上的情况。比如你需要的是毫秒级的规则匹配压根不该引入大模型再比如你的业务对输出格式有强约束且完全不能接受不确定性那么现阶段直接塞给模型处理还是不现实。框架本身只是工具不能帮你消除模型的不确定性。另外如果你完全没有一个可以稳定访问的模型服务地址而是打算“本地随便跑个小模型”应付所有任务那也要谨慎——小模型的指令遵循能力和工具调用成功率往往不够理想这会影响后续所有功能的上限。2. 环境准备三分钟搭好最小可运行工程2.1 JDK、构建工具和 Spring Boot 版本怎么选我推荐直接用 JDK 17 及以上版本配合 Maven 3.8 构建。Spring Boot 选择 3.x因为 LangChain4j 对 Spring Boot 3 的集成支持最到位。有些老项目还停留在 Spring Boot 2也能用但配置和兼容性上要额外折腾新手不建议开局就挑战这个复杂度。如果你是纯 Maven 项目不引入 Spring Boot starter也可以直接用核心依赖。不过对大多数 Java 后端团队来说Spring Boot 几乎是标配所以我下面的示例基于 Spring Boot 3。先别急着引入一堆依赖最简配置只需要一个 starter 就够。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version0.36.0/version /dependency版本号以你查到的当前稳定版为准。这个 starter 会自动拉起核心模块和部分默认实现让ChatLanguageModel等接口可以直接注入。2.2 配置文件里的三个关键参数很多人第一次跑不起来往往就是卡在配置上。LangChain4j 的 spring boot starter 会从配置文件读取模型相关参数你需要在application.yml中至少配置三个东西模型服务地址、密钥、模型名。langchain4j: chat-model: base-url: https://your-provider.example.com/v1 api-key: ${MODEL_API_KEY} model-name: your-model-id temperature: 0.7这里有几个容易忽略的点。base-url是模型服务的 HTTP 接口地址不同服务商地址格式不一样但通常都会有一个统一入口。api-key不建议直接写死在配置里我用的是环境变量注入${MODEL_API_KEY}会从系统环境变量读取避免把密钥提交到代码仓库。model-name也不是随便填的必须是你所选择模型服务上真实存在的模型标识填错了会直接报模型不存在或鉴权失败。2.3 第一个能跑通的模型调用配置完成后写一个最简单的启动类。我用CommandLineRunner在应用启动时直接发起一次对话验证链路是否通畅。import dev.langchain4j.model.chat.ChatLanguageModel; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; SpringBootApplication public class LangChain4jDemoApplication { public static void main(String[] args) { SpringApplication.run(LangChain4jDemoApplication.class, args); } Bean CommandLineRunner demo(ChatLanguageModel model) { return args - { String answer model.generate(用一句话解释依赖注入); System.out.println(answer); }; } }这段代码里ChatLanguageModel是框架的核心接口它由 starter 自动配置成了 Bean。model.generate(...)是同步阻塞调用直接把模型返回的文本打出来。如果你看到控制台正常输出了一句话说明模型链路已经通了。注意这里我刻意没有绑定任何具体厂商的实现类名而是依赖 spring boot starter 的自动装配。不同服务商、不同版本的 starter配置前缀可能略有差异如果你跑不通第一件事就是检查你当前版本对应的配置键名。3. 核心实践从一次对话到结构化输出3.1 ChatLanguageModel 的运行逻辑ChatLanguageModel是理解 LangChain4j 的钥匙。它屏蔽了底层 HTTP 请求、鉴权、响应解析等细节让你面对的核心概念只有“输入一段文本输出一个回答”。但它的方法签名和返回值并不像表面那么简单generate方法在框架内部被设计成多种重载可以接收字符串、ChatMessage列表也可以接收Prompt对象。实际项目中很少直接用纯字符串调用模型。因为模型没有“记忆”每一次调用都是独立的。如果你问完“你好”再问“我叫某某”第二次调用如果不携带第一次的上下文模型根本不知道“某某”是谁。所以后续的所有进阶功能——记忆、工具、RAG——本质上都是围绕“如何把更多上下文组装进这一次调用”展开的。搞明白这条主线你看到那些抽象类时就不会晕了。3.2 提示词模板让 Prompt 可维护写模型应用时提示词就是代码的一部分。如果你直接拼字符串很快就会遇到引号转义、动态参数插值、多轮改写困难等问题。LangChain4j 提供了PromptTemplate来做这件事它的语法很直观用双大括号作为变量占位符。import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.input.PromptTemplate; String templateText 你是一名严谨的技术问答助手。 请基于以下资料回答问题如果资料中没有答案请明确说不知道。 资料{{context}} 问题{{question}} ; PromptTemplate template PromptTemplate.from(templateText); String prompt template.apply(Map.of( context, LangChain4j 是一个 Java 版大模型应用框架, question, LangChain4j 是什么 )).text(); String answer model.generate(prompt); System.out.println(answer);把提示词抽成模板的意义不只是让代码更整洁更重要的是方便后续调试和迭代。你在调 prompt 时经常需要对比不同版本的效果。如果 prompt 散落在各种字符串拼接里改一版就要翻半天代码集中管理后你可以很轻松地把模板放到配置文件或独立的文本文件里甚至做成带版本号的资源文件。3.3 结构化输出让模型返回可解析的 JSON新手最容易碰到的坑是让模型“用 JSON 格式回答”结果返回的格式偶尔多几个解释文字偶尔少一个引号直接JSON.parse就报错。LangChain4j 的结构化输出能力可以把这个过程规范化。一种常见做法是让模型返回 JSON 字符串再用 Jackson 解析成对象。但更顺手的方案是结合框架的AiServices把“输出对象”这件事直接声明在 Java 接口上。import dev.langchain4j.service.AiServices; import dev.langchain4j.service.UserMessage; interface CustomerInfoExtractor { UserMessage(从用户留言中提取客户信息{{text}}) CustomerInfo extract(String text); } record CustomerInfo(String name, String city, String intentType) {}这个接口乍看有点“魔法”但理解后非常顺手。框架会根据方法返回类型自动构造结构化输出指令并负责把模型返回内容解析成CustomerInfo。你不需要手写 JSON 解析也不需要写异常的转义规则。实例化方式也很简单ChatLanguageModel model ...; // 由 Spring 注入 CustomerInfoExtractor extractor AiServices.builder(CustomerInfoExtractor.class) .chatLanguageModel(model) .build(); CustomerInfo info extractor.extract(我叫小明来自上海想咨询续费问题); System.out.println(info); // CustomerInfo[name小明, city上海, intentType续费咨询]结构化输出的核心价值在于它把模型从“文本生成器”变成了“可编程的函数”。你定义一个 Java 方法签名框架负责让模型输出满足签名约束的数据。这样后续业务代码可以直接使用强类型对象而不是整天处理脏字符串。4. 进阶实战带记忆的助手、会调用工具的业务代理、最小 RAG4.1 ChatMemory多轮对话不再“失忆”一个没有记忆的聊天机器人是没法用的。但“记忆”这件事做起来很微妙是把每条历史消息都发给模型还是只保留最近几轮如果保留太长的历史Token 消耗会快速上升响应延迟也会增加如果保留太短对话又会显得“没脑子”。LangChain4j 提供了ChatMemory接口最常用的实现是MessageWindowChatMemory它本质是一个滑动窗口只保留最近 N 条消息。比如窗口大小设为 10就表示模型每次对话时只携带最近 10 条消息。这个 N 的取值需要根据你的业务调整不要盲目调大。import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.service.AiServices; interface ChatAssistant { String chat(String userMessage); } ChatMemory chatMemory MessageWindowChatMemory.withMaxMessages(10); ChatAssistant assistant AiServices.builder(ChatAssistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build(); System.out.println(assistant.chat(我叫小明记住了吗)); System.out.println(assistant.chat(我叫什么名字));第二次提问时模型能够正确回答出“小明”因为MessageWindowChatMemory已经把前一轮的用户消息和助手回复都放进了窗口。这个能力的背后逻辑并不复杂AiServices每次调用模型前会从ChatMemory中取出消息列表加上当前用户消息一起发给模型。这里有一个很隐蔽的坑如果消息窗口设置得太小业务上关键的长期信息可能会被挤出窗口。比如用户在第一轮告诉了你他的需求编号对话进行到第八轮时如果已经刷了十几条消息窗口里可能就找不到这个需求编号了。针对这个场景更合理的方案是把关键信息提取出来写入“持久记忆”而不是依赖窗口大。但那是后话了新手先把窗口机制理解透彻。4.2 工具调用让模型真正操作你的业务如果说ChatMemory让模型“记住你”那么工具调用Function Calling就是让模型“帮你做事”。它的核心机制值得理解一下模型本身不能直接执行代码但它可以根据对话内容生成一个“调用某个工具的请求”框架拿到这个请求后在 Java 侧执行对应方法再把执行结果返回给模型最后由模型基于结果生成最终回答。用 LangChain4j 定义一个工具非常简单只需要在一个普通类的方法上标注Tool。import dev.langchain4j.agent.tool.Tool; class OrderService { Tool(根据订单号查询订单状态) public String queryOrderStatus(String orderId) { // 这里可以是任意 Java 逻辑比如查数据库、调外部接口 return 订单 orderId 状态已发货; } }然后把这个工具注册给AiServicesinterface OrderAssistant { String chat(String userMessage); } OrderAssistant assistant AiServices.builder(OrderAssistant.class) .chatLanguageModel(model) .tools(new OrderService()) .build(); String reply assistant.chat(帮我查一下订单 20240115 的状态); System.out.println(reply);当你运行这段代码时如果模型判断需要查询订单状态它会输出一个工具调用请求LangChain4j 会调用queryOrderStatus方法拿到执行结果后再让模型组织语言回答用户。整个过程对上层代码几乎是透明的——你感觉就是在和“一个懂业务逻辑的接口”对话。工具调用虽然好用但有几点必须注意。第一工具方法的命名和描述必须清晰因为模型是靠方法名和描述来决定什么时候调用它的。如果描述含糊模型就可能在不该调用的时候乱调。第二工具参数越少越好参数过多会显著降低模型生成正确参数的成功率。第三工具方法要具备幂等性和超时保护因为模型可能会重复调用或在你没预期的时候调用你的方法不能有严重副作用。4.3 最小 RAG给模型插上一个临时知识库RAG检索增强生成是目前解决模型幻觉和知识更新问题最实用的手段。它的核心思路是把文档切成片段向量化后存入向量存储用户提问时先从向量存储中检索最相关的片段然后把这些片段和问题一起交给模型让模型基于片段作答。LangChain4j 对 RAG 的支持很完善新手可以用InMemoryEmbeddingStore做原型验证。它把向量存在内存里不需要额外部署向量数据库适合用来跑通链路。import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; // 1. 准备文档内容并切分成块 Document doc Document.from(LangChain4j 是一个 Java 版大模型应用框架支持对话、RAG、工具调用。); ListTextSegment segments DocumentSplitters.recursive(300, 50).split(doc); // 2. 向量化并存入内存向量存储 EmbeddingModel embeddingModel ...; // 同样由配置注入 EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); for (TextSegment segment : segments) { var embedding embeddingModel.embed(segment.text()).content(); store.add(embedding, segment); }查询时只需要三步把用户问题向量化、在向量存储中查找相似片段、把片段拼入提示词。如果你用AiServices可以把检索器注册进去框架会自动完成“召回片段 拼入上下文”的流程。import dev.langchain4j.rag.retriever.EmbeddingStoreRetriever; EmbeddingStoreRetriever retriever EmbeddingStoreRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) .build(); interface KnowledgeAssistant { String chat(String userMessage); } KnowledgeAssistant assistant AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(model) .retriever(retriever) .build();这样搭建出来的是最简 RAG 链路足够你理解原理。生产环境下你大概率需要换用真正的向量数据库比如相对于团队运维能力更成熟、部署成本可控的方案并设计更精细的文档切分策略和检索后处理流程。但概念模型是一致的先检索相关内容再让模型基于内容作答。把这一点理解透了后面就是数据库选型和工程化的问题。5. 新手最容易踩的坑5.1 高频错误速查表我在实际调试中遇到过不少问题绝大多数都是配置和环境引起的。整理成一张表方便你对号入座。症状常见原因解决办法启动报错提示找不到ChatLanguageModelBean配置缺失或 starter 版本不匹配检查application.yml中模型配置项确认配置前缀与当前版本一致请求报 401/403API Key 错误或环境变量未设置确认环境变量名与配置引用一致检查密钥是否过期请求报 404 或模型不存在model-name填错登录模型服务商后台确认模型 ID 的真实值调用模型超时网络到目标服务地址连通性差检查网络连通性尝试切换到服务商提供的低延迟区域地址返回内容不定时会多出解释文字提示词约束不够强在提示词中强调“只输出 JSON不要多余内容”或使用结构化输出接口工具一直不被模型调用工具描述不清晰或模型不支持 function calling优化工具名和描述确认当前模型支持工具调用能力对话中途“失忆”突然严重消息窗口过小关键信息被挤出调大窗口或把关键业务信息存入额外持久化记忆5.2 成本和延迟控制的实用建议模型调用是按 Token 计费的很多新手一开始不管上下文长度导致每次请求携带的历史消息越来越多账单涨得很快。控制成本最简单的方法是限制消息窗口大小同时只把真正必要的上下文传给模型。延迟方面如果你做的是面向用户的交互应用建议使用流式响应StreamingChatLanguageModel让用户看到逐字输出体感会好很多。LangChain4j 也提供了流式接口用法和同步接口类似。另外如果业务上有大量相似的请求可以考虑做一层结果缓存而不是每次都去问模型。5.3 我的一些实操心得体会这几个项目做下来最大的感受是先跑通最简链路再叠加功能。不要一上来就同时上记忆、工具、RAG。我第一次做的时候把记忆、工具和知识库全塞进一个项目结果出了问题都不知道是哪个环节造成的——是提示词没写好还是工具描述不对还是检索结果太差完全没法定位。后来改成“一次只加一个变量”很快就稳定了。还有一点是关于工具方法的异常处理。工具方法是会被模型“指挥”执行的如果方法内部抛出异常框架通常会把异常信息返回给模型模型可能会尝试换个参数再调一次。这本身不是坏事但你的方法必须有完善的日志和超时保护否则可能出现意外的高频调用。建议工具方法保持无状态不要在方法内部维护跨调用的数据。最后再分享一个很实用的小技巧给模型设定角色时把“角色设定”放在提示词最前面并且加一句明确的回答约束。比如“如果你不确定就直接说不知道”。这听起来简单但比很多复杂的调参更能提升回答质量和稳定性。模型应用开发和传统 Java 开发的思维很不一样核心是“你想办法用清晰的上下文引导模型的输出”。把这条想通了LangChain4j 用起来会顺手很多。