☰
Java开发者的大模型应用框架LangChain4j实战指南
2026/10/14 6:36:29 网站建设 项目流程

如果你是个做Java后端的人,最近一定被LangChain4j相关的问题包围。前脚群里有人问“Java怎么接大模型API,有没有靠谱框架”,后脚PM就跑来说“这个对话功能下周能上线吗”。网上一搜,教程大多是Python生态的,很多Java开发者卡在第一步。LangChain4j,就是把大模型应用开发这件事搬到Java技术栈的框架,聊天、提示词模板、记忆、知识库检索、结构化输出、工具调用,它都用Java的方式重新实现了一遍。这篇文章会从拉依赖开始,到跑通对话、接入记忆、搭建知识库问答,全部过一遍,适合任何想在公司Java项目里落地AI能力但又不想另起一个Python服务的开发者。

1. 为什么Java项目值得用LangChain4j,而不是硬套Python生态

1.1 技术团队的真实痛点

如果你在一家Java技术栈占绝对主力的公司,想给业务加一个“智能问答”入口,第一步其实不是写代码,而是先在会议室里吵一顿:要不要单独起一个Python服务。

这问题看着简单,实际牵扯的东西很多。Python侧确实有大模型生态的先发优势,但Java团队接一个新的Python服务,意味着要维护第二套技术栈的部署、监控、告警、日志链路,出了问题还得有人会排查。很多团队折腾到一半,发现在生产环境里最难的并不是“模型回答得准不准”,而是“这套多出来的服务到底谁管”。LangChain4j解决的就是这个入口问题:把大模型能力以普通jar包的形式嵌入现有Java项目,开发者的心智负担一下子小很多。

有人会说,直接用HTTP调用大模型API不就行了吗?当然可以,但裸调API的体验很原始。同一个大模型,每家服务商的请求报文格式都略有差异,返回的参数也五花八门,流式输出、多轮对话历史、上下文Token控制,这些全部要自己造轮子。而普通的业务项目又不值得为这些轮子专门写一套基础设施。

1.2 LangChain4j到底提供了什么

LangChain4j的开源定位就是“Java生态的大模型应用开发框架”。它的核心抽象,是把大模型资源封装成一组简洁的接口,再围绕这些接口提供提示词模板、聊天记忆、文档切块、向量检索、结构化输出、工具调用等一站式组件。设计上不是简单把某个Python框架翻译过来,而是用Java开发者熟悉的构建器、接口、依赖注入这套风格重写。

这样做的好处很明显。首先是模块化,你可以只引核心包加所需能力,模型服务商适配包、向量存储适配包按需引入,项目不会因为一个AI功能引入一大坨无关注释。其次是隔离性,业务代码大部分情况下只关心ChatLanguageModel、EmbeddingModel这些接口,底层服务商换掉了,业务代码改动很小。这种思路其实有点像JDBC,你把数据库连接换成另一种实现,SQL层还能稳定运行。

1.3 给你的第一个建议:先别被“LangChain”四个字吓住

很多Java开发者看到“LangChain”就会联想Python那一套Agent、Chain、记忆机制,第一反应是这东西肯定很重。实际用下来,LangChain4j的入门路径比想象中平缓。你不需要先理解Agent是什么,也不用搞懂整条Chain的抽象。从最普通的对话开始,一步步加上你要的能力,是更舒服的学习节奏。

所以我下面安排的路线是:先配置模型跑通一个对话,再接入提示词模板、结构化输出、记忆、工具调用,最后做知识库问答。每一层都建立在上一层之上,没有多余的概念。

2. 动手前先把这几个概念吃透

2.1 ChatLanguageModel:所有对话能力的入口

LangChain4j里最重要的接口就是ChatLanguageModel。你可以把它理解成“数据库连接对象”,不管背后接的是哪家服务商、跑在什么环境、用的是什么模型,面向你的方法主要就是chat或generate,传入用户消息,拿回模型回复。

配置也很直白:

