☰
Java后端大模型开发框架LangChain4j实战:从对话到知识库
2026/10/12 4:17:09 网站建设 项目流程

做 Java 后端的朋友,这两年应该都有个明显感受:AI 应用相关的需求越来越多,但 Java 生态里能直接上手的框架一直不多。LangChain4j 这个名字在社区里出现的频率越来越高,它是 Java 生态里专门做大模型应用开发的框架,目标就是让 Java 开发者不用去啃 Python 那套工具链,也能快速接上大模型、做对话、做工具调用、做知识库问答。这篇教程,我就以一个同样从零开始踩坑过来的后端开发者的视角,带你把它跑起来,并说清楚每一步背后的逻辑。

先说清楚这篇文章适合谁:你已经会 Java、会 Maven 或 Gradle,但还没系统接触过大模型应用开发。不需要提前学过 LangChain 或者 Python,我会从最简单的对话程序讲起,一路做到工具调用和 RAG 知识库。内容偏实战,每个环节我都会放可以直接复制的代码,也会把那些文档里不会写、只有实际操作才会踩到的坑一并说出来。

1. 先搞清楚:LangChain4j 到底解决了什么问题

1.1 没有它的时候,接一个大模型有多麻烦

很多人第一次接大模型,想法都很朴素:不就是发一个 HTTP 请求,把问题塞进去,拿到返回文本吗?没错,单次调用确实简单。但真实业务里几乎不可能只做一次调用,只要你开始做多轮对话、对接私有数据、让模型调用业务方法,麻烦事就一件接一件冒出来。

我给你还原一下没有框架时的真实场景。先说多轮对话:大模型本身是"无状态"的,它不记得你上一句说了什么,你必须每次把全部历史消息拼好再发给它。于是你得自己维护一个消息列表,自己控制它别超过 token 上限,还得处理超时重试、JSON 解析、异常分类。这些代码写起来不难,但很碎,每个接入方都要重复造一遍轮子。

再说工具调用。模型说"我想查一下订单状态",并不会真的去查数据库,它只是返回一段结构化的调用请求。你的业务代码需要解析这段话、找到对应方法、传参执行、再把结果回传给模型。等模型看到结果,才能继续生成最终回答。这套循环放到业务里,你会发现自己一直在写"解析-分发-执行-回填"的胶水代码。

更别提还有向量化、文档切分、检索增强生成这类概念,第一次接触的人光是理解名词就要花不少时间。所以坦率说,直接用 HTTP 接口做原型没问题,但要做到可以交付、可以维护的程度,没有一层统一抽象,成本会成倍上升。

1.2 LangChain4j 的定位与核心价值

LangChain4j 做的事情,其实很像当年 JDBC 对数据库访问的收敛:它把"接大模型"这件事抽象成一组稳定的 Java 接口,让你不关心底层是哪个服务商、走的是什么协议。

它的核心价值可以归纳成几条:

第一,统一模型访问。你面对的是ChatLanguageModel这样一个接口,下面有面向本地模型的实现,也有面向各大云端服务的实现。换模型商,对业务代码几乎是透明的。

第二,把对话管理变成配置项。它内置了好几种记忆实现,最常用的MessageWindowChatMemory就像一个有最大长度的聊天记录本,满了自动丢最旧的消息,你不用自己写裁剪逻辑。

第三,把工具调用变成注解。在方法上加一个@Tool,框架自动把这个方法的描述、参数信息注册给模型,模型要调用时,框架自动完成方法反射和参数绑定。

第四,把 RAG 链路组件化。加载文档、切分文本、生成向量、检索相关内容,每一步都有现成组件,拼装起来比从头写要快得多。

打个比方,如果说裸写 HTTP 调用是手写 JDBC 的 Connection 管理,那用 LangChain4j 就相当于用了 MyBatis 级别的封装,你要做的是关注业务本身,而不是每次都跟底层 API 细节搏斗。

1.3 什么时候该用框架,什么时候不该用

我也见过一些过度设计的情况,所以想给你一个判断标准。如果你的需求只是"调一次接口,把结果返回给前端",那确实用不着引框架,手写几十行代码就够了,引入框架反而增加依赖和心智负担。

