这两年我在Java服务端里做大模型能力的落地被问得最多的一个问题就是“你是用Python写的还是Java写的”问的人默认了一个前提——搞大模型应用似乎就得拥抱Python生态。其实不然。LangChain4j出现之后Java开发者终于有了一个符合自己工程习惯的LLM应用开发框架而且它把“接入大模型、管理对话记忆、调用工具、对接私有知识库”这些高频需求全部收拢成类型安全、结构清晰的API。这篇文章我想顺着一条完整的上手路径把LangChain4j从依赖配置到实战组装讲一遍适合两类读者一类是已经在技术团队里做Java开发、正准备把AI能力集成进现有服务的人另一类是看着各种AI应用层出不穷、但不想随大流去学Python的纯Java工程师。你会发现用Java写AI应用没有想象中那么别扭甚至可能在可维护性上比脚本式demo舒服得多。1. 为什么Java开发者要盯上LangChain4jAI应用开发里的“Java缺席”最初我在现有服务里接大模型能力用的方式很原始拿HTTP客户端去调模型接口、用字符串拼接提示词、再把返回的JSON手工解析成对象。单个接口还好一旦涉及多轮对话、格式化输出、外部工具调用代码就开始失控——提示词一改拼接逻辑就要跟着动会话上下文要自己维护一个List往请求里塞模型返回的格式稍微变一点解析层就报错。这种状态持续一段时间后我几乎要以为“Java做AI应用就是得这么痛苦”。后来接触到LangChain4j第一感受是“这块拼图终于有人补上了”。它不是一个把Python的LangChain做简单翻译的移植版而是从Java开发者的习惯出发重新设计的一套LLM应用开发框架。它把所有和模型打交道的脏活累活都抽象成了几组干净的接口你不需要关心HTTP细节、不需要手动拼Prompt、不需要自己维护对话记忆的数据结构只需要面对Java对象和方法签名。具体来说LangChain4j帮你解决了这么几类问题统一模型接入聊天、流式对话、向量嵌入分别对应不同的模型接口后端的差异被封装在实现类里换模型不换业务代码。提示词工程化用模板和注解管理System Prompt、User Message不再靠字符串拼接。会话记忆托管一个ChatMemory对象维护多轮上下文窗口大小、清理策略都可以配置。工具调用在方法上标一个Tool模型就能自己决定什么时候调、传什么参数框架负责把调用结果送回模型。RAG知识库文档加载、切分、向量化、检索、再生成整条链路都提供了现成组件。类型安全的AI服务编排这也是我认为最“Java”的地方——你定义一个带注解的接口框架就在运行时生成实现像调用本地方法一样去调大模型。对Java团队来说LangChain4j的另外一个价值是它没有引入一套新的运行时就是一个普通依赖库可以无缝嵌进现有的Web服务、批处理任务或者微服务模块。如果你的项目用的是Java生态最常见的Web快速开发框架官方还提供了对应的starter配置好连接参数之后直接把模型对象注入到Service里就能用。相比在Python环境里单独维护一套服务再和Java应用做网络通信这种“在原来工程里直接加能力”的方式部署和运维成本低了一个量级。2. 上手前的认知清单LangChain4j帮你搭好了哪几个积木写代码之前我建议你先在脑子里搭一张地图一个大模型应用看起来千变万化本质上就四件事——把需求写清楚提示词、把上下文留住记忆、让模型真正干活工具调用、让模型回答自己不知道的信息RAG。LangChain4j的所有模块都是围绕这四件事展开的。先看一张总表把核心抽象和它们解决的问题对上号。你要解决的事LangChain4j的核心抽象一句话理解发起一次对话ChatLanguageModel聊天模型的统一入口输入字符串返回字符串流式输出StreamingChatLanguageModel一边生成一边回调适合打字机效果文本向量化EmbeddingModel把文本转成向量供RAG检索使用提示词管理PromptTemplate/ 注解比字符串拼接更安全、更易维护多轮记忆ChatMemory框架帮你保管历史对话让模型调外部方法Tool注解把Java方法暴露给模型知识库问答Document/Splitter/EmbeddingStore/ContentRetriever完整RAG组件链路组装一切AiServices用接口声明AI能力自动生成实现ChatLanguageModel是使用频率最高的接口。你引入对应的模型适配依赖后会得到一个实现了这个接口的具体类构建时通常需要配置三个参数模型服务地址、访问密钥、模型名称。设计上它刻意保持极简核心方法就一个generate输入处理好的提示词返回文本结果。这样的好处是业务代码根本不关心你后面接的是商用模型API还是本地推理服务哪天切换后端换一个构建参数就行。ChatMemory是另一个容易被低估的组件。模型本身没有记忆所谓多轮对话本质上是每次请求都把历史消息重新发给模型。自己维护这个列表也能做但很快会遇到三个问题消息多了Token爆炸、要自己决定保留多少轮、不同会话要隔离。MessageWindowChatMemory这类实现帮你把这三个问题一次性解决了你只需要告诉它最多保留多少条消息。AiServices则是最有LangChain4j特色的设计。它允许你定义一个普通Java接口方法上加上提示词相关的注解然后通过构建器生成代理对象。调用接口方法时框架自动完成提示词渲染、记忆注入、工具注册、结构化输出转换这一整套流程。从使用者视角看大模型被包装成了一个可测试、可Mock的本地组件。还有一组容易被忽略的接口是Document、DocumentSplitter、EmbeddingStore和ContentRetriever它们组成了RAG的官方推荐链路。文档先加载成Document再切分成小块每块向量化后存入EmbeddingStore查询时通过ContentRetriever找回相关片段最后拼进提示词让模型回答。后面实战部分我会完整演示这条链路。3. 十分钟跑通第一个对话示例依赖、配置与最小代码先把环境准备好。JDK建议用17及以上版本构建工具用你熟悉的Maven或Gradle都行。新建一个空白Java工程引入LangChain4j核心依赖再加一个模型后端的适配依赖。核心依赖的坐标长这样dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version选择你当前可用的稳定版本/version /dependency适配依赖取决于你打算接哪种模型服务。如果走商用API就引入对应的官方适配模块如果走本地推理服务就引入本地模型服务对应的适配模块。选依赖的时候注意两点一是核心库和适配库的版本要一致混用不同版本的API经常出怪问题二是有些适配模块还需要额外的日志库没配置的话控制台一片安静排查起来很头疼。模型后端有两条路线。商用API体验最省事去服务商平台申请一个访问密钥填进去就能跑。如果不想申请密钥或者担心成本可以本机起一个推理服务加载一个开源模型然后在代码里把地址指向本机。本地路线的优势是免费、数据不出内网、延迟也可控缺点是模型能力上限取决于你的机器配置。接下来是最小可运行代码。注意这里XxxChatModel是对你接入的模型后端实现类的一个占位写法你引入对应的适配依赖后把Xxx换成实际的类名即可。构建参数里baseUrl是模型服务的地址apiKey是访问密钥本地服务常常不需要真实密钥随便填一串能过校验就行modelName是你想用的模型名称。String apiKey System.getenv(LLM_API_KEY); String baseUrl System.getenv(LLM_BASE_URL); String modelName System.getenv(LLM_MODEL_NAME); // XxxChatModel 为你的模型后端对应的实现类 ChatLanguageModel model XxxChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .timeout(Duration.ofSeconds(60)) .build(); String answer model.generate(用一句话解释什么是依赖注入); System.out.println(answer);这段代码的背后框架替你做了好几件事构造请求体、把字符串包装成用户消息、发起调用、处理超时和鉴权、解析返回内容。如果手写HTTP调用这些逻辑每一个都要自己处理一遍而且每换一个模型服务商就要重新写一套。我建议第一次跑通之后故意把apiKey改成错的试一试你会看到框架抛出异常的位置非常精准这比对着响应文本猜问题舒服太多了。如果你的项目用的是Web快速开发脚手架还可以直接用官方提供的自动配置功能。在配置文件里写上模型服务的地址、密钥、模型名然后在代码里直接注入ChatLanguageModel。这个方式把底层的生命周期管理都交给容器日常开发时基本感觉不到“我在调一个外部模型”和注入一个普通业务Service没有区别。4. 拆解五个基础积木从提示词模板到AI服务接口4.1 用PromptTemplate告别字符串拼接直接往generate里传字符串短期内没问题场景一复杂就难受了。假设你要构造这样一个提示词要求模型扮演某个角色用特定语气回答用户问题还要附带几条参考资料。用字符串拼接代码长、易出错、难复用。用PromptTemplate模式就变成了“模板 参数”。PromptTemplate template PromptTemplate.from( 你是{role}请始终用{style}的语气回答用户的问题。 如果提供了参考资料请优先依据资料作答。 用户问题{question} ); Prompt prompt template.apply(Map.of( role, 资深技术顾问, style, 简洁专业, question, 如何设计一个高可用的消息队列 )); String response model.generate(prompt.text());这个组件给我的感觉就像把提示词当代码来管理变量一目了然改提示词内容不需要动Java逻辑模板可以放在配置文件或专门的文件里。团队里非开发同学也能参与提示词优化这是字符串拼接方案做不到的。4.2 用ChatMemory接管多轮上下文我第一次做多轮对话时犯过错误自己维护一个消息列表每次请求前把历史消息拼进去结果对话轮数一多消息长度失控模型开始答非所问。换成ChatMemory之后代码简洁了很多ChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build();maxMessages(20)表示最多保留20条历史消息超出后最老的消息会被自动丢弃。这个窗口值需要根据你的实际场景调整窗口越大模型可参考的上下文越多Token消耗也越大窗口太小用户多聊几句就“失忆”了。我一般从20到30条起步单轮修改建议类场景可以适当缩小。真正的多用户场景还要按会话ID做记忆隔离不能所有用户共用一个ChatMemory否则你的用户会互相“串台”。4.3 用结构化输出拿到强类型结果大模型默认返回自由文本如果要拿字段去填业务表单、存数据库就得自己解析。LangChain4j的结构化输出能力可以定义一个Java类型让模型直接返回这个类型的实例。record WeatherResponse(String city, int temperature, String suggestion) {} interface WeatherAssistant { WeatherResponse getWeather(UserMessage String query); } WeatherAssistant assistant AiServices.builder(WeatherAssistant.class) .chatLanguageModel(model) .build(); WeatherResponse weather assistant.getWeather(北京明天适合出门吗); System.out.println(weather.city() / weather.temperature() 度);这里的感觉非常奇妙你在调用一个Java方法返回的是一个强类型对象但背后其实是模型在干活。框架把接口返回值类型告诉了模型模型按要求输出框架再完成反序列化。这份“强类型”的价值在编译期就能体现——字段名拼错了IDE直接报错而不是等到运行期发现JSON字段对不上。要注意的是结构化输出的稳定性依赖模型能力级别较高的大模型普遍遵守格式指令小参数模型偶尔会漏字段或输出额外内容上线前需要加一层校验兜底。4.4 用AiServices把接口变成AI能力AiServices是LangChain4j的编排核心。它接受一个接口为你生成一个代理实现把提示词、记忆、工具、检索这些东西全部融合到接口调用里。刚才的结构化输出就是最简单的一种用法。它带来的好处不只是少写代码更重要的是把“AI能力”和“业务逻辑”解耦了——上层代码面对的是一个普通接口你甚至可以在测试环境用Mock对象替换掉AI实现这让单元测试变得非常干净。如果你愿意还可以在接口方法上组合使用SystemMessage和UserMessage注解。SystemMessage设置模型的角色和行为约束UserMessage声明哪些参数会填入用户消息。这样一来一次AI调用的完整输入输出契约全部写在接口定义里维护起来非常直观。4.5 用流式接口实现打字机效果聊天场景如果等模型全部生成完再一次性返回用户体验会差很多。LangChain4j提供了流式模型接口StreamingChatLanguageModel它把生成过程拆成一段一段往回传配合WebSocket或SSE前端就能看到打字机式的逐字输出。StreamingChatLanguageModel streamingModel XxxStreamingChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); streamingModel.generate(请写一段关于微服务的介绍大约200字, new StreamingResponseHandler() { Override public void onPartialResponse(String partialResponse) { // 把 partialResponse 逐段推给前端 } Override public void onComplete(ChatResponse completeResponse) { // 生成结束关闭连接 } });流式接口在Web场景下的体感差异是决定性的但实现时要注意两件事一是回调线程是框架内部线程池派发的往前端推数据时别直接用这个线程做耗时操作二是要处理用户中断前端断开连接后后端要能及时取消生成任务否则资源一直在空转。这两个细节我在最初实现时都没留意直到线上出现连接数堆积才回头修的。5. 让模型长出手脚工具调用、记忆与RAG的协同实战单靠提示词模型能聊、能写、能总结但它“不知道实时数据也做不了外部操作”。解决方式就是工具调用Tool Calling。我用一个客服助手的场景来串讲。先定义一个查询库存的工具方法class StockQuery { Tool(查询商品实时库存) public int queryStock(String skuId) { // 这里调用真实的库存服务 return 42; } }然后在构建AI服务时把这个工具注册进去interface CustomerServiceAssistant { String chat(UserMessage String message); } CustomerServiceAssistant assistant AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .tools(new StockQuery()) .chatMemory(MessageWindowChatMemory.builder().maxMessages(20).build()) .build(); String answer assistant.chat(帮我看看SKU-10086还有货吗);当用户问库存时模型的思考过程大致是这样先判断这个问题需要调用工具然后输出一个“我要调用queryStock方法参数是SKU-10086”的结构化指令框架收到指令后反射调用你的Java方法拿到返回值42框架把“工具调用结果42”作为新的消息送回给模型模型基于这个结果生成最终答复。整个过程对开发者透明你看到的只是接口方法的返回值但背后模型已经完成了一次“自己决定要不要动手、动什么手”的推理。需要提醒的是工具方法一定不能有副作用隐患。模型可能因为用户话术变化而意外触发某个工具所以方法内部要有参数校验和权限控制关键操作最好加确认步骤。我在生产环境里给工具方法统一加了审计日志谁在什么时候调了什么工具、参数是什么、结果是什么全部留痕方便回溯问题。同样在这次构建里chatMemory让模型记住了前文对话。用户先说“我之前问过一个商品”然后再问“它现在有货吗”模型能关联上历史话题。这里要特别说明记忆和工具并不是互斥的它们是同一套AiServices协调的不同模块模型在一次回答中既可以参考历史上下文也可以决定查询工具还能结合RAG检索到的知识库内容。再引入RAG。假设客服助手需要回答“你们的退换货政策是什么”这类问题这些内容在实时库存工具里是查不到的但政策文档是现成的。做法是把政策文档切块、向量化、存入向量库构建一个检索器// 1. 加载文档并切分 Document document loadDocument(退货政策.md); DocumentSplitter splitter new ParagraphSplitter(300, 50); ListTextSegment segments splitter.split(document); // 2. 向量化并存入向量库 EmbeddingModel embeddingModel new XxxEmbeddingModel(...); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); embeddingStore.addAll(embeddings, segments); // 3. 把检索器接到AI服务上 ContentRetriever retriever new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel); CustomerServiceAssistant assistant AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .chatMemory(...) .tools(new StockQuery()) .contentRetriever(retriever) .build();这样组合出来的客服助手面对三类问题各有办法闲聊直接回答查库存调用工具问政策检索文档。你可能已经发现工具调用和RAG解决的是不同的问题RAG给模型喂的是“参考文本”模型不能修改这些内容适合回答知识型问题工具调用让模型执行外部动作适合实时查询和写操作。实际项目里这两种能力经常同时出现用AiServices把它们组装到同一个接口上是最顺手的做法。6. 组装一个文档问答助手从需求到可运行代码前面讲了单个能力这一节把它们串成一个完整Demo。目标很明确读入本地若干文档构建一个问答助手用户提问后返回基于文档内容的答案。这是RAG应用最常见的形态也是我在团队内部落地知识库问答时的最小骨架。先定需求自动加载某个目录下的所有文本或Markdown文档对文档执行切分、向量化、入库接收用户问题检索相关片段生成回答在回答中标注引用了哪个文档片段便于人工核验接入模型和向量模型的部分和前面一致核心区别在文档处理管线。我把完整代码拆成几段来写。第一段加载文档并切分。这里我使用ParagraphSplitter两个参数分别表示分段大小这里300个字符和相邻段之间的重叠50个字符。重叠段的作用是避免一个完整语义被拦腰切断比如一段话跨在两个相邻切块边界上有重叠就能保证检索时至少有一块包含完整意思。这个参数值得多调试几次太小了检索噪声多太大了Token浪费多。DocumentSplitter splitter new ParagraphSplitter(300, 50); ListTextSegment segments new ArrayList(); for (String filePath : filePaths) { Document document DocumentLoader.load(filePath); segments.addAll(splitter.split(document)); }第二段向量化并入库。向量模型的选择直接影响检索质量同一个模型服务商一般会同时提供对话模型和向量模型选embedding能力时关注两个维度文本长度上限和向量维度。注意更换向量模型时历史已经入库的向量就失效了必须全量重嵌入这是我踩过的一个实打实的坑。ListEmbedding embeddings embeddingModel.embedAll(segments).content(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); embeddingStore.addAll(embeddings, segments);第三段构建检索器和AI服务。检索器负责在查询时把用户问题向量化然后在向量库里找最相似的片段。maxResults参数控制取回几段我通常设3到5段还可以设置相似度阈值低于阈值的片段直接丢弃避免模型被不相关内容干扰。EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.7) .build(); interface DocAssistant { String answer(UserMessage String question); } DocAssistant docAssistant AiServices.builder(DocAssistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String answer docAssistant.answer(合同审批的流程是什么);第四段代码里加上引用来源验证。因为EmbeddingStore里存的是TextSegment检索到结果后可以进一步取出片段文本和它对应的原文档元数据拼进提示词的同时也输出到日志里。这样如果模型答错了你可以立刻判断是“检索到的内容本身不对”还是“模型没有准确依据生成回答”排查效率高得多。这个Demo跑通后有几个明显要往生产方向优化的点InMemoryEmbeddingStore存的是内存数据重启就丢生产环境要换成持久化向量库文档更新时需要做增量索引而不是每次全量重建检索结果可以加一层重排序让模型先对候选片段做相关性打分再进入生成阶段。这些都是后话第一版先跑通链路你已经掌握了RAG应用的主干。7. 避坑红黑榜版本、成本、模型选型与并发场景的真实经验最后分享几条来自真实项目的经验这些内容文档里未必会写但一旦踩中往往要花掉一两天去排查。版本更新速度比想象中快先锁定版本再看API。LangChain4j迭代很积极一些API在版本升级后会发生参数变化。我遇到过的情况是网上教程用的是旧版写法官方文档已经是新接口照着写根本编译不过。建议代码里显式指定版本号升级时先看官方变更说明再决定要不要跟随。生产项目没必要追最新版稳定跑着比什么都重要。API密钥管理别走捷径。密钥放在配置文件里已经是坏习惯了更危险的是不小心提交到代码仓库。泄漏之后别人可以用你的密钥消耗大量Token月底账单会教你做人。请一律使用环境变量或专门的密钥管理服务并定期轮换。如果是本地模型服务不需要真实密钥代码里也应该用占位符避免养成把密钥写死的肌肉记忆。Token成本会被记忆和RAG双重放大。很多人在原型阶段没有成本概念上了生产才发现账单涨得飞快。多轮对话的记忆窗口越大每轮请求消耗的Token就越多RAG检索回来的片段越冗长预测成本也越高。建议做一个简单的监控记录每次调用的输入Token数和输出Token数按天汇总这样能很清楚看到是哪个环节在烧钱。设计上可以给记忆窗口设置上限RAG片段只截取最相关的一两段这通常能省下不少费用。小参数模型做复杂工具调用容易翻车。工具调用看起来简单其实非常考验模型的指令跟随能力。模型要正确判断该不该调工具、调用哪个工具、参数类型是否匹配。参数量偏小的模型偶尔会把参数类型传错或者编造一个不存在的工具名。如果你的场景依赖工具调用务必在小模型上做多轮回归测试不行就换更大的模型或者做一个工具调用的结果校验层。大模型的非确定性会影响测试。同样的输入两次运行结果可能不完全一样这让自动化测试变得很棘手。我的经验是不要断言模型返回的完整文本而是断言结果里是否包含关键实体信息比如订单号、金额、政策名称或者用结构化的方式返回只针对结构化字段做断言。对稳定性要求极高的场景可以降低模型温度参数但这只能缓解不能根除。并发场景要注意限流和重试。商用模型API通常有速率限制并发一大就会报错。框架自身不提供重试策略需要在业务侧实现指数退避重试。我踩过的教训是某次活动流量上来后大量请求同时打到模型接口直接触发限流然后排队重试又放大了压力。后来在接入层加了一个带有信号量的限流器再配合合理的超时时间整个服务才稳定下来。嵌入模型切换必须重算历史向量。我在前面提过一次这里再强调一遍向量模型的升级或更换会导致新旧文本向量不在同一语义空间里新旧向量混着存检索质量会明显下降。如果你要升级向量模型请设计一个全量重建索引的流程宁可停机几分钟也不要新旧混合。最后说几句我从“手动调大模型API”到“用框架组装AI应用”最大的体会是工具链成熟之后Java开发者做AI应用的门槛已经大幅降低真正的难点不再是接口怎么调而是怎么设计提示词、怎么组织工具、怎么治理检索质量——这些恰恰是普通Java工程师本来就擅长的工程化能力。如果你正在犹豫要不要把LangChain4j引入项目我的建议是先别急着上生产拿一个内部知识库问答的Demo跑通整条链路再评估效果和成本。后续如果想深入可以往这几个方向探索多工具组合成复杂Agent流程让模型自主决定先调哪个工具引入更完整的RAG评估手段用一组标准问题持续度量检索和回答质量以及多模态能力把图片、音频理解也纳入现有服务。先把基础打扎实剩下的都是水到渠成的事。
阅读完成 · 觉得有帮助?