ChatLanguageModel model = OpenAiChatModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .baseUrl("https://api.example.com/v1") .modelName("your-model-name") .temperature(0.7) .timeout(Duration.ofSeconds(60)) .build();

apiKey从环境变量读取,别写死在代码里。baseUrl指向大模型服务提供方的接口地址。modelName填写你购买或部署的模型标识。temperature控制随机性,想稳定输出就调低,想让回复更有发散性就调高,普通对话放0.7左右是常用的起点。

ChatLanguageModel只是同步模型接口。想看一个字一个字往外蹦的流式效果,还需要用StreamingChatLanguageModel。这个概念后面单独讲。

2.2 消息和记忆:模型本身什么都不记得

大模型的对话本质是无状态的。你发给它一段文本,它根据这段文本生成回复,并不会像人一样天然记得上一轮的“你好”。所谓的多轮对话,其实是把历史一轮一轮地重新塞给它。

LangChain4j用三种消息类型区分对话内容:SystemMessage是系统设定,比如“你是一个Java架构师”;UserMessage是用户输入;AssistantMessage是模型的历史回复。配置好的ChatMemory会按顺序保存这些消息,每轮都带着它们一起发。理解了这个,你就知道为什么“记忆”问题是所有LangChain类框架都要专门解决的,也更容易明白后续为什么要限制窗口大小。

2.3 Embedding与向量检索:文字变成坐标

知识库问答会用到Embedding。简单理解,Embedding就是把一段文字转换成一串能代表语义的向量数字,语义越接近的文字,向量距离越近。用的时候,先把文档切块并逐个向量化,存进向量库;用户提问时也向量化,拿这个向量去库里做最近邻搜索,找到最相关的几个文本段,再把这些文本段塞给大模型。整个过程看起来像“让模型去翻书”,而不是靠记忆力硬答。

这套机制里,EmbeddingStore是存储和检索向量的组件,ContentRetriever则负责把“检索能力”封装成AiServices可用的组件。你不需要自己写相似度算法,框架已经内置了。

2.4 一张表快速记住核心概念

概念一句话说明常用场景
ChatLanguageModel同步对话模型入口普通问答、生成文案
StreamingChatLanguageModel流式对话模型入口打字机效果回答
ChatMessage消息类型(系统/用户/助手)构造多轮对话
ChatMemory对话历史存储让模型记住上下文
PromptTemplate提示词模板避免字符串硬拼接
AiServicesAI服务封装层结构化输出、绑定记忆和工具
Embedding文本的向量表示语义相似度计算
EmbeddingStore向量存储与检索知识库问答
ContentRetriever检索增强组件RAG

这些概念里,前几个困扰新手最多;后面RAG相关的概念一旦跑通,商业价值最大。

3. 五分钟跑通第一个对话:从依赖到流式输出

3.1 创建工程并引入依赖

我用Maven搭的。推荐先用dependencyManagement锁版本,避免后续模块版本不一致。LangChain4j官方提供了BOM,按下面的方式引入:

<dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>1.0.0-alpha2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

注意,这里写的版本号以你实际操作时官方最新发布的版本为准,不要迷信某个固定数字。引入BOM之后,再加两个核心依赖:一个基础包langchain4j,一个模型服务商适配包。下面以某个兼容主流大模型消息协议的适配模块为例:

<dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> </dependency> </dependencies>

如果后面做知识库问答,再增加对应Embedding扩展模块。新手阶段尽量保持依赖精简,出问题容易定位。

3.2 第一个同步对话

配置好模型对象之后,只需要调chat方法就能拿到回复:

String answer = model.chat("请用一句话说明LangChain4j是什么"); System.out.println(answer);

就这两行,第一个对话跑通了。这一行背后,框架帮你做了HTTP请求组装、鉴权、消息序列化、超时管理、回复解析。如果你之前是裸写HTTP客户端调大模型,这时候应该能感受到封装的价值,因为后面要加流式、加记忆、加工具,每一步都只需要在现有结构上叠组件,而不是重写一套通信逻辑。

