Java大模型应用开发:LangChain4j入门与RAG落地实践 最近不少做Java后端的朋友跟我聊到同一个问题Python生态里大模型开发已经玩出花了Java这边到底怎么下手翻开各种AI教程示例几乎全是Python对Java工程师来说确实劝退。其实Java生态早就有对应的成熟方案LangChain4j就是其中比较靠谱的一个。它把大模型对话、工具调用、知识库检索这些能力封装成Java程序员熟悉的接口风格核心思路是把LLM应用开发变成“配置加接口”的组合你平时写Spring Boot的那套习惯基本可以平移过来。这篇文章我会从一个Java开发者的视角把LangChain4j的核心概念、环境搭建、RAG落地和排坑经验完整串一遍新手照着走就能跑通。1. 项目定位为什么Java团队需要LangChain4j1.1 自己调用大模型API痛点在哪里很多团队一开始是让后端同学直接用HttpClient去调模型的HTTP接口的。诚实地讲跑通一个最简单的对话没问题但只要是稍微真实一点的需求就会开始反复写类似的代码自己拼JSON请求体、处理鉴权头、解析返回结构、维护多轮会话的历史消息数组。更麻烦的是工具调用也就是让大模型调用Java方法去获取实时数据这时候需要自己写一个循环模型返回tool_calls你执行对应方法再把结果作为一条新消息回传给模型模型基于结果生成最终回复。这个循环本身逻辑不复杂但每次新项目都要重新实现一遍很容易写出边界条件没处理好的Bug比如工具返回超时、参数类型对不上、历史消息越来越多导致请求体膨胀。更现实的问题是这种手写方案没有一个统一抽象。你今天接的是OpenAI明天想换国内的厂商或者其他开源模型接口风格完全不一样改动量不小。如果你的项目只是做一两个接口手写也没问题如果是打算正经做一个带知识库、带工具调用、带多轮记忆的智能体应用那这套重复劳动的成本就很可观了。1.2 LangChain4j给Java生态补齐了什么LangChain4j的定位可以理解成“Java版的LLM应用脚手架”它把大模型应用里那些高频重复的环节都抽象成了稳定组件。最核心的几个能力一个是统一的模型入口无论底层接哪家大模型上层都通过一个ChatLanguageModel接口去对话一个是AiServices它允许你定义一个普通的Java接口框架自动生成实现把接口方法变成与大模型交互的入口还有一个是工具调用你只要在方法上打一个Tool注解大模型就能在对话过程中自动决定是否调用这个方法另外RAG检索增强生成相关的组件也都是现成的从文档切分、向量化到语义检索都有对应的API。这套设计对Java开发者非常友好因为整个开发模式和你熟悉的依赖注入、接口驱动、类型安全这些习惯是对齐的。写智能体应用不再是“面向字符串编程”而是面向Java类型和接口编程。类型安全这个优势在项目变大之后尤其明显参数传错了编译期就能发现而不是等到线上跑起来才报错。1.3 和手写方案、Python LangChain的取舍如果你之前接触过Python那边的LangChainLangChain4j虽然名字相似但不是简单翻译而是基于Java语言特点重新设计的。Python版胜在组件丰富、社区资料多但动态类型在项目规模变大以后维护成本会高一点而且要把Python服务嵌入到现有Java后端本身就有环境成本和部署成本。手写HTTP方案则没有框架依赖对简单的单接口场景其实是够用的但复杂链路下代码量会呈指数级增长。我的建议是如果你只是调一次模型玩一玩手写没问题如果你要做的是智能体、文档问答、自动化工具编排这类正经功能直接引入LangChain4j更划算。尤其是现有系统本身就是Spring Boot技术栈的时候集成成本极低后面我会专门讲整合方式。2. 环境准备与第一个可运行Demo2.1 Maven依赖怎么加先建一个普通的Maven项目JDK建议17以上。LangChain4j的核心包和模型适配包是分离的所以至少要加两个依赖核心包langchain4j以及你想用的模型供应商适配包。我在写这篇文章时用到的是1.1.0版本如果你的中央仓库能拉到更新的稳定版直接替换版本号就行接口变化不大。properties langchain4j.version1.1.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 dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama/artifactId version${langchain4j.version}/version /dependency /dependencies需要注意的一个点是langchain4j-open-ai这个包不仅适配OpenAI官方接口也适配很多提供“OpenAI兼容接口”的服务商你可以通过自定义baseUrl指向任意兼容服务这一点非常灵活。2.2 两种模型接入方式外部API与本地Ollama如果你已经在用大模型API直接在代码里构建模型对象即可通过环境变量读取密钥不要硬编码到源码里。以OpenAI为例ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .temperature(0.7) .build();如果不想依赖外部服务更推荐用Ollama在本地跑开源模型。先在本机安装Ollama然后拉一个中文效果还不错的模型比如通义千问的Qwen系列ollama pull qwen2.5:7b代码侧只需要把适配器换成Ollama的实现ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .temperature(0.7) .build();我个人建议新手直接从Ollama开始原因是完全不需要考虑网络和密钥问题在本地就能把整条链路跑通。等理解了核心概念之后再切换到真实API或生产级配置会顺畅得多。Ollama对机器配置要求不算高7B量化模型在16G内存的Mac上可以流畅运行Windows和Linux环境也支持。2.3 最小可运行代码五步走通对话万事开头难所以我把最小Demo砍到极致。新建一个Main类代码如下import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; public class Main { public static void main(String[] args) { ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .temperature(0.7) .build(); String answer model.generate(用一句话介绍你自己); System.out.println(answer); } }直接运行main方法控制台就会输出模型的回答。这里有几个细节值得说一下model.generate(String)是最简单的方法适合快速验证链路temperature控制的是生成随机性值越大回答越发散一般在0.2到0.8之间比较合理做知识问答建议调低一点比如0.2减少自由发挥。这个Demo跑通之后你已经有了一件重要的事确认你的Java工程能够正常连通大模型。如果是通过OpenAI接口只需要把构建模型那段换成OpenAiChatModel其余代码完全不变。这也是LangChain4j设计的核心优势之一上层调用不感知模型供应商差异。3. 核心概念拆解从ChatModel到AiServices3.1 ChatLanguageModel统一的大模型入口上面的Demo里我们已经用过ChatLanguageModel了这个接口是整个框架的地基。它的定位就是“一个Java方法对应一次大模型调用”内部帮你处理了HTTP通信、认证、重试、序列化这些琐事。除了最基础的generate(String)更常用的是基于消息对象的对话方法ResponseAiMessage response model.chat(UserMessage.from(帮我写一段Java冒泡排序代码)); System.out.println(response.content().text());这里涉及几个消息类型UserMessage代表用户输入AiMessage代表模型输出SystemMessage设置系统的角色或行为约束还有ToolExecutionResultMessage用来承载工具执行结果。这种消息设计本质上就是对齐大模型API里的消息数组只是用强类型方式暴露给你写起来更不容易出错。理解这个抽象之后你会发现整条链路其实很简单无论是简单的问答、多轮对话还是后来要做的工具调用、RAG检索最终都是组合这些消息类型而已。框架做的事就是帮你把这些消息编排好。3.2 AiServices让接口声明变成智能体服务如果只有ChatLanguageModelLangChain4j其实只是把HTTP调用封装了一下真正的杀伤力在AiServices。它的核心机制是你定义一个普通的Java接口声明一个方法AiServices会在运行时通过动态代理生成实现类。这个实现类的逻辑就是把你的参数变成一条用户消息发给大模型拿到模型回复后返回给你。举个例子定义一个问候助手接口interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .build(); String answer assistant.chat(你好给我介绍一下你自己);这看起来很简单但妙处在于接口方法签名其实是完全可控的。你可以定义带多个参数的方法返回值也可以定义成自定义类型框架会自动完成消息转换和结果解析。接口和实现分离的设计也让单元测试变得方便需要mock的时候直接mock接口就行。AiServices真正让人眼前一亮的功能是和工具调用结合。假设你想让大模型能获取实时时间传统做法是你自己去查时间再拼到Prompt里但用了工具调用之后你只需要写一个普通类public class DateTimeTool { Tool(获取当前日期返回格式为 yyyy-MM-dd) public String today() { return LocalDate.now().toString(); } Tool(计算两个整数相加的结果) public int add(int a, int b) { return a b; } }然后在构建Assistant时把工具对象传进去Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new DateTimeTool()) .build(); String answer assistant.chat(今天是几号顺便算一下 23 加 19 是多少);这里的执行流程很有意思你的问题会先发给模型模型判断“今天几号”这个信息它自己不知道需要一个工具来获取于是返回一个工具调用请求AiServices收到请求后通过反射调用DateTimeTool.today()方法把结果包装成消息再回传给模型模型看到工具返回的日期之后生成最终的自然语言回复。整个过程对调用方完全透明你只是调了一个接口方法而已。这种“模型自主决策调用Java方法”的能力就是智能体应用的基础。业务上完全可以把查询订单、查库存、查用户余额这些已有服务方法都暴露成工具让大模型根据用户意图去组合调用相当于给你的大模型应用接上了公司现有系统的数据能力。3.3 记忆管理让对话有上下文用上面的Assistant直接进行多轮对话你很快会发现一个尴尬的问题第二轮提问时模型好像“忘了”第一轮聊过什么。原因很简单每次assistant.chat()默认只发送当前这一条消息没有把历史消息带过去。要让对话有记忆需要显式配置ChatMemory。LangChain4j提供了两种内存实现MessageWindowMemory和ChatMemory。前者按照消息条数裁剪历史比如只保留最近20条实现简单适合大多数场景后者按照token预算裁剪历史适合上下文窗口比较紧张的大模型。使用方法是在构建Assistant时传入内存对象ChatMemory memory MessageWindowMemory.withMaxMessages(20); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(memory) .tools(new DateTimeTool()) .build();配置之后每一轮对话的历史消息会自动维护。有几个细节值得注意一是按条数裁剪时如果消息很长可能会把上下文撑爆所以更稳妥的是按照token估算裁剪二是如果要做多用户多会话隔离不能所有用户共用一个ChatMemory应该在每次会话创建时单独构建一个内存实例。这一点在将来接入Web应用时尤其关键否则用户A的问题会出现在用户B的对话里。4. 实战构建一个文档问答助手RAG落地4.1 场景设定与文档切分大模型应用里最实用的一个方向是文档问答也叫RAGRetrieval Augmented Generation检索增强生成。假设你有一份产品说明书或内部FAQ文档希望用户提问时AI能根据文档内容回答而不是自由发挥。直观的做法是把整份文档塞进Prompt再让模型回答但文档一大就会超出上下文窗口而且并不是所有内容都和问题相关塞进去反而会影响回答质量。RAG的思路是先把文档切成小段再对每一段做向量化存进向量数据库。用户提问时先把问题向量化在库里做相似度检索找出最相关的几个片段然后把“问题相关片段”一起发给模型让模型基于检索到的资料来回答。说白了就是给大模型开卷考试只让它看和题目有关的几页讲义。LangChain4j里第一步是加载文档。最简单的方式是直接从一个String构建DocumentDocument document Document.from(这里放你的完整文档内容比如产品FAQ、操作手册等...);但文档一般都比较长直接整段向量化效果不好因为向量是整段的语义压缩内容一长检索精度就会下降。所以需要拆分框架提供了DocumentSplitters工具类常用的拆分策略是按递归字符分割DocumentSplitter splitter DocumentSplitters.recursive(300, 50); ListTextSegment segments splitter.split(document);300指的是每段目标字符数50是相邻段落之间的重叠字符数。为什么要有重叠因为段与段之间的边界可能正好切断一个完整语义单元重叠一点可以让信息不丢失。这两个参数需要根据文档实际情况调整技术文档可以适当调大对话记录这种碎片化内容可以调小。4.2 向量化与向量存储文档切好之后需要把每一段转成向量。LangChain4j提供了EmbeddingModel抽象你可以用OpenAI的向量模型也可以继续用Ollama本地的向量模型。本地方案如下EmbeddingModel embeddingModel OllamaEmbeddingModel.builder() .baseUrl(http://localhost:11434) .modelName(nomic-embed-text) .build();如果模型镜像没拉过先执行ollama pull nomic-embed-text。向量模型的选择对中文场景影响比较大本地方案里nomic-embed-text属于通用型效果中规中矩够用于学习生产环境建议用中文效果更好的专用向量模型。向量化之后需要存储。入门阶段最简单的方式是使用内存版InMemoryEmbeddingStore数据存在进程内存里重启就没了不适合生产但非常适合验证逻辑EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); store.addAll(embeddings, segments);如果你的文档量比较大比如几万段内存版就不够用了LangChain4j支持多种向量数据库适配像PGVector、Chroma、Milvus都有对应的集成包。从内存版切换到外部向量库时API变化很小主要是在构建store的方式上不同。4.3 检索增强问答把资料注入上下文存储完成之后核心是构建一个检索器让用户在提问时自动完成“语义检索-拼接上下文-模型回答”这条链路。LangChain4j提供了EmbeddingStoreRetrieverEmbeddingStoreRetriever retriever EmbeddingStoreRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.65) .build();maxResults控制最多取几个片段取值太小可能漏掉关键信息太大又会引入噪声并占用上下文窗口一般是3到5个。minScore是最低相似度阈值低于这个值的片段会被过滤掉阻止模型拿不相关内容硬答阈值调太高容易检索不到结果建议从0.6到0.7起步。接着通过ContentRetriever把它注入AiServicesContentRetriever contentRetriever ContentRetriever.from(retriever); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .contentRetriever(contentRetriever) .build(); String answer assistant.chat(根据提供的资料发票丢失后应该怎么处理);整个过程里框架会自动完成把你的问题向量化去向量库找出最相关的几段文本把“相关资料”和你的问题一起组装进发送给模型的Prompt。你写的代码和普通对话接口几乎一样区别只是构建Assistant时多配置了一个contentRetriever。这里有一个很重要的思维转变RAG的“检索”不是用SQL的精确匹配而是语义匹配。问题里哪怕没有出现文档里的原词只要语义相近就能被召回。这就是为什么向量检索在这个链路里不可替代。当然这套链路也不是零成本文档切分参数、向量模型效果、相似度阈值都会直接影响回答质量需要根据实际效果调参。5. 常见问题与排坑手册5.1 依赖版本与构建问题新手最容易踩的坑是Maven依赖下载失败或者版本不兼容。先确认你的Maven镜像源能正常访问中央仓库再确认你写的是已发布的版本号。LangChain4j版本迭代比较快有些试用版在中央仓库存在时间很短用旧教程里的版本号经常拉不下来解决办法是去中央仓库搜一下最新的稳定版。另外注意JDK版本LangChain4j 1.x要求JDK 17以上如果用JDK 8会遇到编译错误。还有一个常见问题是部分依赖传递冲突尤其项目本身在用老版本Spring或某些HTTP客户端库时。一般建议是使用Maven依赖树检查一下优先保留LangChain4j带来的版本因为它对兼容性要求更严格。5.2 工具调用不生效、响应异常很多人在配置Tool之后发现模型不调用工具或者报参数解析错误。我排查下来最常见的原因有三个一是工具方法的描述不够清晰模型不知道这个方法应该在什么时候用描述里要把使用条件、参数含义、返回值说明写清楚二是参数类型过于复杂比如传自定义对象或Map模型返回的JSON解析不到Java对象上建议参数尽量用基本类型和字符串三是工具类上有一些框架扫描不到的方法构建AiServices的时候忘了把工具对象传进.tools()。另外一个容易被忽略的问题是工具方法内部如果有网络调用或数据库查询一定要设置合理的超时时间否则模型可能一直等待工具返回。我自己习惯是在工具方法内部加上try-catch返回一段明确错误信息而不是直接抛异常这样模型还能根据错误信息调整回答方式。5.3 上下文超限与并发安全多轮对话时间长了报出上下文超长错误是很正常的。如果你用的ChatMemory按消息条数裁剪但每条消息本身很长也可能触发上下文窗口上限。排查思路是看请求日志里实际发送的tokens数量然后调整ChatMemory的裁剪策略比如从MessageWindowMemory换到按token预算裁剪的ChatMemory或者把历史消息截断得更激进一些。并发安全方面ChatLanguageModel的实现类一般是无状态的可以作为一个单例被多个线程共用。但ChatMemory是有状态的同一个会话内的多轮消息都维护在里面多个用户共用同一个内存实例就会互相串话。实际项目里应该在每个会话创建时new一个ChatMemory或者按userId维度做隔离。6. 和Java工程团队整合的几点建议6.1 接入Spring Boot的配置方式LangChain4j为Spring Boot提供了官方starter集成体验很顺。引入依赖时除了核心包还要加对应的starter适配包dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency然后在application.properties里配置模型参数langchain4j.ollama.chat-model.base-urlhttp://localhost:11434 langchain4j.ollama.chat-model.model-nameqwen2.5:7b langchain4j.ollama.chat-model.temperature0.2配置完成后ChatLanguageModel会被Spring容器自动创建直接注入Service即可。更推荐的做法是把你定义的Assistant接口注册成Spring Bean业务代码里注入这个接口完全屏蔽底层细节。这也符合你熟悉的依赖注入习惯。6.2 与数据访问层结合让智能体查库Java后端团队最关心的一个问题是怎么让大模型访问现有数据库数据。方案是把你已有的Mapper方法封装成Tool。比如用户问“帮我查一下手机号138开头的用户余额”大模型可以自动决定调用一个查询方法Component public class UserQueryTool { private final UserMapper userMapper; public UserQueryTool(UserMapper userMapper) { this.userMapper userMapper; } Tool(根据手机号查询用户余额入参为手机号字符串返回余额数值) public String queryUserBalance(String phone) { User user userMapper.selectByPhone(phone); return user null ? 用户不存在 : String.valueOf(user.getBalance()); } }这样你的智能体应用就不只是“聊天机器人”而是能够结合企业真实业务数据做查询和决策的服务。需要注意给工具方法写清楚返回格式模型对“返回余额数值”这样的描述理解得会比较好。另外涉及敏感数据的操作建议在工具方法内部做权限校验不能只依赖模型层面的判断。6.3 再往下可以做的方向把基础链路跑通之后可以探索的方向还有几个流式输出用StreamingChatLanguageModel配合SSE把回答流式推给前端体验会比一次性返回好很多结构化输出让你的Assistant接口方法返回一个自定义Java对象由框架来自动解析模型输出多工具协同把查询、计算、通知类工具组合起来构造成能独立完成任务的智能体。这些方向都有对应的官方示例上手路径比从零开始顺畅得多。我个人在实际操作中的体会是LangChain4j最值得花时间研究的不是API本身而是“让模型自主决定调用什么工具”这套思维。把现有系统的能力暴露成工具让大模型成为调度者业务价值能立刻体现出来。最后再分享一个小技巧刚开始接入时工具返回的内容一定要用纯文本和明确的数字格式不要返回大段JSON或HTML模型对这些复杂格式的理解和提取不如结构化文本稳定。先跑通最小闭环再逐步增加复杂度是最稳妥的路线。