
做了多年Java后端我一度以为大模型应用是Python开发者的专属领地。每次看到身边同事用Python写各种智能体demo嘴上说“Java也能干”心里其实清楚Java生态里确实缺一个趁手的统一框架。直到项目里正式引入LangChain4j才算把这块缺口补上。它不是一个模型也不是某个厂商的SDK而是一套面向Java开发者的大模型应用开发工具箱。这篇教程不是官方文档的复述而是我从零搭工程、跑通第一个对话、加记忆、做工具调用、接RAG全过程的实战记录。适合两类人一类是刚接触大模型应用的Java后端想知道怎么像调普通接口一样调用模型能力另一类是做过Python版AI应用、想回迁到Java技术栈的开发者。读完你至少能搭出一个带记忆、能调用工具、能做本地文档问答的Java版AI助手。1. 项目概览与最小环境搭建1.1 LangChain4j是给Java开发者的大模型工具箱LangChain4j的核心思想非常朴素把大模型应用开发里那些高频重复的动作比如模型接入、消息组装、多轮记忆、向量检索、工具调用抽象成一套可复用的组件。你可以把它理解成Java世界里连接数据库的JDBC规范只是连接的对象变成大模型服务。不同模型厂商的接口风格差异很大有的返回JSON有的返回纯文本有的要走流式有的带函数调用协议。如果没有统一抽象每接一个模型就要重写一遍HTTP调用、重写一遍请求解析业务代码会被厂商细节淹没。LangChain4j用ChatLanguageModel、ChatMessage、ChatMemory、EmbeddingModel这些接口把这些细节挡住业务代码只面向接口。组件大致分为四层模型接入层负责和大模型服务通信消息层负责构造对话上下文记忆层负责保存历史会话应用层通过AiServices把前面三者组装起来让开发者写几个接口方法就能得到完整的AI能力。后面几节我会按这个层次逐个展开。1.2 准备JDK与构建工程建议使用JDK17及以上版本。JDK18、21我都跑过也没问题。构建工具用Maven或Gradle均可我下面以Maven为例因为项目里现成依赖更多。新建一个普通Java工程在pom.xml里引入LangChain4j的核心依赖dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-core/artifactId version1.0.0-SNAPSHOT/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version1.0.0-SNAPSHOT/version /dependency /dependencies这里说明一下langchain4j-core是最小抽象包只包含接口、模型消息结构等核心类型langchain4j是聚合包会带入常用的工具类、文档加载器和向量存储实现。实际生产项目可以按需裁剪但新手入门阶段先两个都放跑通流程最重要。依赖版本不要乱混。langchain4j-core和langchain4j必须保持一致否则运行时会很痛苦地遇到NoSuchMethodError。我早期在Maven仓库里扒了两个不同版本的jar包启动时就翻车了。建议在properties里统一管理版本号或者直接使用BOM。1.3 接入一个本地模型服务并跑通首个对话我故意不在这里引入任何厂商适配器而是自己实现一个最简ChatLanguageModel。这样做的目的是让你看清LangChain4j的抽象边界到底在哪只要你的模型服务遵循主流chat completions协议接入方式本质都是构造请求、发HTTP、解析响应。先创建一个LocalChatModel类package com.example; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessageType; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.ChatLanguageModel; import dev.langchain4j.model.output.Response; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; import java.util.Map; public class LocalChatModel implements ChatLanguageModel { private final String endpoint; private final String modelName; private final HttpClient client HttpClient.newHttpClient(); private final ObjectMapper mapper new ObjectMapper(); public LocalChatModel(String endpoint, String modelName) { this.endpoint endpoint; this.modelName modelName; } Override public String generate(String userMessage) { return generate(List.of(new UserMessage(userMessage))).content().text(); } Override public Responsedev.langchain4j.data.message.AiMessage generate(ListChatMessage messages) { try { ListMapString, Object list new ArrayList(); for (ChatMessage message : messages) { list.add(Map.of( role, roleOf(message.type()), content, message.text() )); } MapString, Object payload Map.of( model, modelName, messages, list ); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(endpoint)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload))) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); JsonNode root mapper.readTree(response.body()); String content root.path(choices).path(0).path(message).path(content).asText(); return Response.from(dev.langchain4j.data.message.AiMessage.from(content)); } catch (Exception e) { throw new RuntimeException(调用模型服务失败, e); } } private String roleOf(ChatMessageType type) { if (type ChatMessageType.SYSTEM) { return system; } if (type ChatMessageType.AI) { return assistant; } return user; } }这段代码只做了三件事把LangChain4j的消息列表转成请求JSON用JDK自带的HttpClient发POST请求把响应里的文本重新封装成LangChain4j的AiMessage。ChatLanguageModel接口还有几个重载方法在IDE里补齐即可核心逻辑都会落到generate(ListChatMessage)上。实际项目中你不太需要自己实现模型类厂商适配器都做好了。写这个类主要是让你知道LangChain4j本身不生产智能它只负责把智能“接进来”。现在写一个main方法跑一下ChatLanguageModel model new LocalChatModel( http://localhost:8080/v1/chat/completions, local-model ); System.out.println(model.generate(你好介绍一下你自己));如果你的本地模型服务没启动这里会直接抛异常这是正常的。第一段对话跑通后就可以进入核心抽象的学习了。2. 拆解LangChain4j的五个核心抽象2.1 模型接口与消息结构ChatLanguageModel是通往大模型的主入口所有对话请求最后都会经过它。它有一个同步版本还有一个流式版本叫StreamingChatLanguageModel区别就像普通HTTP请求和SSE流式响应的区别。消息结构是理解整个框架的钥匙。LangChain4j里主要有三类消息SystemMessage系统提示词用来约定模型角色和行为边界UserMessage用户的提问AiMessage模型生成的回复。服务端大模型其实只认一段连续的字符串所谓的多轮对话就是把上面这些消息按顺序拼成一个消息数组发送过去。LangChain4j帮你维护这个数组的顺序但内容是否完整还是要看开发者的记忆策略。2.2 记忆与对话窗口ChatMemory是对话历史的容器。最简单的是MessageWindowChatMemory它按消息条数控制记忆窗口。例如MessageWindowChatMemory.withMaxMessages(10)只会保留最近10条消息超出部分会被丢弃。新手最容易忽略的一点是不是保留越多越好。模型上下文窗口有限消息太长会直接请求失败或导致响应质量下降。AiServices是组装好记忆的关键。你只需要定义一个接口它就能帮你把模型和记忆串起来interface Assistant { String chat(String userMessage); } ChatMemory memory MessageWindowChatMemory.withMaxMessages(10); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build(); String answer assistant.chat(帮我查一下订单A1001);这里AiServices会动态生成接口实现每次调用chat时自动把历史消息从memory取出、拼接、发给模型再把模型回复写回memory。你完全不用手写消息拼接代码。2.3 Embedding与向量仓库EmbeddingModel负责把一段文本转成向量数组。EmbeddingStore负责存储和检索向量。它们共同服务RAG场景后面实战三会详细讲。你只需要先记住向量是浮点数组检索的本质是在向量空间里找相似度最高的文本。2.4 PromptTemplate与上下文组装LangChain4j提供了PromptTemplate作用类似字符串模板PromptTemplate template PromptTemplate.from( 你现在是一个客服助手。\\n用户问题{{question}} ); Prompt prompt template.apply(Map.of(question, 我的订单多久能发货));模板变量用双大括号包裹占位符替换后就得到一个可以发给模型的Prompt。如果没有这类工具你会陷入手写字符串拼接的泥潭尤其当上下文里有换行、引号等特殊字符时极易出错。3. 实战一带记忆的客服机器人3.1 模型本身不记历史很多新手以为模型理解“你刚才说了什么”其实模型默认是无状态的。它每次收到请求看到的只是一段新的消息列表。上一轮说了什么它根本不知道。所以多轮对话必须由应用层自己维护历史。我在最开始只调model.generate(你好)时下一轮再问“我叫张三记住我”再下一轮问“我叫什么”模型完全答不上来。这个现象正常不代表模型笨是应用没传上下文。3.2 用ChatMemory管理会话窗口用MessageWindowChatMemory来实现客服机器人最简单。我直接把它和AiServices配合做成一个接口方法public class CustomerServiceBot { private final Assistant assistant; public CustomerServiceBot(ChatLanguageModel model) { ChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); this.assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build(); } public String answer(String question) { return assistant.chat(question); } interface Assistant { String chat(String message); } }测试时你会发现第二次提问时第一次问答的历史已经在上下文里了。LangChain4j在每次调用前会做一次消息组装把你传入的UserMessage和记忆中的ChatMessage列表合并再统一发送。3.3 多用户记忆隔离真实项目中不能所有人共享一个ChatMemory否则用户A的对话历史会被用户B看到这也是一个典型的线上事故隐患。正确做法是给每个用户建一个独立记忆实例。用ConcurrentHashMap维护用户ID到记忆的映射private final MapString, ChatMemory memoryMap new ConcurrentHashMap(); public String chat(String userId, String question) { ChatMemory memory memoryMap.computeIfAbsent(userId, k - MessageWindowChatMemory.withMaxMessages(20)); // 再将memory绑定到当前用户的Assistant return buildAssistant(memory).chat(question); }注意AiServices.builder(...).build()每次构建会产生一个代理实例如果请求量很大建议把“用户ID到Assistant实例”也做成缓存避免反复创建代理对象。3.4 上下文过长怎么办当对话轮数很多时即使设置了maxMessages消息总长度也可能超出模型上下文窗口。常见思路有三种窗口截断只保留最近N条消息简单粗暴适合绝大多数客服场景消息摘要当历史超过阈值时调用模型把之前内容压缩成摘要再用摘要替换旧消息关键信息抽取只保留用户ID、订单号、地址这些业务关键信息。窗口长度不是越大越好我给内部机器人调优时最后发现8到12条消息是体验和成本的平衡点。太长了模型容易受到早期无关内容干扰太短了又记不住关键信息。4. 实战二让模型学会调用工具4.1 Function Calling解决的问题大模型本质是文字接龙它无法主动查询数据库、调用接口、获取实时数据。比如用户问“当前库存是多少”模型没能力查库存表它只能瞎编。工具调用就是让模型在需要时“提出请求”由Java后端实际执行请求再把结果给模型做最终回答。这个机制和人类协作很像模型像一个分析员它知道自己不知道答案于是指定一个函数名和参数Java代码像是执行专员拿到函数名和参数后真正去查库查询结果返回后分析员再组织语言给你答复。4.2 定义一个可被调用的工具方法在LangChain4j里工具方法只需要普通Java类上加Tool注解public class OrderService { Tool(根据订单号查询物流信息) public String getLogistics( ToolParam(订单号) String orderNo) { // 这里写真实的查询逻辑可以先返回模拟数据 return 订单 orderNo 当前状态运输中; } }Tool里的描述会被LangChain4j转换成模型的工具描述模型靠这段描述判断“我该不该调用这个工具”。参数上的ToolParam也很重要描述越清晰模型传错参数的概率越低。然后把工具注册进AiServicesAssistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .tools(new OrderService()) .build();用户问“订单A1001到哪了”模型返回的不是最终文本而是一个“工具调用请求”。LangChain4j捕获到请求后反射调用OrderService.getLogistics(A1001)拿到返回值后再把结果和原始问题一起发给模型最后生成面向用户的回答。这个完整链路全部自动完成你只需要保证工具方法是正确的。4.3 工具调用的注意事项第一不是所有模型都支持工具调用。本地部署的小模型可能只支持普通对话注册了工具也不会被触发。生产环境要确认你选用的模型适配器支持function calling。第二工具方法返回值最好是简洁字符串。模型会把整个返回内容重新读一遍再回答如果返回一个几百行的JSON模型反而可能抓不住重点。尽量返回“关键状态”而不是“全量数据”。第三注意工具方法不要有副作用。模型可能一次请求里要求调用多次工具也可能因为幻觉把参数传错。在执行写操作类工具时要先做参数校验和权限校验。5. 实战三本地文档RAG问答5.1 RAG的基本链路RAG全称检索增强生成核心思路是“先检索再生成”。用户提问后先从知识库中找出最相关的片段把这些片段拼进上下文再让模型基于片段作答。这样模型不再凭空发挥回答有依据也容易溯源。在企业内部落地时知识库通常来自产品文档、FAQ、操作手册等。模型参数里没有这些私有内容RAG就是让私有知识进入模型可见上下文的最常见方式。5.2 文档切块与Embedding模型原始文档不能整篇塞给模型因为上下文窗口有限而且检索粒度太粗。需要先把文档切成小片段。切块策略没有银弹一般做法是固定字符数加重叠。示例ListDocument docs FileSystemDocumentLoader .loadDocuments(Path.of(src/main/resources/docs)); DocumentSplitters.recursive(300, 50) .splitAll(docs) .forEach(segment - { Embedding embedding embeddingModel.embed(segment.text()).content(); embeddingStore.add(embedding, segment); });recursive(300, 50)表示每300个字符一片相邻片段重叠50个字符。重叠的意义是避免关键信息正好被切在边界上。段落切分通常比固定长度效果好但需要文档本身结构清晰。你可以先用固定长度跑通再根据问答效果调参。5.3 用向量存储实现检索向量存储我用的是LangChain4j内置的InMemoryEmbeddingStore适合单机Demo。生产环境建议换成独立向量服务或数据库插件但接口用法类似。检索代码如下Embedding queryEmbedding embeddingModel.embed(question).content(); ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant(queryEmbedding, 3); String context matches.stream() .map(match - match.embedded().text()) .collect(Collectors.joining(\n)); String prompt 请根据以下资料回答问题如果资料不足以回答请直接说明。\n 资料\n context \n问题 question; String answer model.generate(prompt);findRelevant(queryEmbedding, 3)返回最相似的3个片段。返回片段数量可以调整太少可能漏信息太多会稀释关键内容并浪费token。实际调优时我经常先打印检索到的片段看看是不是用户想问的内容再去调切片和返回数。RAG质量的最大变量不是框架而是Embedding模型和切片策略。框架只是把流程标准化真正决定问答效果的是喂进去的资料干不干净、检索准不准。6. 常见坑位与调优速查表6.1 高频报错排查实录现象可能原因处理建议NoSuchMethodError多个LangChain4j模块版本不一致统一所有依赖版本不要混用不同版本ConnectException本地模型服务没启动或endpoint错误先用curl请求确认服务地址可用请求报token超限消息窗口过大或上下文太长调小maxMessages或做摘要压缩工具始终不被调用模型适配器不支持function calling换支持工具调用的模型检查Tool描述检索结果和问题无关切片过大或Embedding模型不匹配调小切片长度更换更合适的Embedding模型首次请求特别慢模型服务冷启动或加载参数预热模型或限制并发避免排队有一次我排查半天最后发现是工程里同时引入了不同版本的langchain4j-coreMaven依赖树里出现了两条路径。强制指定版本后问题立刻消失。如果你遇到诡异异常第一步永远先看依赖树而不是猜代码。6.2 流式输出最佳实践在Web页面里如果等模型全部生成完再返回用户会感觉特别卡。流式输出能边生成边推送体验好很多。LangChain4j对此有独立接口StreamingChatLanguageModel streamingModel ...; // 用支持流式的适配器实例 streamingModel.generate(讲一个短笑话, new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { System.out.print(token); } Override public void onComplete(ResponseAiMessage response) { System.out.println(); } Override public void onError(Throwable error) { error.printStackTrace(); } });Web端一般会把SSEServer-Sent Events和流式接口对接把onNext里的token片段直接推送前端。注意要设置合理的超时时间避免模型服务长时间没返回时前端等到天荒地老。6.3 并发场景下的线程安全ChatLanguageModel实例在绝大多数情况下是线程安全的可以设计成单例系统启动时创建一次所有请求共用。但ChatMemory不行它是会话维度的状态多个请求同时写同一个ChatMemory会造成上下文错乱。我在做接口时会把用户ID与Assistant实例做映射并且给整个Assistant加上无状态设计。把ChatMemory作为每次会话的独立对象用ConcurrentHashMap管理。另外调用模型服务时建议加上重试和熔断。模型接口偶尔会有超时抖动盲目重试可能压垮下游服务限制最大并发比无限重试更有效。LangChain4j仍处在快速迭代阶段API偶尔会变。我踩过几次版本坑之后最大的体会是不要急着追各种新概念先把模型接入、消息、记忆这三板斧磨熟。后面不管加工具还是RAG都是同一套抽象上的组合。目前我们内部那个问答助手已经稳定跑了一段时间我印象最深的一课是Java生态缺的从来不是AI能力而是一个让人敢上手的落地方案。至少对我来说LangChain4j把这个缺口补上了一部分。