这里多说一句:OpenAiChatModel这个类名容易让人误以为只能连某一个固定服务商。实际只要是兼容主流大模型消息协议的服务,都可以通过配置baseUrl接入。现在很多服务都兼容这类协议,拿它当入门示例是最省事的。

3.3 换成流式,体验打字机效果

同步接口适合内部逻辑简单、不关心等待场景的地方。但做聊天界面或者问答入口时,用户盯着空白页面两三秒是致命体验。这时候用流式模型,每个token生成后立刻推给前端。

LangChain4j的流式接口是StreamingChatLanguageModel,配置方式和同步模型基本相似,调用时需要注册回调:

StreamingChatLanguageModel streamingModel = OpenAiStreamingChatModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .baseUrl("https://api.example.com/v1") .modelName("your-model-name") .build(); streamingModel.chat("你好,请做一个简短自我介绍", new StreamingResponseHandler<AssistantMessage>() { @Override public void onNext(String token) { System.out.print(token); } @Override public void onComplete(Response<AssistantMessage> response) { System.out.println(); } @Override public void onError(Throwable error) { error.printStackTrace(); } });

onNext每来一个token就执行一次,onComplete是整体结束,onError用来处理异常。做线上功能时,onNext里应该把token推给前端,而不是打印到控制台。实际项目里流式接口还会涉及响应式流、WebSocket连接管理这些事,但语言层的基础使用就是这一套回调。

3.4 给模型设定角色:SystemMessage

如果你直接问“给我写个生成订单号的规则”,模型可能会给出很通用的答案。希望模型更符合业务需求,就用SystemMessage设定角色和约束。

Response<AssistantMessage> response = model.generate( SystemMessage.from("你是电商系统的订单助手,回答问题时只关注订单业务,并且答案控制在100字以内。"), UserMessage.from("用户要求退款,怎么办") ); System.out.println(response.content().text());

SystemMessage不参与多轮对话的“自然历史”,而是始终作为最上层的指令存在。它决定了模型在整段对话里的身份和口径。很多“模型答非所问”的问题,根子出在SystemMessage写得太模糊。新手阶段多花一点时间打磨这个系统提示,收益远大于反复调试后面的业务逻辑。

3.5 新手最容易踩的五个配置坑

  • 密钥处理不当。写过一次硬编码apiKey之后,你的代码基本就在泄露密钥的路上了。用环境变量或配置中心管理。
  • baseUrl拼错。有的服务要求以/v1结尾,有的不需要。先确认你拿到的接口文档,再按正确格式填写,不要想当然。
  • 超时设置太短。大模型生成长文本时,同步请求可能超过默认的几秒,调大timeout到30秒或60秒比较保险。
  • 模型名写错。模型标识以服务商后台或文档为准,不是自己随便起一个就能通,多一个字母少一个点都不行。
  • 本地配置和线上配置不一致。不同环境用不同模型、不同密钥,至少把模型名和baseUrl做成配置项,不要散落在业务代码里。

4. 提示词模板与结构化输出:让模型按规矩办事

4.1 提示词模板:结束字符串拼接

实际业务里,提示词很少是固定一句话。可能要根据用户输入、业务类型、上下文动态生成。用字符串拼接也能写,但提示词一旦变长,转义、占位、版本维护都是噩梦。PromptTemplate就是来解决这个问题的。

PromptTemplate template = PromptTemplate.from( "你是{{role}}领域的专家,请用中文回答:{{question}}" ); Map<String, Object> variables = Map.of( "role", "Java后端", "question", "如何设计一个幂等接口?" ); String prompt = template.apply(variables).text(); String answer = model.chat(prompt); System.out.println(answer);

把变量抽出来,模板本身可以以资源文件或配置中心的形式管理。下次产品要改语气,你改的是模板而不是一堆if else拼接的字符串。这个习惯越早养成越好,等到提示词变得非常复杂时你就知道省了多少事。

