1. 为什么 Java 项目接入大模型这么让人头疼先讲个真实经历。上个月帮一个朋友的项目组救火需求其实很常规把客服知识库接进大模型做一个能回答产品问题的问答机器人。问题出在技术选型上——组里有 Python 背景的同学开口就是 LangChain但整个业务服务是 Spring Boot 写的为了一个问答功能专门搭一套 Python 微服务部署、监控、开发协同全都得多养一套维护成本直接翻倍。后来我帮他们把方案换成了 LangChain4j两天时间就交付了一版能演示的原型代码量比裸调 HTTP 接口还要少。这就是 LangChain4j 存在的意义它是 LangChain 在 Java 生态的对应实现专门解决 Java 后端接入大模型时的水土不服问题。如果你曾经直接用 HttpClient 拼请求体、手动处理流式 SSE、自己解析 JSON 返回再自己维护对话历史列表那你一定懂我说的折腾。LangChain4j 把这些琐碎的环节全部封装成了 Java 开发者熟悉的 API 风格让你像写普通 Java 代码一样去编排大模型能力。这个教程不是给你堆概念而是从创建一个空的 Maven 项目开始一路走到能处理流式输出、能返回结构化 JSON、能对接自己的知识库、还能让模型调用你已有的业务方法。适合的人很明确正在用 Java / Spring Boot 写业务系统、想接大模型但不想引入 Python 二套系统的人。整个教程基于 LangChain4j 0.36.x 版本API 变化如果你用的是更新的版本核心思路完全一致。1.1 裸调大模型接口的痛点假设你要用 Java 调用一个大模型的对话接口最原始的写法大概是封装请求 DTO、设置 Authorization 头、序列化 JSON、用 HttpClient 发送、再解析响应。单次对话倒还凑合一旦涉及多轮对话你就得自己维护 messages 列表手动把历史记录拼进去涉及流式输出还得处理 SSE 的换行和 data 前缀涉及工具调用要自己解析响应里的 function_call 字段再回传结果。这些代码写起来不难但量大、重复、容易出错而且换一家模型厂商就要重调一遍。LangChain4j 的价值在于把上述动作抽象成了几个稳定接口。不管底层是 OpenAI 兼容接口、智谱、通义还是本地部署的模型你在业务代码里面对的都是同一个ChatLanguageModel。这意味着你可以在本地用一个便宜的模型开发调试部署时切换到生产模型业务代码一行都不用动。1.2 LangChain4j 是什么、不是什么先把这个框架的边界说清楚省得你走弯路。LangChain4j 不是一个完整的低代码平台它不提供可视化编排界面也不自带向量数据库集群。它是一个 Java 库核心是给你一套面向大模型应用的编程模型对话、记忆、提示词模板、检索增强RAG、工具调用、结构化输出。同时它也不是某家云厂商的 SDK。它支持通过 Maven 引入多种实现你也可以用baseUrl拼接任何 OpenAI 协议兼容的服务。框架本身不绑定某个模型这一点在做技术选型时非常重要——避免被单一厂商锁死。1.3 这套教程适合谁、能帮你完成什么如果你满足以下任意一种情况这篇教程会有实际帮助Spring Boot 项目中需要做智能问答、内容总结、信息抽取想把 LLM 能力做成内部 API 提供给前端需要让模型读取企业内部文档后回答问题想让模型通过工具调用去操作数据库、查订单、查天气等已有 Java 服务。教程完成后你会得到一套可直接复用的工程骨架包含依赖管理、模型配置、流式对话、结构化输出、基于向量检索的问答、以及 Function Calling 的完整示例。所有代码我都会标注关键点而不是让你复制后直接跑不通。2. 环境准备与第一个能跑的 Demo2.1 Maven 依赖怎么引先确认环境JDK 17 或更高版本这是 LangChain4j 的硬性要求别再用 JDK 8 硬试了Maven 3.6 或者直接用项目现有的依赖管理工具。在pom.xml中加入核心包和一个模型实现包dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency核心包langchain4j提供接口和基础数据结构langchain4j-open-ai提供 OpenAI 协议实现。如果你想用智谱、通义或者 Qwen 等国内厂商的官方 SDK 风格也可以换成对应的langchain4j-zhipu-ai、langchain4j-dashscope等模块。更通用的做法是继续用langchain4j-open-ai然后通过baseUrl指向兼容 OpenAI 协议的服务地址这个方法对很多国产模型同样适用后面会讲。2.2 5 分钟跑通一个对话引入依赖后写一个最简的对话程序。先别急着上 Spring Boot用main方法跑通链路能让你把框架的底层逻辑看清楚。import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; public class QuickStart { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .temperature(0.7) .build(); String answer model.chat(用一句话解释什么是大模型); System.out.println(answer); } }这段代码里OpenAiChatModel.builder()构建了一个模型客户端apiKey从环境变量读取避免把密钥硬编码进代码仓库。model.chat()是最直接的对话方法传入你的问题返回模型生成的文本。对新手来说这个ChatLanguageModel接口就是你整个应用和所有大模型交互的唯一入口后面所有高级功能包括记忆、工具调用、结构化输出最终都会绕回这个接口。如果你用的是兼容 OpenAI 协议的国产模型服务只需要在 builder 中追加baseUrl例如指向某个服务的/v1地址然后换成对应的apiKey和modelName即可业务代码其余部分不需要变化。2.3 Spring Boot 场景的快捷配置上面这个方法在普通 Java 工程里跑没问题但真实项目基本都是 Spring Boot。LangChain4j 官方提供了 starter 模块让配置成本和注入方式都更符合 Spring 习惯。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai-spring-boot-starter/artifactId version0.36.2/version /dependency然后在application.properties中配置langchain4j.open-ai.chat-model.api-key${OPENAI_API_KEY} langchain4j.open-ai.chat-model.model-namegpt-4o-mini langchain4j.open-ai.chat-model.temperature0.7之后直接在任意Service/Component中注入Service public class ChatService { private final ChatLanguageModel model; public ChatService(ChatLanguageModel model) { this.model model; } public String ask(String question) { return model.chat(question); } }Spring Boot 的自动配置已经把模型对象注册成 Bean 了你不需要手动new。这点看起来很 trivial但在团队协作时意义很大配置文件集中管理模型参数谁都不用去代码里翻 API Key 写在哪。需要说明的是starter 里可配置项很多包括log-requests、log-responses、超时时间等我会在踩坑部分给出一份推荐配置。3. 核心概念逐个拆解3.1 ChatLanguageModel一切的入口ChatLanguageModel是 LangChain4j 最核心的接口。它代表一个能对话的大模型屏蔽了底层 HTTP 调用细节。你调用的每个方法最终都会走一遍构建请求 → 调用模型 → 解析响应的流程但这些繁琐步骤对业务代码完全透明。接口里有几个常用方法新手容易混淆我给你区分清楚model.chat(String)最简单传入字符串返回字符串。model.chat(ChatMessage)传入更完整的消息对象比如带角色的UserMessage、SystemMessage返回更丰富的ChatResponse。model.chat(ListChatMessage)传入一组历史消息模型会结合上下文生成回复。这是实现多轮对话的基础。我建议从一开始就习惯用ChatResponse response model.chat(messages)这种形式因为ChatResponse里有aiMessage()、tokenUsage()等结构化的内容你后面做流式输出、统计 token 消耗都会用到。只图方便用字符串版本的话遇到需要上下文和元信息的场景又得重写一遍。还有一点ChatLanguageModel这个接口是线程安全的可以在 Spring 容器里作为单例 Bean 使用不用担心并发问题。我自己在高并发场景下压过客户端内置连接池性能瓶颈基本都在模型服务端。3.2 Message 体系与角色分类LangChain4j 把对话消息抽象成一个ChatMessage接口下面有几个实现类对应你在模型 API 文档里见到的那套角色体系SystemMessage系统提示词定义 AI 的人设、行为规则、输出格式。在整个对话中权重最高。UserMessage用户输入也就是你问的问题。AiMessage模型生成的回答。在工具调用场景中它还可能携带工具调用请求。ToolExecutionResultMessage工具执行完返回的结果它会被回传给模型做进一步分析。理解这套角色体系后你会发现所谓多轮对话其实就是维护一个ListChatMessage把历史消息按顺序装进去再发给模型。我自己常用的做法是把用户消息和 AI 回复成对加入这样模型才能记得之前聊了什么。3.3 PromptTemplate别再拼字符串了很多新手第一步就踩坑用字符串拼接来构造提示词比如你是 role 请回答 question。这样做有几个问题特殊字符容易破坏 JSON 结构、XSS 等安全问题风险高、模板复用时难以维护。LangChain4j 提供了PromptTemplate用来解决这件事import dev.langchain4j.model.input.PromptTemplate; import dev.langchain4j.model.input.Prompt; import java.util.Map; PromptTemplate template PromptTemplate.from( 你是{{role}}请用{{style}}的语气回答用户的问题{{question}} ); Prompt prompt template.apply(Map.of( role, 客服专员, style, 简洁友好, question, 我的订单多久能发货 )); String answer model.chat(prompt.toUserMessage()).aiMessage().text();模板中通过{{变量名}}占位调用时传入一个Map填充变量。相比字符串拼接它的优势是模板内容和业务代码分离提示词调整不需要改 Java 代码变量值会被安全地序列化不会破坏消息结构。如果你使用AiServices后面会讲还有SystemMessage注解直接在接口方法上声明系统提示词比手动构造消息更符合业务直觉。3.4 ChatMemory让 AI 记住上下文ChatLanguageModel本身是无状态的每次chat()都像和陌生人聊天。要让 AI 记住上下文必须把历史消息重新发一遍。自己维护历史列表很容易出问题无限累积导致 token 超限、需要自定义裁剪策略、多用户并发时互相串数据。LangChain4j 的ChatMemory就是干这个的。MessageWindowChatMemory是其中一个实现它会自动保留最近 N 条消息import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; ChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build();但更省心的用法是配合AiServices。你只要定义一个接口框架会自动管理记忆interface Assistant { String chat(String message); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); // 第一次对话 System.out.println(assistant.chat(我叫小明请记住我)); // 第二次对话模型能想起来 System.out.println(assistant.chat(我叫什么名字));AiServices是 LangChain4j 里一个很强大的代理框架它根据你定义的接口方法自动创建实现并在调用时自动装配记忆、工具、检索器等能力。对新手来说这个模式上手简单而且比手动操作ChatMemory更不容易出错。等到需要精细控制记忆时再手动介入也不迟。4. 实战进阶流式输出、结构化结果与 RAG4.1 流式输出打字机效果怎么实现对话应用最影响体验的细节就是打字机效果——文字逐字蹦出来而不是等十几秒后一次性展示。这背后是模型接口的流式响应SSE。LangChain4j 把这一层封装成了StreamingChatLanguageModel接口和StreamingResponseHandler回调。import dev.langchain4j.model.chat.StreamingChatLanguageModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.output.Response; import dev.langchain4j.message.AiMessage; StreamingChatLanguageModel model OpenAiStreamingChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); model.chat(给我讲一个冷笑话, new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { System.out.print(token); // 每个 token 到达时立即输出 } Override public void onError(Throwable error) { System.err.println(流式响应出错 error.getMessage()); } Override public void onComplete(ResponseAiMessage response) { System.out.println(); System.out.println(流式输出结束); } });onNext会在每个文本片段到达时被调用你在这里将片段传给前端比如 WebSocket 或 SSE 通道。需要注意流式请求一旦发起就不能像普通 HTTP 一样取消底层连接断开除外所以如果你要做停止生成按钮需要在连接层面或网关层处理。另外流式模式下拿不到完整的ChatResponse里的结构化 token 统计如果你需要统计用量可以让服务端在onComplete时读取response.tokenUsage()。4.2 结构化输出让 AI 返回 JSON 实体模型返回的是自然语言但业务系统需要的是User对象、Order对象。虽然你可以让模型输出 JSON 再解析但模型偶尔会夹带 Markdown 代码块、注释或者多余字段解析时直接报错。LangChain4j 在处理这个问题上有一套自己的理念不是让你事后解析而是在构造请求时通过 JSON Schema 约束模型输出格式。最简单的方式是让模型输出 JSON 后用框架自带的 JSON 工具解析import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatLanguageModel; import static dev.langchain4j.data.json.Json.fromJson; ChatResponse response model.chat(UserMessage.from( 从这句话中提取用户信息张三今年25岁住在杭州职业是后端工程师 )); String json response.aiMessage().text(); System.out.println(json); class UserInfo { String name; Integer age; String city; String job; } UserInfo user fromJson(json, UserInfo.class);更严谨的方案是使用AiServices让返回结果直接映射到 record。定义接口时声明返回类型框架内部会解析模型的 JSON 响应并自动反序列化不需要你写一行解析代码。配合UserMessage模板效果非常干净record UserInfo(String name, Integer age, String city, String job) {} interface UserExtractor { UserMessage(从以下文本提取用户信息{{text}}) UserInfo extractUser(String text); } UserInfo user AiServices.builder(UserExtractor.class) .chatLanguageModel(model) .build() .extractUser(张三今年25岁住在杭州职业是后端工程师);看到这里你应该能感受到 LangChain4j 的设计哲学用接口描述业务意图框架负责把意图翻译成模型调用。这种方式在团队协作中很友好接手代码的人看接口签名就知道这个方法的作用而不用去读一堆提示词拼接逻辑。4.3 嵌入模型与 RAG 的最小实现RAG检索增强生成是当前大模型落地最实用的技术先把知识库文档转换成向量存入向量库用户提问时检索相关片段把它作为上下文塞给模型让模型开卷回答。用生活化的方式理解就好比考试时先翻书找答案再落笔作答而不靠模型硬背。LangChain4j 对 RAG 的封装同样很友好。先引入langchain4j-embeddings如果需要本地嵌入模型可以选langchain4j-easy-rag或结合具体向量库模块。最小实现分三步切分文档、嵌入、检索。import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.store.embedding.InMemoryEmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; // 1. 准备嵌入模型 EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(text-embedding-3-small) .build(); // 2. 切分文档并嵌入存储 Document document Document.from(LangChain4j 是 Java 生态的大模型应用框架……你的知识库内容); ListTextSegment segments DocumentSplitters.recursive(200, 50) .split(document); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); for (int i 0; i segments.size(); i) { store.add(embeddings.get(i), segments.get(i)); } // 3. 构建检索器 EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(store) .maxResults(3) .build();检索器构建好后再通过AiServices注入一个问答助手接口整个 RAG 流程就闭环了interface KnowledgeAssistant { SystemMessage(你是一名客服助手只能根据提供的知识库内容回答问题不要编造。) String answer(String question); } KnowledgeAssistant assistant AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); System.out.println(assistant.answer(LangChain4j 是什么));这里有两个容易被忽略的重要细节第一DocumentSplitters.recursive(200, 50)的 200 是分块大小50 是重叠长度合理的重叠能避免关键信息被切断在块边界第二InMemoryEmbeddingStore只适合内存级别的演示生产环境建议换 Milvus、Redis 向量模块等持久化方案。建议新手从一开始就把索引入库和检索回答拆成两个独立流程入库通常是一次性任务而检索回答是高频接口。5. 工具调用让 AI 使用你的业务代码5.1 用注解声明一个业务工具工具调用Function Calling是让大模型真正干活的关键模型本身不执行代码但它能在需要时请求调用你提供的函数再把结果融入回答。LangChain4j 用Tool注解简化了整个过程你只需要写普通 Java 方法然后交给框架去暴露给模型。import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolParam; public class WeatherService { Tool(查询指定城市的实时天气) public String getWeather(ToolParam(城市名称) String city) { if (杭州.equals(city)) { return 晴25 摄氏度微风; } return 暂时没有该城市的数据; } }Tool注解的名称和描述会被翻译成模型的工具定义ToolParam里的描述会帮助模型把用户的话映射到正确参数。因为工具描述越清晰模型选错工具的几率越低。我建议给方法和参数都写清楚描述哪怕是查询天气这种一眼就懂的能力模型也会更精准。5.2 让 AI 自己决定调用哪个函数工具定义好之后还是通过AiServices把它注册进去interface WeatherAssistant { String chat(String userMessage); } WeatherAssistant assistant AiServices.builder(WeatherAssistant.class) .chatLanguageModel(model) .tools(new WeatherService()) .build(); String answer assistant.chat(杭州今天天气怎么样需不需要带伞); System.out.println(answer);实际运行时模型看到杭州今天天气怎么样会先判断需要查询天气于是发出一个工具调用请求框架帮你调用WeatherService.getWeather(杭州)拿到返回结果后模型再基于这个结果组织回答杭州今天晴25 摄氏度微风不用带伞。整个过程你再也不用手写 tools 列表、解析 function_call、拼接 ToolExecutionResult 这些底层操作。这里我要特别提醒工具方法必须保证幂等且无副作用之外还要注意异常处理。如果工具内部抛出异常流式对话体验会变得很奇怪建议在工具方法内部捕获异常并返回一段描述性错误文本让模型根据文本组织友好回复而不是直接中断对话。5.3 工具调用失败的常见原因工具调用写完跑不通绝大多数情况不是代码问题而是模型没有正确理解工具。我总结过几类现象第一模型压根不触发工具。原因往往是Tool的描述写得模糊或者用户提问时没有明确提到能触发工具的关键词。解决办法是尽量在系统提示词里引导你可以使用可用工具来查询实时信息。第二模型调用了工具但参数是空或乱填。常见原因是参数描述不清楚模型不知道city该传城市名还是城市代码。处理方式是在ToolParam描述里给出例子比如城市名称例如杭州、上海。第三模型陷入工具调用循环反复请求同一个工具。这种情况通常是因为返回结果不够明确模型判断信息不足继续请求。你需要在工具返回的文本中直接给出结论别只返回查询成功要让模型能基于它完成回答。6. 踩坑实录与调试建议6.1 超时、限流与配额问题真实生产环境里大模型接口的稳定性是最大挑战。第一个高频问题是超时模型推理慢时几十秒没响应很常见。默认 HTTP 客户端超时时间往往不够需要显式调长。OpenAI 客户端 builder 里可以配置timeoutOpenAiChatModel.builder() .apiKey(...) .modelName(gpt-4o-mini) .timeout(Duration.ofSeconds(60)) .maxRetries(2) .build();第二个高频问题是 429 限流和insufficient_quota余额不足。前者可以靠重试缓解后者只能充值或换 Key。LangChain4j 的重试机制是按模型实现的maxRetries设置后能自动应对部分限流。但如果你的接口要支撑高并发建议在网关层做令牌桶限流和降级而不是依赖模型端的重试。这里给一个通用建议把所有模型调用统一封装在一个服务类中不要散落在业务代码里。这样无论是加缓存、加熔断、还是切换模型都只需要改一个类。6.2 模型返回乱码、JSON 解析失败怎么办模型返回内容不可控是新手最容易崩溃的地方。比如要求返回 JSON模型却夹带了 json 前缀或者字段名大小写不一致又或者中文内容出现编码异常。LangChain4j 结构化输出对 JSON Schema 的支持能大幅降低这种问题但对严格的 SCHEMA 必须完整指定字段类型和 required 列表。如果你选择手动解析记得做防御性处理先用正则提取 JSON 片段再使用容错性较好的解析器。更重要的是生产环境一定要做好失败兜底解析失败时返回一个兜底对象或者提示用户稍后重试绝不能把堆栈直接抛给前端。关于中文乱码大部分原因是工程本身编码问题而非模型问题。确保 Maven 编译编码和 HTTP 请求编码一致Spring Boot 项目检查server.servlet.encoding配置通常就不会出现乱码。6.3 本地调试的三个实用技巧调试 LangChain4j 应用不能光靠断点因为模型行为具有不确定性。我推荐三个方法第一打开请求/响应日志。OpenAI builder 上有logRequests(true)和logResponses(true)Spring 配置里对应是langchain4j.open-ai.chat-model.log-requeststrue。开一会儿你就能看到发送给模型的完整消息结构、模型返回的原始内容定位问题非常高效。比如模型为什么不触发工具看日志里模型有没有返回 function_call 就知道了。第二用单元测试固定提示词。模型输出不稳定但你可以用不依赖真实模型的测试来验证模板拼接是否正确。简单做法是把PromptTemplate.apply的结果打印出来人工检查占位符和上下文组装是否正确。第三构造最小复现用例。遇到问题时把输入消息列表、系统提示词、工具定义抽到一个单独的 main 方法里跑排除业务干扰。6.4 成本控制别让 Token 悄悄烧钱最后说一个很多人没注意的事多轮对话和 RAG 会显著放大 token 消耗。MessageWindowChatMemory.withMaxMessages(20)意味着每次请求都会携带最多 20 条历史消息这些消息反复参与计费。RAG 把检索到的片段拼进上下文同样每次都重复计算。如果对成本敏感有几点经验第一根据业务合理设置maxMessages。客服场景保留 6~10 条足够需要长记忆的场景再用更大的窗口。第二RAG 的maxResults不要盲目设大检索 3 个片段和检索 10 个片段的效果差距不大但 token 消耗翻倍。第三选择模型时贵模型不一定更好——简单模板化的问答任务小型模型往往又快又省把复杂的推理任务才交给大模型。我自己维护一个线上问答系统成本优化的路径基本是调低历史窗口、控制检索片段数量、把高频简单问题用缓存兜住这样月成本能下降 40% 以上。LangChain4j 这个框架从 2023 年底开始快速迭代到现在版本更新频繁API 偶尔会有调整。我的建议是把本教程当作一个入门骨架理解清楚核心概念之后再去读对应版本的官方文档。踩过几次坑之后我最大的体会是大模型应用开发的复杂度大部分不在模型本身而在工程化——记忆怎么管、工具怎么暴露、错误怎么兜底、成本怎么控制。LangChain4j 帮你把这些工程问题整理成了 Java 开发者熟悉的抽象省下的时间足够你专注在业务价值上。最后分享一个小技巧在新项目里先用AiServices把接口定义清楚再慢慢替换底层模型实现这套思路能让你的架构从一开始就保持灵活。