做 Java 后端的朋友想顺带接入大模型能力第一反应往往是去搜 Python 那边的 LangChain看几篇教程就发现全是 Python 语法照搬到公司项目里也不现实。LangChain4j 就是 Java 生态里对应的一套轻量抽象层它把调用大模型、构造提示词、维护多轮记忆、做文档检索、让模型调用工具这些高频操作封装成了 Java 开发者更熟悉的 API 风格。这篇教程适合已经写过 Spring Boot、会查 Maven 依赖但完全没碰过大模型开发的读者。我会按照实际项目落地的顺序从最小 Demo 开始一直讲到 RAG 和工具调用顺带把最容易踩的坑提前标出来。1. 项目定位与整体设计思路1.1 LangChain4j 到底在解决什么问题如果完全不使用框架你接入一个大模型接口需要做哪些事第一是拼 HTTP 请求把 system prompt、历史消息、用户输入按厂商要求的 JSON 结构组装好第二是处理鉴权、超时、重试第三是解析返回内容从一大段文本里抠出关键字段第四是把多轮对话的历史消息手动保存并每次回传。这样写一个接口还好一旦要做客服助手、内部知识库问答、报表生成等多个场景90% 的代码都是重复的。LangChain4j 把这层做了抽象核心是几个稳定接口ChatLanguageModel 统一模型调用PromptTemplate 负责提示词复用ChatMemory 负责上下文记忆EmbeddingStore 负责向量存储AiServices 负责把这些组件组装成带业务能力的对象。用一个生活化的类比它有点像我们有了 MyBatis 之后就不再手写 JDBC 连接复杂调用被收敛到一条清晰链路里。你只需要关注业务本身我要一个能记住上下文的客服机器人还是一个能查公司手册的问答助手。这个库还顺手解决了一个很实际的问题不同大模型厂商的接口细节各不相同但 LangChain4j 的接口是统一的。今天用某家的模型明天因为成本原因换成另一家的兼容接口只需要改构建模型的那几行配置业务代码基本不动。这对企业项目来说非常关键毕竟大模型供应商的选择经常会调整。1.2 为什么在 Java 项目里不直接用 Python 那套说实话如果你的服务可以完全独立团队也愿意维护一套 Python 服务直接用 Python 生态没有问题。但对大部分后端团队来说更希望把大模型能力集成到现有 Java 系统中复用已有的用户体系、权限、日志链路、监控告警。一个 Spring Boot 应用里再塞一个 Python 微服务只为了调用模型接口从运维角度看是个不小的负担。LangChain4j 的抽象不是简单把 Python 代码翻译成 Java而是按 Java 的习惯做了调整。大量使用 builder 模式、泛型、Optional以及和 Jackson 无痛配合的工具注解开发的直观感受是类型安全。比如你在代码里调用方法时参数是 String、返回是强类型对象而不是在 Python 里对着一个不知道内部结构的字典猜来猜去。编译期就能发现不少问题出错时定位也容易得多。同时Java 版本和 Spring Boot 的整合非常自然。你可以把 ChatLanguageModel 配置成一个 Bean在 Service 或 Controller 里直接注入日志方面可以直接用现有 logback 体系不需要额外搞一套日志收集。做 Agent 应用时工具调用往往要访问公司内部订单、权限、库存系统这些系统大多是 Java 的用 Java 直接写工具注册逻辑链路最短。2. 环境准备与依赖引入2.1 最小依赖清单与版本选择开始动手前先讲依赖。LangChain4j 的核心包是 dev.langchain4j:langchain4j但真正要用某个模型提供商的实现还需要引入对应模块。以使用某厂商的兼容接口为例我一般这样建工程properties langchain4j.version1.0.0/langchain4j.version /properties dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency具体版本号请以 Maven Central 上的最新稳定版为准。我特意用属性统一版本否则两个模块版本不一致时运行时会各种莫名其妙的类找不到或方法签名不匹配。这个坑我在早期踩过当时核心包和模型模块差了一个小版本运行起来直接 NoSuchMethodError排查了整整一下午。另外如果只想在本机跑通最小示例不需要额外引入 Spring Boot 相关依赖LangChain4j 本身不依赖 Spring。后面要集成到 Web 项目里再引 spring-boot-starter-web 即可。对于 RAG 场景内存版向量存储在核心包里就有不需要额外加依赖但如果要接独立的向量数据库就得根据选型补对应模块。项目里如果用了 Lombok注意版本兼容一般不会冲突但构建时留意一下编译日志更稳妥。2.2 API Key 与基础配置管理API Key 的管理是新手最容易翻车的地方。我见过有人把 Key 直接写在代码里提交到仓库后马上被外部扫描到不得不紧急换 Key。正确做法是放环境变量或配置中心。本地开发我通常在启动脚本里维护变量然后在代码里用 System.getenv 读取LLM_API_KEYyour_api_key_here LLM_MODEL_NAMEyour_model_id如果是 Spring Boot 项目更推荐放进 application.yml配合配置中心的密文管理。总之一个原则Key 不进代码库不打印日志。调试时哪怕开日志也要记得把 Key 的打印关掉。还有一个值得提前说的问题模型名称和 API 域名这些配置不要散落在代码里。把 API Key、baseUrl、modelName 都放配置将来从测试环境切到生产环境或者更换服务商改配置比改代码安全得多。ChatLanguageModel 的构建器也支持通过系统属性批量读取配置但我觉得显式读环境变量更直观减少魔法。3. 第一个对话 Demo把模型“喊”起来3.1 ChatLanguageModel 是与模型交互的统一入口写第一行代码前先理解一个核心概念整个 LangChain4j 使用链路中ChatLanguageModel 是通向大模型的统一入口。无论后面接了多少层 RAG、工具、多轮记忆最终都是把整理好的消息列表交给它去生成回复。先看一个最小可用示例import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; import java.time.Duration; public class QuickStart { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(LLM_API_KEY)) .modelName(System.getenv(LLM_MODEL_NAME)) .temperature(0.7) .timeout(Duration.ofSeconds(60)) .logRequests(true) .logResponses(true) .build(); String answer model.generate(请用一句话介绍你自己); System.out.println(answer); } }这里的 OpenAiChatModel 名字里带的 open-ai指的是兼容协议实现不是特指某一家公司。如果你用的是其他厂商但提供了兼容接口一般只需要额外设置 baseUrl。这段代码虽然只有十几行但值得注意几个细节。temperature 控制随机性做创意写作可以调高到 0.8 以上做 JSON 结构化输出建议 0.2 以内timeout 一定要显式设置默认值在一些场景下不够用logRequests 和 logResponses 在开发期建议打开它会把发送给模型的消息和模型返回的内容完整打印出来调试时能省很多力气。但要注意生产环境一定把这两项关掉否则每个请求的完整提示词和回答都会打进日志文件既浪费磁盘也容易泄露敏感业务内容。3.2 用 PromptTemplate 替代字符串拼接新手最容易犯的错是拿字符串拼接去构造提示词。一开始确实能跑但很快会遇到两个问题一是业务提示词越来越长字符串里到处是加号和引号改起来极其痛苦二是同样的提示词要在 Controller 层、异步任务、定时任务里重复用复制粘贴很容易改漏。LangChain4j 提供了 PromptTemplate用法和 Java 里的模板渲染很像import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.prompt.Prompt; import dev.langchain4j.prompt.PromptTemplate; import java.util.Map; public class PromptDemo { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(LLM_API_KEY)) .modelName(System.getenv(LLM_MODEL_NAME)) .build(); PromptTemplate template PromptTemplate.from( 你是{{customerName}}的售后客服。请用不超过{{wordLimit}}个字回复用户{{userMessage}} ); Prompt prompt template.apply(Map.of( customerName, 某零售项目, wordLimit, 50, userMessage, 订单为什么还没发货 )); System.out.println(model.generate(prompt.text())); } }模板里的 {{xxx}} 是变量占位符。有个细节要注意如果你在提示词里写 JSON 示例JSON 本身也包含大括号和模板占位符会冲突。处理办法有两个要么用不带渲染的纯文本提示词要么在 JSON 示例里用转义形式。我自己的习惯是凡是包含复杂 JSON 示例的提示词不用模板直接定义为常量字符串再用 Message 组织拼接避免转义地狱。可能有人觉得模板不就这么回事吗有什么了不起。但在团队协作里这样把提示词集中管理后产品和业务同学可以直接改模板文件不需要动 Java 代码重新发版。提示词本身就是业务逻辑的一部分越早用模板管理后患越少。3.3 让返回内容可控输出解析的几种姿势模型返回的是自然语言未必是程序能直接用的结构。最常见也最实用的方案是让模型返回 JSON再在 Java 里用 Jackson 解析。我会在提示词里明确要求“只输出 JSON不要输出任何解释文字”同时在解析前对返回文本做预处理import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class JsonDemo { public static JsonNode parseModelJson(String raw) throws Exception { // 有些模型会在 JSON 外层包一个 json 代码块先剥掉 String text raw.trim(); if (text.startsWith()) { int firstLineBreak text.indexOf(\n); int lastMark text.lastIndexOf(); text text.substring(firstLineBreak 1, lastMark).trim(); } return new ObjectMapper().readTree(text); } }这个剥代码块的逻辑虽然土但非常实用。不同模型听话程度不一样有的会乖乖只输出 JSON有的非要加几句“这是您要的JSON”于是解析就炸了。更稳的办法是把 JSON Schema 放进提示词让模型按 Schema 输出。LangChain4j 里也有结构化输出相关的支持原理就是给模型一个严格的 Schema。对新手来说先用“提示词约束 预处理 Jackson 解析”跑通以后再优化到严格模式这个过渡最自然。输出解析这条链路最容易出现玄学问题因为模型是概率输出同样的提示词跑两次结果可能不同。所以解析失败时不要死磕代码先看日志里模型到底返回了什么八成是格式不干净而不是代码写错了。4. 多轮对话与记忆管理4.1 默认无状态必须自己维护上下文大模型接口本身是无状态的。也就是说你每次调用 model.generate它并不知道你上一次说了什么。如果你做一个客服机器人用户说“我叫张三”下一句问“我叫什么”模型回答不出——因为它根本没记住。这就是多轮对话的第一个核心难题记忆管理。LangChain4j 用 ChatMemory 抽象解决这个问题配合 AiServices 使用非常顺手。先定义一个接口import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.service.AiServices; interface Assistant { String chat(String userMessage); } ChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build(); System.out.println(assistant.chat(我叫张三)); System.out.println(assistant.chat(我叫什么名字));AiServices 的原理是动态代理它在运行期为 Assistant 接口生成一个实现类你调用 chat 方法时它自动把请求交给模型同时在调用前后把消息写入 memory。这样业务代码里完全感觉不到记忆的存在。如果之后想把记忆从纯内存换成 Redis 持久化LangChain4j 也有对应的持久化接口业务代码基本不需要改。我第一次用这个功能时有个误区觉得 AiServices 是不是太重了不如自己手动拼历史消息。用了几个星期才发现手动拼历史消息有两个致命问题一是很容易搞混 User 消息和 AI 消息的角色标签二是历史消息无限增长后 prompt 变得巨长。用 ChatMemory 至少能保证消息角色不会错并且有窗口机制帮你控制长度。4.2 记忆窗口与 Token 预算怎么调MessageWindowChatMemory 的 maxMessages 只控制“最多保留多少条消息”这不等于“不会超 token”。每条消息有长有短有的用户一次粘贴一大段日志20 条消息可能瞬间撑爆上下文窗口。所以更合理的做法是用 token 数而不是消息条数来衡量窗口。LangChain4j 还支持设置 token 预算底层会按消息的 token 数做截断。我自己的经验是别把上限顶到模型上下文窗口的最大值。还要预留系统提示词、RAG 检索结果、工具返回结果的空间。比如一个模型的上下文是 8K token我会把记忆窗口上限设在 4K 左右留出一半给刚检索到的资料和工具结果。宁可让模型忘记更早的对话也不要让它在运行中突然报“上下文超限”。还要提醒一点memory 是会话级别的对象。如果整个系统只创建一个 Assistant 单例所有用户都会共享同一份记忆那可就出大问题了。正确做法是每个会话创建自己的 Assistant 实例会话结束时销毁或持久化。在 Web 场景里要么用会话 ID 做 key 缓存记忆对象要么在服务端维护 session 维度的 memory 实例。4.3 超长对话的妥协方案即使设置了合理窗口长对话还是会遇到瓶颈。一个用户和你聊了 50 轮窗口只保留最近 20 条他提到“三小时前你说过那个优惠券的用法”新窗口里已经没有这条信息了。要解决这种问题思路有两个一是把更早的关键信息做摘要二是把信息外置到检索系统。摘要方案是当对话长度超过阈值时让模型把已聊内容总结成若干条要点后续对话把这份摘要作为系统提示词的一部分。LangChain4j 社区也有对应的自动摘要记忆思路。这里不展开实现但你要明白一个方向对于产品级客服场景纯靠滑动窗口解决不了所有问题需要把用户画像、订单状态这类高价值信息以结构化数据存入业务表每次对话前主动查询而不是指望模型记住历史。5. 文档问答RAG与工具调用5.1 RAG 的整体流程与最小实现RAG检索增强生成简单说就是先把你自己的文档切片、向量化、存到向量库用户提问时先从向量库检索相关内容再把检索结果和问题一起交给模型回答。这一套能力在 LangChain4j 里被封装得比较友好先看建库和入库import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.InMemoryEmbeddingStore; import dev.langchain4j.model.embedding.EmbeddingModel; EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(LLM_API_KEY)) .modelName(System.getenv(LLM_EMBEDDING_MODEL_NAME)) .build();然后用 ingestor 一次性处理文档import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.parser.TextDocumentParser; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.ingest.EmbeddingStoreIngestor; import java.nio.file.Path; Document document FileSystemDocumentLoader.loadDocument( Path.of(docs/help.txt), new TextDocumentParser() ); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 100)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(document);这段代码隐藏了“分片”这个关键环节。为什么必须分片因为大多数模型的输入长度有限一个 20 万字的帮助文档没法整体向量化而且用户的问题通常只和文档里某一小段相关。分片长度到底选多少没有统一答案。我试过 300、500、1000 三种对小规模知识库来说 500 字左右效果比较均衡。太短会导致检索内容碎片化太长则可能混入不相关的内容检索命中率反而下降。分片的重叠长度同样重要避免把一句话从中间截断。查询端更简单。用检索器把最相关的几条内容捞出来再交给配置了内容检索能力的模型组件组装答案import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.6) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String answer assistant.chat(文档里提到了哪些仓库部署步骤);这里 minScore 是相似度阈值设置太高考不到内容设置太低则会把无关内容塞进去。0.6 只是起点要按你实际文档的向量分布调整。内存向量库适合演示和小规模数据数据量上来或要多人共享时记得换成独立向量数据库。5.2 工具调用让模型拥有“外挂能力”Java 开发者第一次听到 Agent 这个概念时往往觉得玄乎其实核心就是工具调用。你写一个普通 Java 方法加上一个注解模型在回答用户问题时发现需要查真实数据就会自动调用你的方法并把方法返回值当作上下文继续组织回答。import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.P; class OrderTool { Tool(根据订单号查询订单状态) public String getOrderStatus(P(订单号) String orderId) { // 这里写真实的订单中心查询逻辑 return 已发货物流单号SF123456; } } interface CustomerAssistant { String chat(String userMessage); } CustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .tools(new OrderTool()) .build(); String answer assistant.chat(帮我查一下订单 A123 到哪了);实际调用时模型看到用户问题里包含订单号会先解析出参数 A123然后框架自动调用 getOrderStatus拿到“已发货物流单号SF123456”这样的返回值再结合返回值生成最终回答。整个过程用户无感知。这里有几个关键注意点。第一工具方法一定要写清楚 Tool 描述和 P 参数描述因为模型就靠这两段描述来判断什么时候调用、传什么参数。第二一个工具方法只做一件小事返回结果尽量是简洁的结构化文本别让模型二次解析一大段业务日志。第三工具调用会额外消耗 token而且可能连续调用多次调试时一定开着日志观察调用序列。工具调用是 Agent 的基座。有了它你可以让模型扮演一个能查订单、能看库存、能计算价格的客服而不是只会背知识点的聊天机器。再配合流程编排做一个简单的自动化助手完全够用。5.3 RAG 和工具调用如何搭配在复杂业务场景里RAG 和工具往往同时出现。比如一个运维助手需要先从运维手册中检索排障步骤再调用监控系统接口拉取实时指标。LangChain4j 的 AiServices 允许同时配置 contentRetriever 和 tools检索结果和工具结果会一起进入模型上下文。但也不是一味叠加。我见过一个惨痛案例项目里塞了 5 个工具、一个全量知识库 RAG每次请求的上下文飞快突破上下文窗口模型回答明显变迟钝甚至开始把不相干的内容揉到一起。实践经验是对于一个会话工具数量控制在 5 个以内检索结果的片段控制在 3 段左右每段 300 到 500 字。如果业务确实需要很多工具就拆成多个专用 Assistant由上层路由决定调用哪一个而不是全塞在一个对话里。6. 常见问题与排查实录6.1 高频报错速查表做 LLM 应用和做普通 Web 应用有个很大的区别大部分报错不是堆栈信息里一层层翻就能定位的而是模型返回了超出预期的内容或者网络链路出了幺蛾子。我把新手时期踩过的坑整理成一张速查表现象可能原因排查建议SocketTimeoutException模型接口响应太慢或网络不稳定调大 timeout观察日志里请求耗时401 / 403API Key 错误、权限不足、套餐过期检查环境变量和配置中心的 Key404 Model Not FoundmodelName 填错或该账号没有这个模型权限对照厂商文档确认模型名NoSuchMethodError核心包和扩展包版本不一致统一依赖版本清掉本地缓存重新构建JSON 解析异常模型返回了额外文字或 markdown 代码块先用日志打印原文再做预处理上下文超限消息和检索内容超过模型窗口减小记忆窗口、减少检索条数、精简 system prompt调用工具后回答偏题Tool 描述不够明确或返回结果太长重写工具描述让返回值精简这个表不能覆盖所有问题但至少覆盖了 80% 的新手场景。6.2 新手最容易踩的四个坑第一个坑是依赖版本不一致。LangChain4j 从早期版本到现在迭代很快老接口在新版本里要么废弃要么改签名。网上搜到的示例代码可能已经失效。我建议只看官方仓库的示例目录和当前版本的 JavaDoc遇到报错先排查依赖版本而不是怀疑代码逻辑。第二个坑是 API Key 顺手写进配置文件后提交。我甚至见过部署脚本里带着真实 Key 的好在后来及时轮换。正确的做法上面讲过环境变量或者密码管理工具。一定要有密钥轮换流程泄露后能快速处理。第三个坑是 temperature 设置太高导致输出不稳定。做结构化输出时哪怕 temperature 只从 0.1 调到 0.7模型都可能从可靠输出变成“灵感型”输出。在需要程序解析 JSON 的场景temperature 尽量在 0 到 0.2 之间。第四个坑是为了调试方便一直开 logRequests(true) 就忘了关。有一次线上问题排查时才发现日志里全是完整用户输入和模型输出其中还包括一些用户上传的敏感文本。这个从日志落盘那一刻起就是安全隐患。我的习惯是开发环境开全量日志测试环境只开请求耗时生产环境完全关闭除非临时开 5 分钟抓问题。6.3 调试大模型应用的两个技巧第一个技巧是把提示词当代码来管理。不要顺手写在 Service 方法内部而是统一放到 resources/prompts 目录下用模板加载。这样改提示词就不用重新编译和发布同时还可以对每个版本做对比测试。我一直认为提示词的变更频率远高于代码不把它当一等公民管理迟早要吃大亏。第二个技巧是单元测试里固化模型输出期望。把一次典型的输入输出录下来作为断言基准。因为模型输出不稳定你不需要断言完整文本而是解析出关键字段后做 JSON 相等性比较。这样重构代码或升级版本时一旦发现核心字段变了能第一时间定位是代码问题还是模型行为变化。7. 收尾一点个人实操心得7.1 我的最小接入路径我每次接入一个大模型场景都是先拿最小 Demo 把链路跑通再逐步加入记忆、RAG、工具调用避免一开始就上全套。全套上齐后出了问题根本不知道是哪一环出的错。最小链路只需要一个 ChatLanguageModel跑通一次生成调用之后再按业务需求一步步加能力。7.2 一条调试心法调试到怀疑人生的时候记得先看模型返回的原始日志。大模型应用的很多玄学问题其实都是模型按它的理解输出了你不想要的东西而不是什么高深的技术故障。把这些基础动作做好LangChain4j 这套东西用起来会顺很多。最后再提一句配置和提示词尽量外置这可能是所有经验里最值得你立刻动手改的一点。