4.2 结构化输出:别再手写JSON解析

很多业务场景需要模型返回一个固定结构的对象,比如“生成商品描述”要返回商品名称、卖点列表、适用人群。如果让模型返回一段JSON,再自己解析,问题非常多:模型偶尔会在JSON前后输出解释性文字、字段名不稳定、嵌套层级容易出错。

LangChain4j的AiServices就是解决这个问题的。先定义一个Java接口,再定义一个返回结构体,方法上加模板注释:

record ProductDescription(String name, List<String> sellingPoints, String targetAudience) { } interface Assistant { @SystemMessage("你是电商文案专家,输出内容必须匹配Java方法返回类型。") @UserMessage("请为商品《{{it}}》生成一条简洁的产品描述") ProductDescription describe(String product); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .build(); ProductDescription desc = assistant.describe("智能保温杯"); System.out.println(desc.name()); System.out.println(desc.sellingPoints()); System.out.println(desc.targetAudience());

框架会引导模型输出符合类型的JSON,并自动反序列化成record对象。Java 17以后用record非常简洁,旧版本也可以用普通POJO。这种“接口即服务”的写法,比手动构造一堆Message再解析返回值要直观得多。

这里有一个细节:@UserMessage里默认的{{it}}指向方法唯一参数。如果你有多个参数,LangChain4j提供了@V注解来绑定不同变量名。新手阶段先记住“方法参数最终会渲染到提示词里”就行。

4.3 控制好模型发挥的边界

结构化输出不是加了注解就万事大吉。模型偶尔还是会输出不符合预期的内容,尤其是当你的业务描述模糊、返回类型字段过多、示例缺失的时候。我的习惯是:SystemMessage里明确“输出严格匹配方法返回结构,不要输出任何解释性文字”;把temperature设置到0到0.3之间;字段名用业务常见词,避免让模型猜。还有一个建议:返回结构中不要放太多字段,一次让模型生成几十个字段,失败概率会明显上升。

结构化输出的价值被很多人低估。它让大模型从“聊天工具”变成了“能被业务代码直接调用的远程函数”。后面做RAG时,它还能帮你把模型返回的结果转换成前端需要的对象。

5. 聊天记忆与工具调用:从单轮问答到多轮协同

5.1 给机器人装一个短期记忆

前面提到,模型本身无状态,多轮对话要靠上下文回传。如果你想实现一个“记住用户姓名”的客服机器人,需要把之前的对话内容存起来,每轮一起发给模型。LangChain4j提供了ChatMemory。

ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build();

用Memory之后,用户连续追问“我叫小明”“我叫什么”这类问题,模型都能记住上下文。maxMessages表示最多保留多少条消息,这个值不是越大越好。消息条数越多,Token消耗越高,响应也越慢。一般先定20条左右,跑通后再根据真实对话长度调整。

5.2 多用户隔离:最容易被忽略的坑

如果系统同时服务多个用户,千万别做成一个全局ChatMemory。否则用户A的消息会出现在用户B的上下文里,轻则串味,重则泄露隐私。正确做法是按用户标识(memoryId)提供独立的记忆空间。

ChatMemoryProvider provider = memoryId -> MessageWindowChatMemory.builder() .maxMessages(20) .build(); Assistant multiUserAssistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(provider) .build(); multiUserAssistant.chat(1001L, "我叫小明"); multiUserAssistant.chat(1001L, "我叫什么名字?"); multiUserAssistant.chat(1002L, "你猜我叫什么?");

第一个用户会正常回答“你叫小明”,第二个用户因为用的是另一个记忆空间,模型无从猜起就会回答“我不知道”。这个机制决定了线上多租户场景的会话隔离,务必一开始就按这个思路设计。

5.3 工具调用:让模型指挥Java代码

大模型擅长总结和理解,但“今天天气是多少度”“两笔订单能否合并”这类需要实时数据或精确计算的场景,如果不让模型调用外部能力,它就只能瞎编。LangChain4j中的工具调用,指的是给模型声明一些Java方法,模型在回答过程中可以生成“调用哪个方法、传什么参数”的结构化请求,框架收到后帮你执行,再把结果喂给模型生成最终回答。

看一个最简单的计算器:

public class CalculatorTool { @Tool("计算两个整数的和") public int add(int a, int b) { return a + b; } } Assistant calcAssistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new CalculatorTool()) .build(); String result = calcAssistant.chat("请计算12加29等于多少?"); System.out.println(result);