但只要你满足下面任意一条,就可以考虑上 LangChain4j:

  • 需要多轮对话,且要持久化或控制上下文长度;
  • 需要让模型调用你已有的 Java 方法,也就是 Function Calling;
  • 需要对私有文档做问答,也就是 RAG;
  • 需要在多种模型服务之间切换,避免被单一厂商锁死;
  • 项目不是一个人写着玩,而是多人协作,需要公认的抽象和约定。

我见过最典型的反面例子是:某团队自己封装了一个"对话工具类",刚开始很爽,后来要支持工具调用,又加了"工具分发器";要支持文档问答,又加了"向量管理模块",整个工具类膨胀到上千行,最后重构成本远高于当初省下的那点依赖引入成本。框架的价值不是在第一天体现的,而是在第二三个需求进来的时候。

2. 环境准备:5分钟跑通第一个对话程序

2.1 依赖和模型服务怎么选

环境要求不复杂,Java 17 及以上就行,构建工具用 Maven 或 Gradle 都可以。我自己习惯 Maven,后面示例都按 Maven 写,Gradle 只是坐标写法不同,换一下格式就好。

依赖方面,主模块和模型实现模块是分开的。这样设计的目的很明确:你用什么模型,就引什么模块,不会把无关的 SDK 全拖进来。以本地模型方案为例,你需要引入两个依赖:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>0.35.0</version> </dependency>

版本号我写的时候是 0.35.0 附近,你实际使用时去 Maven Central 查一下当前最新稳定版即可,不要纠结具体小版本。

接下来是模型服务的选择。这一块不少人绕了弯,我建议第一次尝试的读者直接用本地模型方案,比如 Ollama。理由是:不用申请密钥、不用考虑配额费用、断网也能调试,最适合把框架本身跑通。等你熟悉了这套抽象,再换云端服务商,只需要替换模型实现类和相关配置,业务代码基本不用动。

用 Ollama 需要先安装并启动它,然后在终端拉一个模型,比如:

ollama pull qwen2.5:7b

这个命令会下载一个 7B 参数的模型,普通开发机就能跑,速度也能接受。虽然推理速度和效果不如云端大模型,但作为新手入门完全够用。

2.2 最小可运行代码

依赖配好、模型拉好之后,写一个类就能跑。

