Java工程师的LLM应用开发实战:LangChain4j入门与避坑指南

发布时间:2026/10/22 21:51:27
Java工程师的LLM应用开发实战:LangChain4j入门与避坑指南 Java 后端做 LLM 应用这件事过去两年我一直处于一种“看 Python 那边热闹自己这边没趁手兵器”的状态。直到把 LangChain4j 完整跑下来几个项目才确定这确实是 Java 生态里最值得投入的一条路。这篇不是官方文档翻译是我从零上手 LangChain4j 的真实笔记包含版本选型、可直接跑的代码、以及我踩进去又爬出来的五个坑。适合刚接触 LLM 应用开发、想在自己的 Spring 项目里加 AI 能力、或者单纯不想写一堆 Python 胶水代码的 Java 工程师。1. Java 开发者做 LLM 应用为什么绕不开 LangChain4j1.1 Python 圈的繁荣与 Java 圈的窘境我见过太多团队做 AI 功能时的路线争执后端明明是 Java 技术栈一碰到大模型就默认要开一个 Python 微服务把 Prompt 拼接、文档切分、向量检索全塞进去Java 这边只负责转发 HTTP 请求。这么做不是不行但代价是两套语言、两套部署流程、两批维护人员中间只要业务一改沟通成本立刻翻倍。Python 生态的优势是框架多、更新快但它的繁荣有一部分是靠“每天冒出新框架”堆出来的对 Java 团队来说学习与迁移成本被放大了。Java 这边虽然起步晚却一直缺一个真正理解 Java 开发习惯的 LLM 框架。后来我找到 LangChain4j试用第一个星期就意识到这才是 Java 里该有的样子。它把模型调用、消息历史、结构化输出、工具调用、文档检索这些能力全部封装成 Java 工程师熟悉的接口、注解和 Builder 模式。对于一个写过多年 Spring 的人来说这个东西的学习曲线比想象中平滑得多。1.2 LangChain4j 到底解决了什么问题LangChain4j 解决的核心问题不是“帮你调一个模型”而是“帮你把模型集成进 Java 业务代码”。举个例子普通方式调用大模型就是发一个 HTTP 请求拿回一段文本但真实业务需要的是把用户输入、历史对话、检索到的资料、当前想调用的业务方法组合起来让模型理解上下文再把它输出的文本转成 Java 对象必要时还要触发你系统里的某个方法。这些事如果全自己做我要写很多模板代码维护消息列表、处理工具调用的循环、解析模型输出的 JSON、处理重试…… LangChain4j 把这些流程抽象成了几个核心组件用 Java 的方式组织起来我可以专注于业务本身。1.3 什么样的人最需要它我自己判断下面三类团队最适合作技术选型时考虑 LangChain4j已有成熟 Java 后端想快速给现有业务加上问答、总结、抽取类 AI 能力不想引入第二套技术栈。团队里 Python 经验薄弱但 Java 功底扎实希望 LLM 应用的代码能被现有体系完成 Code Review 和测试。需要把模型能力和业务逻辑紧密耦合比如让模型调用内部服务、查询数据库、调用领域方法而不是简单地“问一句答一句”。反过来如果项目只是做一个纯实验性 Demo或者团队本身 Python 很强那用什么都行不必迁就 Java。选型这件事核心还是看现有资产。2. 环境准备与第一个能对话的 main 方法2.1 依赖版本选择的一个大坑LangChain4j 的版本演进速度很快0.31 到 0.35 之间就有多处破坏性变更类被重命名、模块被拆分都是常事。我见过不少人在 Maven 里直接写最新版本第二天一编译突然报一堆找不到类最后只能整个工程回滚。我的建议是优先选择稳定的 release 版本不要追最新快照。初始引入时参考官方文档当前主推的版本号并在 pom 里显式锁定。properties langchain4j.version0.35.0/langchain4j.version /properties dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency /dependencies这里说句实话langchain4j-open-ai这个名字虽然带了某家服务商的标签但它对接的是业内通用的聊天补全协议国内很多模型服务商都提供兼容接口所以这一套依赖关系适配性很广。如果你用的是别家的模型服务商找到对应模块即可思考方式完全一致。另外要注意某些模块是单独发布的比如langchain4j-easy-rag、langchain4j-embeddings需要按需引入别指望核心包把所有能力都带进来。2.2 API Key 与基础地址的配置细节很多新手上来就把 API Key 写死在代码里这绝对是要改掉的坏习惯。不要因为本地测试图省事而硬编码密钥否则代码一旦被提交到仓库泄露只是时间问题。我习惯用环境变量注入LLM_API_KEY你的key LLM_BASE_URLhttps://你的服务商地址 LLM_MODEL_NAME你购买的模型标识代码里这样读取String apiKey System.getenv(LLM_API_KEY); String baseUrl System.getenv(LLM_BASE_URL); String modelName System.getenv(LLM_MODEL_NAME);还有一点容易被忽略不同服务商的兼容层并不保证所有参数都生效。比如有些平台不认temperature以外的采样参数传了也不报错但实际没起作用。先保持默认值跑通链路再逐步调参数这样排障范围会小很多。2.3 第一个能对话的 main 方法引入依赖、配好环境变量后可以直接写一个最简入口验证连通性import dev.langchain4j.model.chat.ChatLanguageModel; public class FirstDemo { public static void main(String[] args) { String apiKey System.getenv(LLM_API_KEY); String baseUrl System.getenv(LLM_BASE_URL); String modelName System.getenv(LLM_MODEL_NAME); // 实际类型取决于你选择的模型插件Builder 的写法大方向一致 ChatLanguageModel model ChatLanguageModel.builder() .apiKey(apiKey) .baseUrl(baseUrl) .modelName(modelName) .temperature(0.7) .build(); String answer model.generate(用一句话介绍你自己并说明你的知识截止时间。); System.out.println(answer); } }这段代码跑通说明依赖、密钥、网络链路都没问题。第一次调用通常会比后续慢一些这是正常的因为存在连接建立和鉴权的过程。2.4 超时与重试第一个容易被忽略的配置对话类接口的延迟波动非常大高峰期可能从几百毫秒飙到几十秒。LangChain4j 默认超时往往不够用业务场景下建议主动配置ChatLanguageModel model ChatLanguageModel.builder() .apiKey(apiKey) .baseUrl(baseUrl) .modelName(modelName) .timeout(Duration.ofSeconds(60)) .maxRetries(2) .build();我的建议是超时不要低于 30 秒重试次数控制在 1 到 2 次。重试逻辑只适用于“连接类错误”和“限流类错误”对于模型正常返回但因内容太长被截断的情况重试没有意义。3. 把模型输出的“乱话”变成能入库的数据结构化输出实战3.1 为什么不能直接让模型返回 JSON 然后硬解析我刚开始做 LLM 应用时犯过一个典型错误让模型“返回一个 JSON”然后在 Java 里用 Jackson 硬解析。上线前测试一切正常上线后才发现模型偶尔会在 JSON 前面加一句“好的这是你要的结果”后面再跟 JSON甚至中途插入注释。硬解析当场失败用户收到一片错误提示。这不是模型笨而是自然语言输出天然不稳定。指望模型输出严格遵守格式不如在框架层面做约束。LangChain4j 的解法是通过消息类型和输出格式配置把“结构化返回”做成模型侧的协议约束再由框架把返回内容映射成 Java 对象。实践下来成功率比“直接在 Prompt 里说请返回 JSON”高不少而且代码更干净。3.2 Java 里结构化输出的正确打开方式核心思路是定义好 Java POJO然后让框架根据 POJO 的结构去约束模型输出而不是自己拼 JSON Schema。假设业务场景是从用户反馈中抽取信息定义一个数据类import dev.langchain4j.model.output.structured.StructuredOutput; public class FeedbackItem { private String productName; private Integer rating; private String problemType; private String comment; // getter / setter 必须齐全否则 JSON 映射会失败 }调用侧把输出格式告诉模型String userText 京东买的机械键盘用了三天回车键就失灵了售后倒是挺好但质量真的不咋地。; FeedbackItem item model.generate( userText, StructuredOutput.forType(FeedbackItem.class) ); System.out.println(item.getProductName()); System.out.println(item.getRating()); System.out.println(item.getProblemType());这里有一个容易忽略的细节FeedbackItem必须提供完整的空构造器和字段对应的 getter/setter。Java 的反射机制对字段访问方式很敏感字段是 private 且没有公开读方法结果是静默返回 null而且不会报错。3.3 抽取结果不达标时的兜底策略即使框架做了约束模型仍可能在极端情况下返回不符合预期的内容。我的项目里一般叠加三层兜底基础校验对关键字段做空值判断和长度判断凡是缺少必填字段的直接标记为解析失败不能放行。单次重试把上一轮的结果作为“错误示例”反馈给模型让它重新生成。重试一次的性价比最高三次以上边际收益很低。业务降级如果重试仍失败进入人工处理队列或返回模糊结果而不是给用户一个崩溃异常。结构化输出是 LLM 应用从“玩具”走向“生产资料”的关键一步但它不是银弹。模型依然可能“编造”字段尤其是 rating 这类数字字段所以重要数据必须做范围校验。4. 让 Java 方法变成模型能调用的“工具”AiServices 与 Tool 实战4.1 AiServices 是什么AiServices 是 LangChain4j 里一个很核心的抽象。它允许你定义一个普通的 Java 接口框架自动帮你生成代理实现把模型的对话能力与你的业务方法绑定。打个比方普通的方式是你和模型一问一答而有了 AiServices你的系统里就像多了一个“会调用内部系统的实习生”。用户说“帮我查一下订单 S10023 的物流状态”模型读到这句话自己决定调用你的logisticsQuery()方法拿到结果后再组织语言回答用户。它的价值在于工具调用、消息组装、结果整理这些脏活全部由框架完成你只需要写业务方法。4.2 定义工具类的三个硬性要求想让 Java 方法被模型识别并调用工具类必须满足三个条件少一个模型就“看不见”你的方法方法必须是public。方法必须用工具描述注解说明用途描述越清晰模型越容易在恰当场景下调用它。每个参数都必须有明确的说明注解模型需要知道每个参数代表什么。一个最简示例import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolParameter; public class OrderTools { Tool(根据订单号查询物流状态) public String queryLogistics( ToolParameter(订单号格式为 S 加数字) String orderId) { // 这里可以是 HTTP 调用、数据库查询或本地缓存 return 订单 orderId 已发货当前到达中转站。; } }工具类本身不一定要注册成 Spring Bean但如果你想让它被自动扫描推荐配合组件扫描注解一起使用否则手动装配时很容易漏掉。4.3 一个完整的智能助手雏形定义一个服务接口import dev.langchain4j.service.AiServices; public interface Assistant { String chat(String userMessage); }在代码中组装Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderTools()) .build(); String answer assistant.chat(帮我查一下订单 S10023 到哪里了); System.out.println(answer);运行后你会发现模型先解析出意图选择调用queryLogistics拿到结果再生成回答。整个过程不需要你自己写“判断该调哪个方法”的逻辑。4.4 工具调用的死循环问题工具调用最常见的坑是死循环模型不断调用同一个工具或者两个工具互相调用直到资源耗尽。原因是模型在特定场景下会陷入“反复确认”的状态比如一次查询返回结果不明确它就试第二次、第三次。LangChain4j 提供了最大迭代次数控制Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderTools()) .maxIterations(3) .build();实际经验是 3 到 5 次比较合适。小于 3 次可能导致正常的多步骤任务做不完大于 5 次则在异常情况下拖慢接口响应。同时建议在工具方法的返回值里包含“查询完成”这类明确状态词帮助模型判断任务是否已经结束。5. 给模型装上专属知识库RAG 流程的最小实现5.1 RAG 的本质开卷考试带书进考场RAG检索增强生成这个词听起来很高深本质却很简单模型本身的知识有截止时间回答不了你私有文档里的内容。RAG 的做法是先把你的文档切块、向量化并存储用户提问时把与问题最相关的几段内容检索出来拼进 Prompt再让模型阅读这些资料后回答。它相当于一场开卷考试模型不再凭空背答案而是翻着指定参考书作答。这样既能回答私有知识又能在一定程度上减少“胡编乱造”。5.2 最小视觉下的组件选型LangChain4j 的 RAG 流程涉及几个组件逐个说明文档加载器把 txt、md、pdf 等文件读成文本。文档分割器把长文档切成有语义边界的小块官方内置了按字符数、按段落切分的实现。嵌入模型把文本转成向量。它决定了“语义相似”判断的质量。向量存储保存向量并支持相似度检索。测试阶段可以用内存实现生产环境再换成独立的向量数据库。选型的优先级是先跑通内存版流程再考虑生产部署。不要在第一步就引入分布式向量数据库学习成本会淹没你对核心流程的理解。5.3 最少代码的“文档问答”实现先把一份文档灌入向量存储import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.TextFileDocumentLoader; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.model.embedding.EmbeddingModel; Path path Path.of(company_manual.txt); Document doc TextFileDocumentLoader.load(path); EmbeddingModel embeddingModel ...; // 根据你的模型插件初始化 EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); // 切分、向量化、入库 for (TextSegment segment : DocumentSplitter.paragraphs(300).split(doc)) { var vector embeddingModel.embed(segment).content(); store.add(vector, segment); }然后创建带检索能力的 AiServicesAssistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(ContentRetriever.from(embeddingModel, store)) .build(); String answer assistant.chat(公司请假流程是什么); System.out.println(answer);注意这里DocumentSplitter.paragraphs(300)指的是切块粒度300 个字符左右在检索精度和上下文完整度之间比较平衡。切太细语义碎片化切太粗检索命中不精准。5.4 查不到相关文档时代码永远比资料诚实RAG 效果差绝大多数问题出在召回环节而不是模型本身。症状是模型回答得冠冕堂皇但内容其实是泛泛而谈根本没引用你的文档。排查思路比较朴素把最终发给模型的 Prompt 打印出来看看检索出来的片段到底是哪几段。如果片段相关度低优先调整切分策略和召回数量如果片段本身是对的但模型没用好再考虑调整 Prompt 指令。RAG 是一个工程链路真正排障时不能只看最终输出要一层层看数据流转。6. 我把新手期最容易踩的五个坑都踩了一遍6.1 Java 泛型擦除让结构化输出静默失败泛型擦除是 Java 老话题在 LangChain4j 里却特别阴险。如果你用ListFeedbackItem作为结构化输出类型因为泛型擦除框架拿不到FeedbackItem的类型信息最终返回的可能是一堆 Map或者直接解析失败。解决方法是避免在顶层类使用泛型字段。非要返回列表时定义一个包装类public class FeedbackListResponse { private ListFeedbackItem items; // getter / setter }这样反射可以拿到完整类型。这个坑的麻烦之处在于它不一定报错而是静默返回错误结构只能靠断点排查。6.2 HttpClient 复用问题导致连接耗尽早期我图省事每次调用模型都重新创建 HTTP 客户端低并发时没感觉稍微上点压力就报连接池耗尽。LangChain4j 的模型对象本身就持有 HTTP 客户端正确做法是把它设计成单例重用不要每次请求都新建。Spring 场景下把模型声明为 Bean 就不容易踩这个坑。连接池参数也要关注尤其在网关型应用中默认连接数可能远远不够。6.3 把计费 Token 和上下文窗口混为一谈这是成本失控最常见的原因。计费 Token 是模型实际处理的 token 总量包括输入和输出上下文窗口是模型最多能处理的 token 数量。两者不同你可以在窗口内塞很多东西但成本按实际传入量计算。很多人做 RAG 时为了“多给一点资料”把窗口塞得满满当当结果发现成本飙升响应速度还变慢。建议对检索片段做数量限制比如默认检索 3 到 5 段而不是一股脑全塞进去。日志中可以记录每次请求的 token 用量定期检查成本分布数据会告诉你哪些 Prompt 塞得太多。6.4 日志与异常信息里的敏感内容泄露LLM 应用的日志比普通应用更敏感因为用户输入的内容会被原样发送给模型服务商日志里如果打印请求体等于把所有原始输入记录下来。更危险的是把 API Key 打出来。某种默认配置下异常堆栈可能包含完整请求头里面就有鉴权信息。我给项目定的规矩是日志屏蔽所有鉴权字段统一输出脱敏标记。只打印 token 用量、耗时、模型名称等元信息不打印输入原文。测试环境可以开完整日志生产环境强制关闭请求体打印。这类问题不出事则已出事就是合规事故。6.5 小版本升级后的破坏性变更LangChain4j 的快速迭代既是优点也是风险。我经历过一次升级模块名变了类名变了连 Builder 的字段名都变了几十个文件的编译错误排了一整天。给所有团队的升级建议是升级前先读发布说明和迁移指南。把项目分为“依赖升级”和“代码适配”两步不要混在一起改。保留一个只做冒烟测试的回归用例集覆盖对话、结构化输出、工具调用、RAG 四条主链路。生产环境不要追求最新版选择已经稳定运行两周以上的版本。框架的新功能确实吸引人但在生产项目里稳定压倒一切。最后说一点个人的体会把 LangChain4j 这些核心能力跑通之后我发现它的学习路径其实很清晰先跑通一次普通对话理解模型对象怎么配置再做一次结构化输出体会约束和类型转换的价值接着引入工具调用让模型真正触达业务代码最后按需叠加 RAG。四步走完绝大多数日常 AI 功能都能覆盖了。我给新手唯一的建议是不要一上来就搭完整架构先用最小的 main 方法跑起来再逐步加组件。每个环节都亲手打印输出、观察数据流转比读十遍文档都有用。Java 生态做大模型应用开发虽然比 Python 冷清一些但 LangChain4j 提供的这些抽象恰恰贴合 Java 工程化的习惯。工具链成熟度在快速追赶现在入场正是时候。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询