这里的add方法并不是直接被Java代码调用,而是模型根据问题生成“调用add(12, 29)”的意图,框架执行后把结果41回传给模型。你在日志里会看到完整的“思考—调用—回传—回答”流程。工具方法要尽量用@Tool描述清楚功能和参数含义,模型判断是否调用时靠的就是这段描述。

5.4 工具调用不是银弹

用工具调用前想清楚:真需要让模型做决策吗?比如“删除订单”这类操作,不应该让模型来做,即使它能调用,也应该由业务权限校验逻辑严格锁定。工具调用适合读操作、计算操作、查询操作。涉及写操作和危险动作时,最多让模型生成一个“用户意图”,实际动作仍由业务代码执行。可以说清楚“模型负责翻译,业务负责执行”。

6. RAG知识库问答:把企业文档接入对话

6.1 为什么不用微调,而用RAG

想让模型回答你公司内部的知识,比如产品手册、技术规范、历史工单,第一反应可能是微调。但微调成本高、周期长,而且模型可能仍然“背诵”而不是“查证”。RAG的思路更像是开卷考试:模型不预先背诵所有文档,而是带着你的文档片段去答题。

用户提问之后,系统先从文档库里找出最相关的若干片段,连同问题一起交给模型,模型基于这些片段作答。好处是不用针对每条知识重新训练模型,文档更新后替换向量库里的内容即可,解释性也好。

6.2 文档切块:直接影响检索效果

使用向量检索前,文档不能整篇塞进去。整篇文档太长,向量化之后语义容易被稀释,用户问一个小点时,很难找到精确对应的片段。所以要先把文档切块。切块大小和重叠策略有讲究,太大会模糊语义,太小会失去上下文。

LangChain4j的DocumentSplitters提供递归切分器,示例配置成每块300字符,块间重叠30字符:

Document document = Document.from("你的产品手册或技术文档内容"); List<Document> chunks = DocumentSplitters.recursive(300, 30) .split(document);

重叠的作用是让分割点附近的语义不会“拦腰截断”,相邻块之间保留一定连续性。生产环境不建议照搬这个数值,可以先按300到500字试验,再根据检索效果反向调。

6.3 完成向量化、入库和检索问答

切块后,每一块都要通过Embedding模型转成向量,然后存入EmbeddingStore。为了演示方便,我用内存版向量存储,生产环境可以换用独立向量数据库组件。代码逻辑如下:

EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(System.getenv("LLM_API_KEY")) .baseUrl("https://api.example.com/v1") .modelName("your-embedding-model") .build(); EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); for (Document chunk : chunks) { Embedding embedding = embeddingModel.embed(chunk.text()).content(); store.add(embedding, TextSegment.from(chunk.text())); } ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(store) .maxResults(5) .minScore(0.6) .build(); Assistant ragAssistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String answer = ragAssistant.chat("我们产品的质保期是多久?"); System.out.println(answer);

maxResults表示从向量库取回几条相关片段,minScore是相似度阈值。minScore设太高可能召回为空,设太低会带回无关内容。0.6是一个可以起步的参考值,具体需要结合你的文档和Embedding模型调整。

6.4 检索效果不好时,先检查这三件事

