Java开发者如何用LangChain4j快速集成大模型能力

发布时间:2026/10/22 16:17:24
Java开发者如何用LangChain4j快速集成大模型能力
1. 为什么 Java 开发者值得花时间上手 LangChain4j1.1 从一个真实痛点说起我在几个不同的团队里都遇到过类似的场景后端服务是清一色的 Java 技术栈Spring Boot 跑得稳稳当当结果产品那边突然提了个需求要接入大模型做智能问答、文档摘要或者工单自动分类。团队里没人写过 Python也不打算为了一个功能点专门维护一套 Python 服务于是问题就变成了——Java 生态里到底有没有一套趁手的工具能把大模型能力像调用普通 SDK 一样接进来LangChain4j 就是在这个背景下进入我视野的。它不是 Python 版 LangChain 的简单翻译而是一套从零开始为 Java 设计的 LLM 应用开发框架。核心定位很清晰把「和大模型打交道」这件事抽象成 Java 开发者熟悉的接口、注解和构建器模式让你用写 Service 层代码的思路去写 AI 应用。它解决的核心问题是集成成本——不用手搓 HTTP 请求、不用自己管理对话历史、不用为每个模型厂商写一套适配代码。这篇文章适合谁看如果你是有 Spring Boot 基础、想快速把大模型能力落到业务里的 Java 后端那这篇就是给你写的。如果你完全没接触过 LLM 应用开发也没关系我会把每个概念用生活化的方式讲清楚。整篇内容基于我自己的实操经验包含完整的依赖配置、代码示例、参数计算和踩坑记录你可以直接抄作业。1.2 LangChain4j 到底解决了哪些具体问题在它出现之前Java 接大模型通常有三种做法。第一种是直接用 HttpClient 调 REST API灵活但重复劳动多光是处理流式响应和重试就够写一堆工具类。第二种是用各家厂商自己的 Java SDK问题是换一个模型供应商就得重写一遍调用逻辑。第三种是团队自己封装一层但封装质量参差不齐对话记忆、工具调用这些高级能力基本靠手写。LangChain4j 的价值在于它把这几层都标准化了。它提供了统一的ChatLanguageModel接口底层可以是不同厂商的模型上层业务代码几乎不用改。它内置了ChatMemory做对话历史管理内置了EmbeddingStore做向量存储抽象还提供了AiServices这种声明式的方式让你定义一个接口、加几个注解框架自动帮你生成实现。这种设计思路和 Spring Data JPA 的 Repository 很像——你声明意图框架负责实现。从影响范围看它覆盖的场景相当广智能客服、RAG 知识库问答、文档处理、代码辅助、结构化信息抽取、多轮对话机器人基本都能找到对应的模块。而且它对 Spring Boot 有一等公民级别的支持有专门的 starter配置写在 application.yml 里就行这对 Java 团队来说几乎没有学习门槛。2. 环境准备与依赖选型的关键决策2.1 JDK 版本和构建工具怎么选LangChain4j 对 JDK 的最低要求是 17这一点必须先确认。我实测过在 JDK 11 上跑编译阶段就会因为用到 record、sealed 等特性而报错。如果你所在的项目还停留在 JDK 8 或 11要么升级要么就别考虑这个框架了硬凑只会浪费时间。JDK 17 是目前的稳妥选择JDK 21 的虚拟线程在处理大量并发 LLM 调用时会有额外收益但不是必须。构建工具用 Maven 或 Gradle 都行官方文档两边都有示例。我个人偏向 Maven因为 LangChain4j 的模块划分很细Maven 的依赖树看起来更直观排查版本冲突时方便。下面是一个典型的 Maven 依赖配置我把它拆成了几个部分说明。properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target langchain4j.version0.35.0/langchain4j.version /properties dependencies !-- 核心模块包含 ChatLanguageModel 等基础接口 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- 具体模型厂商的适配模块按需引入 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- Spring Boot 集成 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency /dependencies这里有个关键点核心模块和适配模块是分开的。langchain4j只包含接口和抽象具体的模型实现要靠langchain4j-open-ai这类适配包。这种设计的好处是你换模型供应商时只需要换适配包业务代码不动。坏处是新手容易漏引依赖然后对着ClassNotFoundException发懵。2.2 模型接入方式的选择逻辑接入模型有两条路一是调用云端 API二是本地部署模型。这两条路的取舍直接决定了你的架构。云端 API 的优点是开箱即用模型能力强不用关心显卡和显存。缺点是数据要出你的服务器有隐私合规要求的场景要慎重另外按 token 计费量大时成本要算清楚。本地部署的优点是数据不出内网调用不计费缺点是硬件门槛高7B 参数的模型至少需要 8GB 显存才能跑得比较流畅而且推理速度受硬件限制。我的建议是分阶段来。开发调试阶段用云端 API快速验证业务逻辑上线前根据数据敏感度和调用量决定最终方案。LangChain4j 的好处是这两条路它都支持本地部署可以通过兼容接口的方式接入切换时上层代码基本不用改。注意无论选哪种方式API Key 这类敏感信息绝对不要硬编码在代码里用环境变量或配置中心管理。我见过有人把 Key 提交到代码仓库结果被扫出来盗用账单直接爆掉。2.3 项目结构规划建议在动手写代码之前先把包结构规划好后面扩展会省很多事。我习惯按职责分层config包放模型、记忆、向量库的配置类service包放 AiServices 接口和业务服务controller包放对外接口model包放请求响应的 DTOtools包放需要暴露给模型调用的工具类这个结构不是强制的但遵循它能让你的代码在功能变多之后依然清晰。特别是当你要接入 RAG、工具调用、多模型路由时分层清晰的项目会好维护得多。3. 核心概念拆解与第一个可运行示例3.1 ChatLanguageModel一切调用的起点ChatLanguageModel是整个框架最基础的接口你可以把它理解成一个「会聊天的对象」。它最常用的方法就两个generate(String)接收一个字符串返回字符串generate(ListChatMessage)接收消息列表返回ResponseAiMessage。前者适合简单场景后者适合需要区分角色系统、用户、AI的复杂场景。为什么要有消息列表这个概念因为大模型的对话是有角色区分的。系统消息SystemMessage用来设定模型的角色和行为边界用户消息UserMessage是用户的输入AI 消息AiMessage是模型的回复。把这三者分开管理模型才能理解上下文。这就像开会时你要先说明「今天讨论什么议题」再让各方发言而不是把所有话混在一起说。下面是一个最小可运行的示例我加了详细注释import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.chat.ChatLanguageModel; public class FirstDemo { public static void main(String[] args) { // 从环境变量读取 Key不要硬编码 String apiKey System.getenv(OPENAI_API_KEY); ChatLanguageModel model OpenAiChatModel.builder() .apiKey(apiKey) .modelName(gpt-4o-mini) // 模型名称按实际可用填写 .temperature(0.7) // 随机性0 最确定1 最发散 .timeout(Duration.ofSeconds(60)) .build(); String answer model.generate(用一句话解释什么是向量数据库); System.out.println(answer); } }这段代码跑通说明你的环境和依赖都没问题。如果报错八成是 Key 没配好或者网络不通先排查这两个。3.2 参数背后的含义temperature 和 maxTokens很多人调模型时参数是抄来的不知道为什么要这么设。我解释两个最关键的。temperature控制输出的随机性。它的原理是在模型输出下一个词时对概率分布做缩放。温度趋近 0模型几乎总是选概率最高的词输出稳定但可能死板温度升高低概率词也有机会被选中输出多样但可能跑偏。做信息抽取、分类这种要求确定性的任务设 0 到 0.3做创意写作、头脑风暴设 0.7 到 1.0。我一般默认 0.7具体任务再调。maxTokens限制单次回复的最大长度。这里有个容易踩的坑它限制的是输出的 token 数不是输入。而且不同模型的 token 和字符换算比例不一样中文大致是 1 个汉字对应 1 到 2 个 token。如果你设了 100模型回复到一半被截断别以为是模型坏了是参数设小了。反过来设太大也没用因为模型本身有上下文窗口上限输入加输出不能超过这个上限。3.3 用 AiServices 声明式地写业务逻辑直接调ChatLanguageModel适合简单场景但真实业务里你要处理对话历史、要注入系统提示词、要解析结构化输出手写会很啰嗦。AiServices就是来解决这个问题的。它的用法是定义一个 Java 接口用注解描述行为框架自动生成实现。这有点像 MyBatis 的 Mapper——你只写接口SQL 映射交给框架。下面是一个例子import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.AiServices; interface Assistant { SystemMessage(你是一个专业的 Java 技术顾问回答要简洁准确) String chat(UserMessage String userMessage); } // 构建 Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .build(); String reply assistant.chat(解释一下什么是依赖注入);SystemMessage定义的角色设定会作为系统消息自动注入UserMessage标注的参数会作为用户消息。框架帮你把消息组装、调用、解析结果这一整套流程都封装了。你写的是接口用的是对象这就是声明式编程的威力。4. 对话记忆与多轮交互的实现细节4.1 为什么需要 ChatMemory大模型本身是无状态的它不记得你上一句说了什么。每次调用都是独立的你发过去的消息列表就是它全部的「记忆」。所以多轮对话的本质是每次调用时把之前的对话历史一起发过去。手动维护这个历史列表很麻烦要控制长度、要处理截断、要区分角色。ChatMemory就是干这个的。它提供了add添加消息、messages获取历史、clear清空这几个操作。最常用的是MessageWindowChatMemory它按消息条数保留最近的 N 条超出就丢弃最旧的。为什么按条数而不是按 token 数因为按 token 数需要精确计算实现复杂按条数简单直接对大多数场景够用。但要注意如果单条消息特别长按条数保留可能还是会超出模型上下文窗口这时候就需要更精细的策略。4.2 给 Assistant 加上记忆能力把记忆接进 AiServices 很简单构建时加一个chatMemory就行ChatMemory memory MessageWindowChatMemory.withMaxMessages(20); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .build(); assistant.chat(我叫小明); assistant.chat(我叫什么名字); // 能正确回答小明这里withMaxMessages(20)表示保留最近 20 条消息。为什么是 20这是个经验值。太少模型记不住上下文太多每次请求的 token 消耗大成本高而且可能超出上下文窗口。20 条大约对应 10 轮对话对大多数客服、助手场景够用。如果你的场景需要更长的记忆可以考虑用向量存储做长期记忆把历史消息向量化后按相关性检索而不是全量塞进上下文。4.3 记忆隔离多用户场景的必答题单用户场景下一个 Assistant 实例配一个 Memory 没问题。但如果是多用户比如一个 Web 服务同时服务很多用户就绝对不能共用一个 Memory否则 A 用户的对话会被 B 用户看到这是严重的数据泄露。正确的做法是按用户 ID 隔离 Memory。常见方案是用ChatMemoryStore配合一个 Map 或者数据库每个用户 ID 对应一份记忆。LangChain4j 提供了ChatMemoryProvider可以根据内存 ID 动态提供记忆ChatMemoryProvider provider memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(provider) .build(); // 调用时传入 memoryId 区分用户 assistant.chat(user-001, 你好); assistant.chat(user-002, 你好);memoryId就是用户标识框架会根据它去找对应的记忆。生产环境里ChatMemoryStore建议用 Redis 或数据库实现这样服务重启后记忆不丢多实例部署时也能共享。提示记忆持久化时要注意数据清理策略。用户注销、会话过期后对应的记忆要及时删除否则存储会无限膨胀还可能违反数据留存的相关要求。5. RAG 知识库问答的完整落地流程5.1 RAG 要解决的核心问题大模型有两个硬伤一是知识有截止日期训练之后的新信息它不知道二是它不知道你公司内部的私有文档。RAG检索增强生成就是来解决这个的。思路很直白把私有文档切块、向量化、存起来用户提问时先检索出最相关的几块连同问题一起发给模型让模型基于这些材料回答。这个流程听起来简单但每一步都有讲究。切块大小、向量模型选择、检索条数、相似度阈值任何一个参数没调好效果都会大打折扣。我见过不少人搭完 RAG 发现答非所问八成是检索环节出了问题而不是模型不行。5.2 文档加载与切分策略LangChain4j 提供了DocumentLoader来加载各种格式的文档PDF、Word、文本都支持。加载进来的是Document对象包含文本内容和元数据。接下来是切分这一步最关键。为什么要切分因为模型上下文窗口有限不可能把整本书塞进去。而且检索时粒度太粗会引入无关信息粒度太细又会丢失上下文。常见的切分策略是按固定字符数切配合重叠区。比如每块 500 字符相邻块重叠 50 字符。重叠是为了避免一个完整的句子被从中间切断导致语义不完整。DocumentSplitter splitter DocumentSplitters.recursive(500, 50); ListDocument documents FileSystemDocumentLoader.loadDocuments(/path/to/docs); ListTextSegment segments splitter.splitAll(documents);recursive的意思是它会优先按段落切段落太长再按句子切句子还长才按字符切。这种递归策略比单纯按字符切效果好得多因为它尽量保持了语义单元的完整。500 和 50 这两个数字不是金科玉律中文文档我一般用 300 到 500 字符一块英文可以大一些。具体要拿你的文档实测看检索效果调整。5.3 向量化与存储的选型切分完的文本块要转成向量才能做相似度检索。向量化用的是 Embedding 模型它把文本映射成一个高维浮点数数组语义相近的文本在向量空间里距离也近。选 Embedding 模型主要看两点一是支持的语言中文场景要选中文效果好的二是维度维度越高表达能力越强但存储和计算成本也越高。存储向量的地方叫向量库。LangChain4j 支持很多种从内存版到专业的向量数据库都有。开发阶段用InMemoryEmbeddingStore就够了重启数据丢失但调试方便。生产环境建议用专业向量库它们支持持久化、支持大规模检索、有索引优化。EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(apiKey) .modelName(text-embedding-3-small) .build(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(documents);EmbeddingStoreIngestor把切分、向量化、存储三步串起来了一行ingest搞定。这个设计很贴心省得你手动循环处理每个文本块。5.4 检索与生成的组装检索环节的核心是EmbeddingStoreContentRetriever它负责把用户问题向量化然后在向量库里找最相似的几块。关键参数是maxResults返回几块和minScore相似度阈值。maxResults设多少太少可能漏掉关键信息太多会引入噪声还增加 token 消耗。我一般从 3 到 5 开始试。minScore是过滤低质量结果的设太高会检索不到设太低会引入无关内容。这个值跟 Embedding 模型有关需要实测确定通常 0.6 到 0.8 之间。ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build();把 retriever 接进 AiServices 后每次提问框架会自动检索相关文档拼进提示词再发给模型。你调用assistant.chat(问题)时背后已经完成了「检索 增强 生成」的全流程。6. 工具调用与结构化输出的实战技巧6.1 让模型调用你的 Java 方法工具调用Function Calling是让模型从「只会说」变成「能做事」的关键。原理是你把一些 Java 方法暴露给模型模型在需要时返回一个「我要调用某个方法参数是这些」的指令框架执行方法后把结果回传给模型模型再基于结果生成最终回复。LangChain4j 里用Tool注解标记方法用AiServices的tools方法注册public class WeatherTool { Tool(查询指定城市的天气) public String getWeather(P(城市名称) String city) { // 实际调用天气 API return city 今天晴25度; } } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTool()) .build(); assistant.chat(北京天气怎么样);Tool的描述很重要模型靠它判断什么时候该调用这个方法。描述要写清楚功能参数用P标注含义。描述写得含糊模型可能该调的时候不调或者不该调的时候乱调。注意工具方法的返回值会作为消息回传给模型所以返回内容要简洁别返回一大堆无关数据否则会挤占上下文。另外工具方法里如果有副作用比如写数据库要做好幂等和权限校验因为模型可能重复调用。6.2 结构化输出的两种实现方式很多时候你要的不是一段自然语言而是结构化的数据比如从简历里抽取姓名、电话、技能。LangChain4j 支持两种方式。第一种是让模型返回 JSON然后自己解析。这种方式灵活但容易出错因为模型可能返回格式不对的 JSON。第二种是用AiServices的返回类型自动映射你定义一个 POJO框架负责让模型按这个结构输出并反序列化record PersonInfo(String name, String phone, ListString skills) {} interface InfoExtractor { UserMessage(从以下文本抽取信息{{it}}) PersonInfo extract(String text); }框架会自动在提示词里加入格式说明并把模型返回的 JSON 映射成PersonInfo对象。这比手动解析省心得多而且类型安全。实测下来对于字段不多的结构准确率相当高字段特别多或者嵌套很深时可能需要拆成多次调用。6.3 提示词工程的几个实用原则提示词写得好不好直接决定输出质量。我总结了几个实操中验证有效的原则。第一角色设定要具体。「你是一个助手」不如「你是一个有十年经验的 Java 架构师擅长性能优化」。角色越具体模型的输出越贴合预期。第二给例子比讲道理管用。与其描述「输出要简洁」不如直接给一个输入输出的示例。模型模仿示例的能力很强这叫少样本提示。第三明确输出格式。要 JSON 就写清楚字段名和类型要列表就说明用短横线还是数字。格式要求越明确解析越省事。第四把约束条件写全。比如「如果信息不存在返回 null不要编造」这种边界情况的说明能显著减少幻觉。7. 常见问题排查与避坑经验实录7.1 高频问题速查表问题现象可能原因排查方向启动报 ClassNotFoundException适配包没引检查是否引入了对应厂商的依赖调用超时网络问题或 timeout 设太短加大 timeout检查网络连通性回复被截断maxTokens 设太小调大 maxTokens多轮对话记不住没配 ChatMemory 或 memoryId 不对检查记忆配置和用户标识RAG 答非所问检索质量差调整切分大小、maxResults、minScore工具不被调用Tool 描述不清优化描述明确使用场景结构化输出解析失败模型返回格式不符优化提示词或改用自动映射7.2 几个我踩过的坑第一个坑是上下文窗口溢出。有次做长文档问答把整篇文档塞进提示词结果报错说超出 token 限制。后来改成 RAG 检索只把相关片段塞进去问题解决。教训是永远不要假设模型能处理任意长度的输入要主动控制输入规模。第二个坑是并发下的记忆串号。早期没做记忆隔离多个用户同时用对话历史混在一起闹了笑话。后来用ChatMemoryProvider按用户 ID 隔离问题解决。这个坑很隐蔽单用户测试时发现不了上线后才暴露。第三个坑是Embedding 模型和检索不匹配。文档用 A 模型向量化检索时用 B 模型结果相似度计算完全不对。记住入库和检索必须用同一个 Embedding 模型换了模型就要重新向量化全部文档。第四个坑是成本失控。开发阶段没注意每次请求都带一大堆历史消息和检索结果token 消耗飞快。后来加了记忆窗口限制、检索条数限制成本降了一大截。建议上线前算一笔账单次请求平均多少 token每天多少请求乘以单价心里有数。7.3 性能优化的几个方向如果发现响应慢可以从几个方向优化。一是减少输入 token精简提示词、控制记忆长度、减少检索条数。二是用流式响应让用户先看到部分结果感知上快很多。三是并发调用如果一次请求要调多次模型用异步并行。四是缓存相同或相似的问题可以缓存结果避免重复调用。流式响应在 LangChain4j 里通过StreamingChatLanguageModel实现配合StreamingResponseHandler处理分块返回。这个对用户体验提升很明显尤其是生成长文本时用户不用干等。8. 从 Demo 到生产的工程化建议8.1 配置外部化与多环境管理Demo 里把 API Key、模型名写死在代码里没问题生产环境绝对不行。正确做法是全部外部化到配置文件用 Spring Boot 的ConfigurationProperties绑定。不同环境开发、测试、生产用不同的配置文件敏感信息用环境变量或配置中心注入。langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini temperature: 0.7 timeout: 60s embedding-model: api-key: ${OPENAI_API_KEY} model-name: text-embedding-3-small这样配置的好处是换模型、调参数不用改代码改配置重启即可。而且 Spring Boot 的 starter 会自动把这些配置装配成 Bean你直接注入用就行。8.2 可观测性日志、指标与追踪生产环境必须能观测。至少要记录每次调用的输入、输出、耗时、token 消耗。LangChain4j 提供了ChatModelListener接口可以在调用前后插入逻辑用来打日志、上报指标。为什么要记录 token 消耗因为这是成本的主要来源也是发现异常调用的手段。如果某个接口突然 token 消耗暴涨可能是提示词被注入了奇怪的内容或者检索返回了超长文本。这些都要能及时发现。8.3 降级与容错设计大模型服务不是 100% 可用的网络会抖、服务会限流、模型会超时。生产系统必须考虑这些。常见的做法是设置合理的超时和重试重试要带退避策略避免雪崩准备降级方案模型不可用时返回兜底话术或走规则引擎对关键路径做熔断避免一个慢调用拖垮整个服务。我一般会给模型调用包一层统一处理超时、重试、降级、日志。这层封装让业务代码保持干净容错逻辑集中管理改起来也方便。8.4 安全与合规的底线最后说几个安全要点。一是输入校验用户输入可能包含提示词注入攻击试图让模型忽略原有指令。对关键场景要做输入过滤和输出审核。二是权限控制工具调用的方法要校验调用方权限不能让模型随意触发敏感操作。三是数据脱敏发给模型的数据要评估敏感度该脱敏的脱敏。四是输出审核模型生成的内容在展示给用户前最好过一遍敏感词和合规检查。这些不是可选项是上线前的必答题。我见过因为没做输出审核模型生成了不当内容导致事故的案例。技术能力越强越要守住底线。这套东西我从零搭到跑通前后大概花了一周其中大部分时间是在调 RAG 的检索效果和排查各种配置问题。LangChain4j 本身的 API 设计是清晰的坑主要在于对 LLM 应用开发范式不熟悉。一旦理解了「消息列表」「记忆」「检索增强」这几个核心概念剩下的就是熟练度问题。建议你从最小的 Demo 开始跑通一个再加一个能力别一上来就搭全套那样出了问题很难定位。