import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; public class HelloLangChain4j { 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 方法,等待几秒到十几秒,你就能看到模型返回一段自我介绍。到这里,你的 Java 程序已经正式和大模型打通了。

这段代码里只有几个关键点:baseUrl是 Ollama 默认监听地址;modelName必须和你 pull 下来的模型名称一致;temperature控制随机性,值越大回答越发散。第一次跑通之后,你可以试着改 temperature,感受一下不同取值带来的差异,这是理解模型行为最快的方式。

2.3 这段代码背后发生了什么

很多人不关心内部链路,结果后面排错就抓瞎。我简单拆一下这段代码执行时的完整路径,你心里有个数,后面遇到问题就能快速定位。

程序启动后,OllamaChatModel内部会构建一个 HTTP 客户端,当你调用generate方法时,它做的事情是:

  1. 把你传入的字符串包装成一个 UserMessage;
  2. 按照 Ollama 的接口协议,发送一个包含模型名、消息、参数的请求到localhost:11434;
  3. Ollama 服务端把请求转给本地模型推理引擎,模型生成文本;
  4. 响应返回后,框架解析出回答内容,作为字符串交还给你。

整个过程中,你只管了"输入问题、拿到回答"这两件事,HTTP 细节、消息包装、协议解析都被框架消化掉了。这就是前面说的抽象价值。

这里有一个新手常踩的坑:如果你的 Ollama 还没拉模型,或者modelName写错,请求不会报错很快,而是会卡住一段时间后才抛异常,而且异常信息说的是"模型不存在"而不是"你写错了名字"。所以遇到这种情况,先去终端确认ollama list能看到对应模型。

3. 核心抽象拆解:消息、记忆与参数

3.1 三句话讲清消息体系

跑通最小示例后,下一个必须搞懂的是消息模型,因为你后面所有对话逻辑都建立在它上面。LangChain4j 把对话里的每一条消息抽象成ChatMessage,最常用的有三个子类:

  • SystemMessage:系统指令,用来设定模型的人设和行为规则;
  • UserMessage:用户输入;
  • AiMessage:模型生成的内容。

它们的职责可以类比成一个客服工作台:SystemMessage 相当于公司在开单前发给客服的"服务手册",规定语气、流程和禁区;UserMessage 是用户打来的诉求;AiMessage 是客服给出的回复。

一旦你需要精确控制对话,就不能再直接传字符串了,而是要把这些消息按顺序组装起来。比如:

List<ChatMessage> messages = List.of( SystemMessage.from("你是一个严谨的Java技术顾问,回答要简洁,不超过100字。"), UserMessage.from("什么是虚引用?") ); String answer = model.generate(messages);

这种写法看起来比传字符串麻烦,但它的价值在于你可以精确控制每一轮对话的输入,后面做复杂的多轮场景时,这是不可替代的能力。

3.2 对话记忆是怎么工作的

模型本身不记事儿,所以多轮对话必须你自己维护历史。最笨的办法是每轮都把全部消息发过去,但这有两个问题:一是消息越来越长,成本越来越高;二是超出模型的上下文窗口后,请求直接报错。

LangChain4j 给出的答案是MessageWindowChatMemory,中文可以理解为"滑动窗口记忆"。你可以把它想象成一张固定长度的便签纸,新内容写在末尾,写满了就把开头的旧内容撕掉,保证便签纸永远不超过上限。

实际使用中,建议通过 AiServices 来接触记忆,而不是自己手动维护消息列表。先定义一个接口,再绑定记忆:

import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.service.AiServices; interface Assistant { String chat(String message); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build(); System.out.println(assistant.chat("我叫张三,请记住这个名字。")); System.out.println(assistant.chat("我叫什么名字?"));

这里的withMaxMessages(10)表示最多保留 10 条消息。框架会自动把每次的 UserMessage 和 AiMessage 追加进记忆,并在下一次请求前拼接好发送给模型。你会注意到,第二个问题回答了"张三",因为它已经从记忆里取到了上一轮上下文。

这里有一个容易踩坑的点:withMaxMessages(10)的"10条"是指 5 轮对话左右,不要把它等同于 token 数。如果模型上下文很短(比如 4K、8K),建议把条数设置得更保守一些,否则可能触发上下文超限。等后面聊到参数,我再补充 token 估算的思路。

3.3 几个直接影响效果的关键参数

模型实现的 Builder 里通常会暴露一类影响行为的关键参数,我实际项目里最常用的是这几个。

temperature控制随机性,范围一般是 0 到 2。做知识问答、信息抽取这类任务,我建议调低到 0.1 到 0.3,让输出更稳定;做文案生成、头脑风暴,可以调到 0.7 到 0.9。这个值不是越高越好,超过 1 后回答质量往往会明显下降,因为模型开始"放飞自我"了。

maxTokens限制单次生成的最大长度。有些模型的默认值偏小,比如只有 256,你会发现让它写一段长文时突然截断。如果业务需要生成完整内容,记得显式调大,比如 1024 或 2048。注意这个值只限制"生成"的部分,不包括输入的历史消息。

timeout是超时时间,新手很容易忽略。本地小模型还好,云端大模型在高峰期响应可能很慢,默认超时时间如果太短,经常会出现"模型明明在跑,程序却抛超时异常"的假象。我一般设置成 60 秒以上,具体看你选的模型服务商建议值。

还有两个参数topP和frequencyPenalty,它们在部分模型实现里才暴露,而且不同模型对它们的处理方式不一样。我的建议是:刚入门阶段,先用好 temperature 和 maxTokens,其他参数等遇到具体问题再逐个研究,没必要一开始全都调一遍。

4. 实战:让大模型调用你自己的Java方法

4.1 Tool Calling 到底是什么

聊到 Tool Calling(工具调用),很多人第一时间想到的是"模型会帮我执行代码",这个理解是不准确的。模型的本质是一个文本生成器,它不会主动去查数据库、调接口、读文件,它只会根据你的指令,生成一段"我要调用某某工具,参数是什么"的格式化内容。

你可以把这个过程想象成老板和助理的互动:老板(模型)自己不做电话外呼,但它可以口述"你帮我查一下某公司的电话",助理(框架)听到指令后真正拿起电话去查,查到再把结果汇报给老板。老板根据结果继续做判断。

在 LangChain4j 里,你只需把一个普通 Java 方法的逻辑写出来,然后加一个注解,框架就会自动完成"模型生成调用意图 -> 框架反射调用方法 -> 执行结果回填给模型"的循环。这对 Java 开发者极友好,因为你的业务逻辑不需要做任何特殊封装。

4.2 一个可运行的天气查询示例

为了演示完整的工具调用,我写一个最简单的天气查询工具。这里不真的对接天气服务商,方法内部直接返回固定字符串,重点看工具方法的组织、注解的使用和 AiServices 的装配。

import dev.langchain4j.agent.tool.Tool; public class WeatherTool { @Tool("查询指定城市的当前天气情况") public String getWeather(String city) { // 这里在实际项目中会去调用第三方天气服务或查数据库 return city + "今天晴,气温25摄氏度,风力3级"; } }

接下来定义一个人工智能服务接口,并把工具注册进去:

import dev.langchain4j.service.AiServices; interface Assistant { String chat(String message); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTool()) .build(); String answer = assistant.chat("北京今天天气怎么样?"); System.out.println(answer);

运行后,模型会先判断"用户想知道天气,而这有一个 getWeather 工具可用",于是生成一个调用指令,框架自动把 city 参数填成"北京",执行方法,把返回的天气信息交给模型,模型最后综合生成一句自然语言回复。最终你看到的是类似"北京今天晴,气温25摄氏度,风力3级"的回答。

这里我不建议你一开始就追求复杂工具。先把一个无参数或有单参数的方法跑通,理解整条链路后,再逐步增加参数类型和业务逻辑。

4.3 设计工具时容易踩的坑

工具调用用起来爽,但设计不好,模型经常"听不懂"你的意思。我总结几个高频问题。

第一,工具的 description 写得太随便。@Tool的注解描述不是给人看的,是给模型看的。模型靠这段描述来决定"什么时候该调这个工具"。你写"查询天气",模型可能在你问"今天适合穿什么"时就不调用它;但你写"查询指定城市的当前天气情况,包括温度、风力、降水信息"后,模型就能更准确地把相关提问关联到这个工具上。

第二,参数类型过于复杂。模型生成参数依赖的是参数名和类型推断,复杂对象类型很容易让模型编造出一个不符合格式的值。我的经验是:工具方法的参数尽量用 String、Integer、Boolean 这类基础类型;如果需要传结构化的数据,拆成多个基础参数,比强行传一个嵌套对象稳定得多。

第三,忽略了工具本身的时间成本。工具方法如果涉及远程调用,要设置合理的超时时间。模型在等待工具结果时,整个请求链路是同步阻塞的,一个工具卡住,用户就得不到任何响应。所以在工具方法内部做好超时控制和异常兜底,比在框架层设置超时更重要。

第四,不要在一句话里让模型同时调用太多工具。部分模型能处理多个工具并行调用,但新手阶段很容易出现模型调用顺序错乱、参数互相引用的情况。先保证一次只调用一个工具,把链路跑稳,再考虑复杂编排。

工具调用这部分,不同模型的遵循能力差异非常大,换模型后一定要用几个固定问题回归测试一下,比如"如果工具返回空,模型会不会编造结果"。我实测下来,有的模型在你工具返回异常值时,宁可如实说"查询不到",也有的模型会硬编一个答案。这个问题,本质上靠工具侧做好结果兜底,不能指望模型自觉。

5. 实战:用 RAG 给你的模型装上私人知识库

5.1 为什么非要有 RAG

你迟早会遇到一个需求:让模型回答关于公司内部制度、产品文档、私有数据的问题。这时候你会发现,模型对这些内容一无所知,因为它训练时根本没见过你的数据。直接把它当客服,它只能瞎编,而且编得一本正经。

RAG(检索增强生成)解决的就是这个问题。思路很朴素:每次用户提问时,先从你的知识库里检索出和问题最相关的几段文本,把这些文本和问题一起交给模型,让它基于这些资料作答。相当于每次考试前先翻书,再把翻到的内容连同考题一起做开卷答题,而不是让模型背下整本书。

这套流程拆开就是三件事:把文档切块并向量化入库,提问时向量化并检索相似内容,把检索结果和问题拼装后交给模型生成。LangChain4j 把这三件事都封装成了组件,我下面带你跑一个最小闭环。

5.2 一个最小 RAG 落地示例

先引入嵌入式向量模型相关的依赖,这里使用的还是本地 Ollama 方案:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings</artifactId> <version>0.35.0</version> </dependency>

然后是整条链路的核心代码。我把它分步骤写清楚。

第一步,加载文档并切分。假设你有一个 docs 目录,里面放了一些 txt 文本:

import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; List<Document> documents = FileSystemDocumentLoader.loadDocuments(Paths.get("docs")); List<TextSegment> segments = DocumentSplitters.recursive(300, 50).split(documents);

这个切分器的作用是把长文本拆成一个个约 300 字符的片段,每个片段之间有 50 字符的重叠。为什么要有重叠?因为一个完整的意思可能在上一段结尾和下一段开头,没有重叠就容易在切分处丢失上下文。

第二步,生成向量并入库。这里需要用到嵌入模型,它的作用是文本变成一串表示语义的数字向量:

import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() .baseUrl("http://localhost:11434") .modelName("nomic-embed-text") .build(); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); embeddingStore.addAll(embeddings, segments);

注意nomic-embed-text这个嵌入模型需要提前用ollama pull nomic-embed-text拉下来。上面用的是内存向量库,数据重启即丢失。正式项目可以换用带持久化的向量存储组件,但对理解 RAG 原理来说,内存版本足够了。

第三步,装配到 AiServices 里。这里引入ContentRetriever,它会自动完成"检索 -> 拼接给模型"的动作:

import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.6) .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(contentRetriever) .build(); String answer = assistant.chat("根据内部文档,我们的报销流程是什么?");

这时模型回答问题时,会先通过嵌入模型将问题向量化,从向量库中找出最相似的 3 段文本,过滤掉相似度低于 0.6 的结果,然后连同问题一起交给大模型生成答案。整个检索对调用方是透明的,你看不到中间过程,但答案已经基于你的文档产生了。

5.3 影响检索质量的关键点

跑通很容易,跑得好很难。我调试 RAG 的经验里,影响最终效果的因素按重要性排序如下。

文档切分的粒度和策略。切得太粗,检索到的片段可能包含大量无关内容,模型容易被带偏;切得太细,单个片段信息量不足,模型又没法给出完整答案。300 到 500 字符是一个比较通用的起点,具体要看你文档的语境。重叠值建议设成片段大小的 10% 到 20%,不要省。

检索数量的设置。maxResults不是越多越好。给模型塞太多片段,不仅增加 token 消耗,还容易引入噪声。我一般从 3 开始调,如果答案总缺细节就上调到 5,如果出现答非所问就回调。

相似度阈值。minScore如果不设,向量库可能把所有片段都捞出来,哪怕完全不相关。设得太高,又会出现"明明有答案但检索不到"的情况。我建议先跑一条真实问题,打印出每个候选片段的相似度分数,再根据分布定阈值,不要凭空拍一个数。

数据格式的清洁度。RAG 最忌讳"知识库是个垃圾场"。PDF 扫描件、带大量页眉页脚的网页、格式混乱的表格,切分后都是碎片化噪声。我在实际项目里都会先做一轮清洗,把格式统一成相对干净的纯文本,检索效果提升非常明显。

最后提醒,RAG 不是"一次配置永久生效"。你的文档更新了,向量库也要跟着更新。最简单的做法是更新时把相关文档删掉重新走一遍入库流程。内存向量库虽然简单,但重启后要重新加载,我建议在本地调试阶段就把这份加载逻辑写成独立的初始化代码,方便复用。

6. 常见问题与实战心得

6.1 我踩过的五个典型坑

做一个新手教程,如果不把排错经验讲透就有点说不过去了。下面这五个问题,我在不同阶段、不同项目里都遇到或听同行提起过,每个都是真实场景,可以按表排查。

现象常见原因解决办法
请求卡住很久后超时模型服务未启动、模型名错误、网络不通先用 curl 测试模型服务接口;确认ollama list里有对应模型
中文回答乱码或返回内容异常终端编码问题,或未设置 UTF-8Maven 里配置项目编码 UTF-8;确认模型本身支持中文
多轮问答时上下文越来越慢记忆窗口过大,token 超限调小MessageWindowChatMemory.withMaxMessages的数值
工具调用总是没反应工具方法不是 public、没有 @Tool 注解、描述不清晰确认方法修饰符和注解描述;先用最简单的工具验证链路
RAG 检索出来一堆无关内容切分太粗或 minScore 太低打印候选片段相似度,调低/调高阈值;优化切分粒度

第一个问题其实是新手最容易误判的。很多人发现"程序没反应",第一反应是去改代码,结果最后发现问题出在服务端压根没有模型。排错顺序应该是:先确认模型服务,再确认请求参数,最后才是排查代码逻辑。

6.2 流式输出怎么接

前面所有示例都是同步等待完整结果返回。真实业务里,一个长回答可能要等十几秒,用户看着界面干等十秒,体验非常差。解决办法是流式输出,也就是一个字一个字地"吐"出来。LangChain4j 为此提供了StreamingChatLanguageModel。

先构建流式模型:

import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.chat.StreamingChatLanguageModel; import dev.langchain4j.model.chat.response.StreamChatResponseHandler; StreamingChatLanguageModel streamingModel = OllamaStreamingChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build();

然后通过回调接收增量内容:

streamingModel.chat("用三句话介绍Java的垃圾回收机制", new StreamChatResponseHandler() { @Override public void onPartialResponse(String token) { System.out.print(token); } @Override public void onCompleteResponse(ChatResponse response) { System.out.println(); System.out.println("回答结束"); } @Override public void onError(Throwable error) { error.printStackTrace(); } });

onPartialResponse会收到模型逐步生成的文本片段,你的前端可以拿到这些片段后实时展示;onCompleteResponse在回答完整结束时触发。流式接口和同步接口的模型实现类是分开的,如果你只构建了同步模型,是不能直接"变成"流式的,需要换成对应的 Streaming 实现类。

这一块的实际体验差异很大,本地小模型由于推理速度本就偏慢,流式的效果会更明显。在正式项目里,把流式接入 WebSocket 或 SSE,交互体验会提升一个档次,值得优先做。

6.3 让模型输出结构化 JSON

最后一个实操环节:让模型输出结构化数据。很多时候你需要的不是一个自然语言段落,而是一个可以直接序列化成对象的 JSON。比如从一段文本里抽取人员信息,或者做关键词识别。

LangChain4j 里最优雅的做法是利用 AiServices 的返回类型推断。当你把接口方法的返回值定义成 POJO 时,框架会引导模型按照对应结构输出。先定义一个实体类:

public class Person { private String name; private Integer age; private String city; // getter / setter 省略 }

再定义一个抽取接口:

import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; interface PersonExtractor { @UserMessage("从文本中抽取人物信息:{{text}}") Person extractPerson(@V("text") String text); }

使用起来非常直观:

PersonExtractor extractor = AiServices.builder(PersonExtractor.class) .chatLanguageModel(model) .build(); Person person = extractor.extractPerson("张三今年25岁,目前住在杭州。"); System.out.println(person.getName() + " / " + person.getAge() + " / " + person.getCity());

框架会自动要求模型按 Person 的字段结构返回 JSON,并完成反序列化。如果字段缺失,框架还能带着错误信息重新请求模型补全,这个机制对实际生产的价值很大,建议你在项目里优先用这种声明式方式,而不是自己写提示词再手工解析 JSON。

用这个方法时有一个注意事项:实体类字段要和你的业务字段一一对应,不要出现多余的大字段,否则模型容易不知道怎么填。字段数量尽量少,类型尽量简单。等你把这种抽取玩熟了,可以进一步研究JsonSchema相关的底层能力,但对大多数业务场景,接口返回值绑定这种方式已经足够。

同一个人工智能服务接口里,你还可以组合使用@Tool工具方法和chatMemory记忆。这也是我建议用 AiServices 而不要自己手动拼消息的根本原因:工具调用、记忆、结构化输出、RAG 这些能力,统一在一个接口声明和 Builder 配置里完成,可读性和可维护性比手写胶水代码好太多。我在实际项目里把这一套沉淀成通用模板后,新接入一个业务场景,往往只需要新增一个工具类和一份知识库文档,开发效率提升非常明显。

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

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

立即咨询