第一,切块策略是否合理。如果很多问题都检索到同一块无关内容,把切块调小一些。第二,召回数量是否足够。topK太小容易漏,太大了又会把噪声带进上下文。第三,是否缺少关注点过滤。当你的知识库包含多类文档时,最好给不同来源打标签,检索时按业务域过滤,而不是全部混在一个库里。

RAG做到这一步已经具备商业价值。企业里最常见的第一版AI知识助手,通常就是这套架构。

7. 实战避坑清单与排查技巧

7.1 线上稳定性:超时、重试和降级

接入大模型后,线上接口的耗时从几十毫秒变成几秒,这是正常的,但必须处理超时。同步调用时设置合理的timeout,比如60秒;流式调用时也要有整体超时兜底。还要考虑重试:大模型服务偶尔会有限流和5xx错误,简单的重试策略能提升可用性。不过重试要注意幂等,比如生成类接口重复调用会多花Token,但结果通常可接受;如果业务对重复敏感,重试前要想清楚。

更现实的问题是渠道限流。多个用户同时打进来,模型服务经常比数据库更快被压垮。建议在业务层加简单的并发控制和降级:高峰期部分请求走缓存答案,或返回“当前服务繁忙”,总比整个接口打挂好。

7.2 Token成本:循环里的每一分钱都是钱

大模型的成本体现在Token上,一个中文Token通常对应几个字符,具体以服务商计费为准。团队最容易烧钱的地方是:把超长历史全部回传、RAG检索结果不带过滤直接塞、失败后一遍遍重试。

我的经验是给模型分级。轻量任务,比如文本分类、意图识别,用便宜快速的小模型;复杂推理,比如长文档总结,才用能力更强的模型。不要所有请求都上同一个高配模型。同时,可以给常见问题加一层缓存,命中缓存的请求根本不会走到模型。如果你的系统里某个问题的重复率很高,缓存省下的钱会非常明显。

7.3 问题排查速查表

现象可能原因优先处理方式
模型答非所问SystemMessage缺失或太模糊写清楚角色、目标、约束和示例
返回JSON解析失败模型在JSON前后加了解释性文字改用AiServices结构化输出,调低temperature
上下文一长就报错Token超出模型限制缩小记忆窗口,摘要历史,RAG只取相关片段
多用户会话串线共用了同一个ChatMemory用ChatMemoryProvider按用户ID隔离
RAG检索不到答案切块太大、topK太小、阈值太高调小切块,提高maxResults,降低minScore
结果不稳定temperature过高调到0到0.3,必要时固定seed
本地正常线上超时服务端响应慢或达到限流阈值加大超时时间,加重试和降级

这张表基本覆盖了从入门到第一个线上版本会遇到的八成问题。碰到问题先看是哪一类再动手,别一上来就怀疑框架有问题。LangChain4j本身很年轻,问题大多出在配置和提示词设计上。

7.4 一个可落地的工程分层

最后分享一个我比较喜欢的分层方式。把模型配置、业务接口、工具实现拆开,避免所有逻辑都堆在一个类里:

src/main/java ├── config/LLMConfig.java ├── assistant/ChatAssistant.java ├── tool/CalculatorTool.java ├── service/ChatService.java └── controller/ChatController.java

config负责构建模型实例和管理密钥;assistant负责定义提示词接口和记忆配置;tool放工具方法;service做业务编排;controller只负责接HTTP请求。这个结构一开始可能觉得小题大做,但项目一旦从“演示”变成“上线”,比的是谁更容易排查和扩展。至少把模型实例做成单例或由Spring容器管理,不要每次请求都new一个。

我个人实际做下来最大的体会是:大模型应用开发真正的难点,从来不是“调用模型”本身,而是“怎么把模型接到你的数据、你的业务流程和你的工程体系里”。LangChain4j把这层接缝很好地磨平了。最后一个建议:第一次做RAG项目时,别急着上复杂组件。先用内存向量存储和最简单切块策略,把整个链路跑通,再逐步升级存储和优化策略。流程通了,后续优化都是工作量;流程不通,再漂亮的框架设计也救不了你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询