最近大半年我基本都在用 Java 写大模型应用试过直接拼 HTTP 请求、也试过 Spring AI最后把主力框架定在了 LangChain4j 上。如果你是 Java 工程师想在自己系统里加上大模型对话、文档问答、文本分类这类能力又不想为了写 AI 功能去转 Python那这篇 LangChain4j 新手入门实战就是给你准备的。我会从最基础的环境搭建讲起一步步跑通一个能对话的 Java 程序再带你用 Spring Boot 做一个真实的客服问答接口最后把参数调优和常见坑一并说清楚。文中的代码都是我实际跑过的写法不是那种“看上去能跑”的伪代码你照着敲基本不会翻车。1. 为什么Java开发者做AI应用绕不开LangChain4j1.1 大模型开发在Java生态里的尴尬现状先讲个背景。大模型能力开放之后最先火起来的是 Python 生态因为那边有 LangChain、LlamaIndex 这些成熟框架文档多、示例全社区讨论也多。Java 这边就比较尴尬大量企业系统是 Java 写的但要做 AI 功能时要么自己封装 HTTP 调用要么把模型能力单独做个 Python 服务再对接业务团队维护起来非常痛苦。我自己一开始就走的是“直接调 HTTP”的老路。写一个调用函数并不难但功能一多就麻烦了不同模型的请求格式不一样、提示词要频繁拼接、多轮对话要自己维护历史、返回的 JSON 要反复解析清洗。这些细节零零散散写下来代码又丑又难维护换一个模型厂商几乎等于重写一遍。所以后来我下定决心找 Java 原生的框架LangChain4j 就是在那时候进入视线的。1.2 LangChain4j的设计哲学与优势LangChain4j 名字里带个“4j”意思就是 For Java。它不是把 Python 版 LangChain 逐行翻译过来而是按照 Java 社区的惯例重新做了设计。最明显的一点构建对象统一用 Builder 模式不用动不动就写配置文件输入输出尽量用强类型比 Python 那种“一切皆字典”的方式在 IDE 里写起来舒服很多和 Spring Boot 整合非常顺滑直接声明一个 Bean 就能注入到业务代码里。它的核心思路可以概括成三句话统一模型接口、把提示词/记忆/检索这些能力模块化、让开发者只关注业务逻辑。模型厂商之间的差异被抽象成了 ChatLanguageModel 这个门面底下的 OpenAI、Ollama、智谱、通义等实现随时可以替换。这样一来你代码里写的永远是与业务相关的调用而不是某个厂商特有的 API。1.3 和其他Java方案的横向对比市面上并不是只有 LangChain4j 一个选择。Spring AI 背靠 Spring 官方加入了不少企业级特性但它的定位更像是 Spring 生态的一个扩展起步相对慢API 变动也比较快社区里能查到的实战案例还不多。另一个选择是彻底自己封装我不建议新手这么做因为大模型应用看起来简单实际上要考虑提示词管理、上下文窗口、流式输出、向量检索、结构化返回这些一连串问题自己轮一遍成本非常高。我自己的选型结论很直接只要你的项目是 Java 技术栈目标是快速把大模型能力做进业务里LangChain4j 是当前最省心的一条路。下面我就用实际代码一点一点拆给你看。2. 搭建最小可运行工程依赖、配置和第一个对话2.1 环境准备与Maven依赖先说环境。我用的是 JDK 17LangChain4j 新版本基本要求 Java 17 起步如果你的项目还在 Java 8能跑但需要找对应支持的旧版本我的建议是遇到这种情况优先升级 JDK。构建工具我用 MavenGradle 同理依赖坐标是一致的。去 Maven Central 拉依赖核心就两个。第一个是框架主包第二个是对应模型厂商的实现模块dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency版本号我写的是 0.35.0。LangChain4j 到现在的 API 都还在演进不同小版本的写法会有细微差别但核心思路是稳定的。真遇到编译不通过优先看官方 changelog别硬刚。API Key 不建议写死在代码里更不要提交到 Git。本地开发我习惯用环境变量比如OPENAI_API_KEY一劳永逸。如果你们公司有配置中心通过配置中心读出来设置到系统属性也是常见做法。2.2 第一个程序让模型跟你打个招呼依赖拉好之后最小可运行的程序其实就十行代码。创建一个普通的 main 方法类import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; public class HelloLangChain4j { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); String answer model.chat(用一句话介绍你自己控制在20字以内); System.out.println(answer); } }这里最关键的是ChatLanguageModel这个接口。注意变量类型是接口而不是实现类这就是第一节说的统一抽象。OpenAiChatModel.builder()负责创建一个 OpenAI 兼容协议的实现如果你用的是别的模型只需要把构造过程换成对应模块的 Builder后续代码一行都不用改。我用的模型是 gpt-4o-mini便宜速度快非常适合新手拿来练手。如果只是验证流程也可以换更基础的模型名Builder 里传名字就行。运行后的输出大概是“我是AI助手帮你解答问题、编写代码、处理信息。”这行字说明整个调用链路已经通了。到这里你就已经拥有一个能对话的 Java 程序了不需要额外启动任何中间件也不需要写复杂的 HTTP 封装。如果趁热把 apiKey 换成自己的配置方式再改一改 modelName 体验不同模型你会更快理解 Builder 模式里每个参数的作用。2.3 兼容协议接入Ollama和国内模型很多同学没有海外模型的 API或者出于数据隐私想把模型部署在本地。这种情况我的经验是先确认一个前提——绝大多数模型厂商和本地模型服务都提供了“OpenAI 兼容协议”的 HTTP 接口。也就是说模型端不管你是什么实现只要能按 OpenAI 的请求/响应格式说话就可以用langchain4j-open-ai这个依赖接入。Ollama 就是个典型例子。本地把 Ollama 跑起来以后它会监听 11434 端口其中/v1路径就是 OpenAI 兼容的服务。代码改成这样OpenAiChatModel.builder() .baseUrl(http://localhost:11434/v1) .apiKey(ollama) .modelName(qwen2.5:7b) .build();这里踩过一个坑Ollama 的 OpenAI 兼容端点要求 apiKey 参数非空哪怕不校验也得随便填一个字符串否则 Builder 会直接抛异常。另外modelName必须和你本地ollama list里拉取的名字完全一致大小写、冒号标签都不能错。国内模型厂商也是如此。智谱、Kimi、通义这些服务大多暴露了 OpenAI 兼容接口把 baseUrl 换成厂商给的地址、apiKey 换成自己的 key、模型名换成厂商的模型标识就能跑通。用同一个框架管理不同来源的模型这在生产环境里非常关键。3. 核心API实战从文本对话到结构化输出3.1 ChatLanguageModel的完整能力边界先别急着写业务代码把ChatLanguageModel这个接口的能力边界搞清楚后面会少踩很多坑。除了上一节演示的chat(String)方法它还支持传消息列表包括 SystemMessage、UserMessage、AiMessage这样就能实现带系统提示词的多轮对话还可以传封装好的请求对象把 temperature、topP 这些推理参数一次性带进去。我建议日常场景尽量使用消息列表而不是字符串拼接。举个例子客服场景里系统提示词应该是独立的一条 SystemMessage一旦把它和用户问题拼在一起语义边界就被破坏模型容易把“规则”和“问题”混为一谈。这看起来只是代码组织方式的不同但对回答质量的影响非常大。3.2 PromptTemplate提示词模板不靠字符串拼接业务里写提示词最怕什么怕拼字符串。今天换个变量名明天加个输入字段改起来容易乱还可能引入注入风险。LangChain4j 提供了一套提示词模板机制做法和你熟悉的模板引擎类似PromptTemplate template PromptTemplate.from( 你是客服小助手。请根据以下客户问题回复 客户问题{{question}} 请用友好的语气回答不超过50字。 ); Prompt prompt template.apply(Map.of(question, 你们的退款政策是什么)); String answer model.chat(prompt.text());变量用双大括号包裹调用时通过apply传入一个 Map 完成渲染。好处很明显提示词可以集中管理内容修改不用碰 Java 代码复杂提示词还能挪到资源文件里通过类路径加载让业务代码更干净。3.3 AiServices让Java对象直接和LLM对话如果说模板是“用熟了方便”那 AiServices 就是“用了就回不去”的功能。它的思路是把 LLM 调用伪装成一个普通 Java 接口方法。你定义方法、写注解、声明返回类型框架自动生成实现。看这个例子我要做一个评论情感判断服务interface SentimentAnalyzer { UserMessage( 判断下面评论的情感倾向。 评论{{text}} 只返回 JSON不要解释。 ) SentimentResult analyze(String text); } public record SentimentResult(boolean positive, double confidence, String reason) {}然后一行代码构建代理SentimentAnalyzer analyzer AiServices.builder(SentimentAnalyzer.class) .chatLanguageModel(model) .build(); SentimentResult result analyzer.analyze(你们家东西质量太差了用了三天就坏); System.out.println(result.positive());麻烦的 JSON 解析、字段映射、容错处理全部由框架搞定。你拿到手的直接是一个类型安全的 Java 对象。这个模式在生产里价值极高尤其当你需要从大模型的自由文本输出里提取固定字段的时候比手写正则、手解析 JSON 可靠太多。3.4 ChatMemory多轮对话的记忆管理多轮对话表面上像是“把聊天记录一直带着”实际上涉及窗口管理问题模型上下文有限聊天记录不能无限增长否则早晚超出 token 上限调用直接报错。LangChain4j 的消息记忆组件就是干这个的。interface Assistant { String chat(String userMessage); } ChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build();MessageWindowChatMemory的意思是保留最近 N 条消息超出后最老的消息自动丢弃。20 条大概对应五轮对话左右适合大多数轻量场景。它的实现原理不复杂内部维护一个消息队列每次模型调用前拼上历史消息调用后把新回复追加进去。但正是这个简单的队列省掉了我早期手写聊天历史管理时的一堆 bug。4. 实战Spring Boot智能客服接口4.1 项目结构与配置类组织框架基础了解清楚以后我们做一个完整的可落地案例智能客服接口。整个项目就用 Spring Boot 3.x不额外引入 AI starter手动配置 Bean这样你能看清楚每一层到底发生了什么。先建一个配置类管理模型实例Configuration public class LlmConfig { Value(${ai.model-name}) private String modelName; Value(${ai.api-key}) private String apiKey; Bean ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(apiKey) .modelName(modelName) .temperature(0.3) .timeout(Duration.ofSeconds(30)) .build(); } }application.yml 里对应配置ai: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini这里把 temperature 设为 0.3是因为客服场景要求回答稳定、少发挥。如果后续做文案创作再给另一个 Bean 配高一点的 temperature 就行。不同的业务诉求用不同的模型实例不要所有地方共用一个。4.2 实现同步应答接口业务层定义一个服务组件把提示词和调用逻辑都收敛进去Service public class CustomerService { private final ChatLanguageModel model; public CustomerService(ChatLanguageModel model) { this.model model; } public String answer(String question) { String prompt 你是电商平台客服。回答要简短、礼貌、实用。 用户问题%s .formatted(question); return model.chat(prompt); } }Controller 就是薄薄的一层RestController RequestMapping(/api/customer) public class CustomerController { private final CustomerService customerService; PostMapping(/ask) public MapString, String ask(RequestBody MapString, String body) { String answer customerService.answer(body.get(question)); return Map.of(answer, answer); } }跑起来以后POST 一个{question:怎么退货}接口就能返回一段客服口径的回答。功能跑通后再思考一个问题大模型回答的速度通常在 2 到 10 秒不等如果用户在前端等一个同步接口的完整返回值体验相当糟糕。所以下一个优化点是流式输出。4.3 流式输出长回答的体验救星LangChain4j 里流式能力由StreamingChatLanguageModel提供搭配 Spring MVC 的SseEmitter做 SSE 推流。我封装了一个简化版流程PostMapping(/ask/stream) public SseEmitter askStream(RequestBody MapString, String body) { SseEmitter emitter new SseEmitter(60_000L); StreamingChatLanguageModel streamingModel OpenAiStreamingChatModel.builder() .apiKey(apiKey) .modelName(modelName) .build(); streamingModel.chat(body.get(question), new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { try { emitter.send(token); } catch (IOException e) { throw new RuntimeException(e); } } Override public void onComplete(ResponseAiMessage response) { emitter.complete(); } Override public void onError(Throwable error) { emitter.completeWithError(error); } }); return emitter; }onNext里每收到一个 token 就向前端推送一点用户看到的是“字在蹦出来”等待焦虑感会大幅下降。这里有个小细节SseEmitter 的超时时间要设得比模型回复时间宽裕默认值太短容易导致连接提前断掉。另外生产环境记得对并发和流量做限制流式连接非常占资源别把接口直接裸奔出去。4.4 召回式问答让AI基于业务文档回答客服大多数问题围绕 FAQ 和商品说明展开光靠大模型内置知识回答容易瞎编。这里用一节讲怎么用最朴素的方式做“召回式问答”也就是 RAG 的最简骨架。思路很简单把业务文档拆成小块转成向量存进内存用户提问时拿问题的向量去库里检索相似片段把命中的片段拼进提示词让模型“阅读后回答”。LangChain4j 里这个链路不需要额外部署服务内存实现就够了EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(apiKey) .modelName(text-embedding-3-small) .build(); InMemoryEmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); TextSegment segment TextSegment.from(退货政策签收后7天内可无理由退货。); Embedding embedding embeddingModel.embed(segment).content(); store.add(embedding, segment); Embedding queryEmbedding embeddingModel.embed(我能不能退货).content(); EmbeddedTextSegment hit store.findRelevant(queryEmbedding, 1).get(0); String answer model.chat(根据以下资料回答\n hit.segment().text() \n问题我能不能退货);新手阶段不要一上来就引入 Elasticsearch、pgvector 这些重量级组件。把内存版跑通你就基本理解了检索、召回、生成的完整链路。后续数据量大了再迁移到真正的向量数据库迁移的只是存储层上层逻辑基本不动。5. 参数调优与稳定性保障5.1 temperature和maxTokens回答质量的两个旋钮LangChain4j 的 Builder 里提供了大量可调参数但新手阶段最值得关心的只有两个temperature和maxTokens。temperature 控制随机性取值范围一般是 0 到 2。0 附近输出几乎一模一样适合客服、分类、数据提取1 以上就比较放飞适合文案脑暴。我建议业务默认值设在 0.2 到 0.4如果想要一点变化但不想失控0.7 左右是个不错的折中。不过要提醒一下每个模型的 temperature 映射感受不完全一致同一个值在不同模型上的表现略有差别最终需要你自己多跑几次对比。maxTokens 控制模型最多输出多少个 token直接决定回答长度和单次调用成本。一个汉字粗略估算 2 个 token 左右如果你希望回答不超过 100 字maxTokens 给 256 就足够了不用一上来就写最大值。设得太小回答会被截断设得太大成本浪费这个参数值得根据业务反复测试。5.2 超时、重试和并发生产级的三个硬指标大模型 API 不是本地调用网络抖动、服务端过载都可能导致失败。如果代码不做超时和重试用户体验就是长时间转圈然后报错。Builder 里有个timeout(Duration)方法可以直接设置连接和读取超时我一般给 30 秒到 60 秒。注意本地部署模型第一次加载可能极慢容易误伤超时这种情况要提前预热或者调大超时。重试方面框架自带maxRetries、logRequests、logResponses这些控制参数。日志开关非常建议打开大模型应用排查问题时最需要看到的就是“到底发了什么请求、模型回了什么内容”。并发场景下不要把 model 实例每次 new 一个应该设计成单例复用。底层 HTTP 客户端本身有连接池频繁创建实例反而浪费连接。如果单实例满足不了吞吐优先做业务层限流而不是盲目开更多实例烧钱。5.3 成本意识别让AI把预算吃光大模型调用是按 token 计费的很多人上线时完全没做预算控制月底账单下来才傻眼。我自己的经验是三件事必须做日志里记录每次请求的 token 用量给高频固定问答加一层本地缓存对单用户请求频率做限制。token 用量的读取很简单同步调用拿到的Response对象里带tokenUsage字段流式调用在onComplete里同样能拿到。把这些数据压到日志或者埋点里后续做成本分析才有依据。缓存就更直接了同一问题本就可以命中同一答案用 Caffeine 或者 Redis 缓存结果能省掉一大笔重复调用费用。6. 常见问题与排查技巧实录6.1 新手高频问题速查表现象典型原因解决思路编译报错找不到类依赖版本不一致或缺少对应模型模块检查 langchain4j 核心版本和各模型模块版本是否一致运行时提示 apiKey 不合法环境变量没加载或 key 传错先打印 System.getenv 确认或用配置文件注入返回内容被截断maxTokens 设置偏小调大 maxTokens或让提示词明确要求“简短回答”多次对话后调用失败上下文超过模型 token 上限使用 ChatMemory 并限制 maxMessagesJSON 输出解析不了模型返回了带代码块的文本改用 AiServices 结构化输出或在后处理中去除代码块标记本地模型响应超时模型冷启动、显存不足预热模型、调大 timeout、确认机器配置每次回答都一模一样temperature 设成了 0需要一点随机性就调高到 0.5 以上这张表覆盖了我在社区和实际工作中遇到的大多数入门问题。碰到报错别慌先读日志再对照表里定位。6.2 我踩过的三个最深的坑先说第一个坑JSON 结构化输出。早期我用纯文本提示“请返回 JSON”模型确实返回了 JSON但外层包了 Markdown 的 json 代码块标识。后端用 Jackson 直接解析直接炸报错信息又长又晦涩。后来改用 AiServices 结构化输出问题彻底消失。如果非要自己解析记得先正则去掉代码块标记。第二个坑是对话记忆的无序膨胀。有一次我把聊天记录全量存在内存里聊到二十多轮之后一次请求的 token 量已经冲到了几万成本飙升之外还经常触发服务端的上下文长度限制。换成MessageWindowChatMemory并把窗口限制到 20 条以后问题立刻缓解。第三个坑是本地模型的冷启动。我用 Ollama 跑 7B 模型第一次调用等了 40 多秒程序配的 10 秒超时直接抛超时异常。第二次学乖了应用启动后主动发一个“你好”预热再把超时调到 60 秒。新手如果也跑本地模型这个情况大概率会遇上提前预防比临时排查舒服得多。6.3 快速定位问题的三个方法论最后分享三个排查手段它们救了我无数次。一是打开日志。logRequests()和logResponses()是框架提供的两大神器打开后你能看到请求体里的每一条消息、响应体里的完整输出。排查“模型为什么不听话”这类问题第一件事就是看日志不要靠猜。二是用最小样例复现。遇到奇怪行为不要在整个业务代码里打日志单独写一个十几行的 main 方法复现。模型输出不确定但你能把问题的边界缩小是提示词的问题、参数的问题还是框架本身的问题。三是跑确定性测试对拍。如果一个功能要求稳定输出把 temperature 调到 0 后多跑几次看结果是否稳定。如果 0 温度下仍然不稳定那问题多半出在提示词本身有歧义或者检索增强环节返回的上下文不稳定。最后补充一点我现在的工作习惯任何新能力我都会先用最小 demo 验一遍再往项目里放。LangChain4j 最大的价值不是某个 API 多好用而是它把大模型应用开发里那些重复的、容易出错的环节收敛成了可复用的抽象。对于新手我建议的路径是先跑通model.chat()感受一次调用再学 AiServices 做结构化输出这能解决 70% 的业务需求真正做问答系统时再碰 RAG。别一上来就把所有模块都铺开铺太开往往什么都学不深。版本更新这块我多说一句LangChain4j 迭代很快今天写的 0.35.0 写法过两三个版本可能就有新推荐。好在这套核心抽象一直稳定只要理解了 ChatLanguageModel、AiServices、ChatMemory 这三个概念迁移成本就很低。上手过程中遇到具体报错优先去看官方 GitHub 的 issue 和更新日志比任何二手教程都准确。希望这篇实战记录能帮你少走我走过的弯路用 Java 也能把大模型玩得顺手。