
使用LangChain4j:实现一个AI对话助手
需求
实现一个挂号小助手的AI对话功能,患者可以通过描述自己的症状,AI根据描述告知患者应该挂哪个科室。
需求比较简单,主要是通过这个需求来学习运用LangChain4j框架的功能,包括大模型平台接入,通过chatModel与大模型进行对话,自定义系统提示词,实现会话记忆,结构化输出,外接RAG知识库,Tool,mcp,护栏机制,流式输出;
开始之前的碎碎念
先简单聊下LangChain4j的作用,有一个基本的概念。目前关于AI仍然处于探索可扩展的阶段,也就是说还有一些能力边界和某些场景下的处理方式还不够完整,在探索的过程中有些问题有了阶段性的解决方式,那为了方便实现这些方式,就有了各种AI框架,比如本文介绍的LangChain4j。这种框架和spring、mybatis等框架一样,都是为了解决某些场景化的问题,只不过LangChain4j解决的问题场景是我们之前所没有接触到的。不过不用担心,既然是框架,学习和使用起来就不会太难,阅读完这篇文章之后,你一定会有所收获!
本文使用到的环境
- jdk版本:21,LangChain4j最低支持jdk17,jdk21正式加入虚拟线程,一步到位。
- 开发框架:springboot 4.0.x,选择支持LangChain4j的版本即可。
- AI框架:LangChain4j,因为本文讲的就是它。
- qwen-max大模型:阿里的qwen-max,选择阿里系的原因,是因为阿里的主要技术栈就是java,对于java的兼容性会更好一些,选择qwen-max的原因是这个模型的能力相对比较均衡,也可以使用qwen-plus,但是能力比qwen-max稍微差一点点。
- 本文使用外部地址。关于AI相关功能的集成说明和示例代码在LangChain4j文档中都可以找到。
- 本文的项目源码:https://github.com/socialzhy/ai-chat
- LangChain4j文档(英文):https://docs.langchain4j.dev/
- LangChain4j文档(中文):https://langchain4j.cn/
- LangChain4j社区GitHub:https://github.com/langchain4j/langchain4j-community
- maven仓库(可以查询各种大模型平台):https://mvnrepository.com/artifact/dev.langchain4j
- 百炼平台:https://bailian.console.aliyun.com/
- 智谱BigModel:https://open.bigmodel.cn/
- MCP市场:https://mcp.so/zh,最好使用官方服务,不然可能会有安全隐患。
一、实现一个最简单的对话功能
LangChain4j官方的示例的默认实现是使用的open-ai,并且支持的大部分是国外的大模型,对比国产大模型来讲,开发成本会有一点高,使用也没有那么方便。所以想接入国产大模型就需要使用到LangChain4j社区提供的代码和依赖。
可以在社区GitHub的models中看到一些眼熟的模型,qianfan、zhipu、和本篇文章使用的dashscope,也就是阿里系的大模型。
创建项目:选择maven、java版本选择21、配置文件类型选择YAML方便配置,springboot版本选择4.0.x,依赖选择spring web和lombok。
引入依赖:LangChain4j社区的dashscope大模型的springboot整合包,也可以在maven仓库中选择其他你喜欢的其他大模型整合依赖
1 | <dependency> |
申请大模型平台KEY:在阿里百炼平台申请一个API KEY,如何使用的是其他大模型平台,就在其他大模型平台申请对于的API KEY,注意key不要泄露!
配置参数:在yml文件中配置模型参数, api-key就是上一步申请的API KEY。
参数简单说明:langchain4j - 社区 - 阿里大模型 - 对话模型 - 模型名称 - API KEY,参数的设置方式是层层递进的关系,之后在配置其他参数时,可以便于理解为什么这么配。
1 | langchain4j: |
新建一个service类:实现一个chat方法,调用这个方法就可以实现与大模型的对话啦,就像deepseek、chatGpt那种。
简单说明下代码
- qwenChatModel:引入千问的对话模型
- UserMessage:用户消息,UserMessage.from可以传入多种类型的Content,包括AudioContent、ImageContent、PdfContent、TextContent、VideoContent,也就是说传入的参数可以是文本,也可以是图片等其他用户消息,传入String msg后也是封装成了TextContent进行的实现。注意如果使用了其他类型的Content需要大模型支持多模态。
- ChatModel.chat:调用大模型对话接口
1 |
|
- 测试:可以创建一个测试类对这个对话方法进行测试
1 |
|
1 | AI 回复的消息:AiMessage { text = "你好,土豆丝!很高兴认识你。有什么我可以帮助你的吗?或者你想聊些什么呢?" toolExecutionRequests = [] } |
一个最简单的对话功能就实现了。
二、多模态
- 白话解释:简单理解为可以处理文本以外的数据类型的能力。比如图片、视频等。
- 上面举得例子是直接传入了String msg,也就是文本,为了实现多模态,新建一个方法,入参改为UserMessage,方便调用方自定义Content类型
1 | /** |
- 测试
1 |
|
1 | AI 回复的消息:AiMessage { text = "我目前无法直接查看或访问图片。如果你能提供关于图片的一些描述或者细节,比如图片中的主要对象、颜色、场景等信息,我会很乐意帮你进一步描述或分析这张图片的内容。" toolExecutionRequests = [] } |
可以看到目前所使用的大模型是不具备多模态功能的,如果想使用相关功能可以切换其他支持多模态的大模型,这里只做如何实现多模态对话的例子。
ps:在写到这里的时候我有一个想法,我想确认下大模式是不支持多模态还是大模型不具备联网功能,因为如果是不具备联网功能也可能导致上面的回复。于是我做了一个实验,ImageContent.from支持uri和base64两种方式,我想是不是可以使用base64的方式去验证,但是很遗憾,base64字符串过长无法传入,于是我调用了普通文本对话的方式传入了工具网址的icon,让大模型来解答。
1 |
|
1 | AI 回复的消息:AiMessage { text = "我直接访问了您提供的链接(https://tools.tdsay.cn/favicon.ico),但作为一个文本交互的AI,我无法直接查看或分析图像内容。不过,通常情况下,favicon.ico 文件是用来显示网站图标的小图片,它会出现在浏览器标签、书签栏等地方。 |
上面的消息回复比较明确了,“我直接访问了您提供的链接”也就是说大模型具备联网查询功能,
“作为一个文本交互的AI,我无法直接查看或分析图像内容” ,确实不具备多模态功能。
三、系统提示词
白话解释:提示词分为两种,一种是系统提示词、一种是用户提示词,系统提示词是系统默认设置的提示词,一般来说用户是不可以直接看到的,多用于设置AI的定位和规则约束。目的在于更好的去理解和解决用户提出的问题。
1
2
3
4
5
6
7你是一位专业的挂号助手,擅长根据用户描述的症状或已知疾病,快速、准确地推荐合适的就诊科室。你的建议仅供分诊参考,不能替代医生的诊断。
请围绕以下 4 个方向给出帮助:
1、常见症状分诊:如发热、咳嗽、头痛、腹痛、胸痛、头晕、呕吐、皮疹等,分别建议挂什么科(呼吸内科、神经内科、消化内科、心内科、皮肤科等)。
2、特定疾病对应科室:如高血压、糖尿病、骨折、眼红、牙痛、耳鸣等,告知首选科室,并提示可能涉及的相关科室。
3、急诊与危重指征:明确哪些情况必须去急诊(如剧烈胸痛、大出血、意识丧失、严重呼吸困难等),避免耽误病情。
4、特殊人群与复杂情况:儿童(14岁以下优先儿科)、孕产妇(产科或妇产科)、外伤(急诊外科或骨科)、老年人多种病共存(可建议老年科或先看全科)。
比如有一些患者生病了但是不知道挂什么科室,可以通过上述提示词,规定了AI的角色,按照什么方式去回答问题,重点关注方向是什么。
实现原理:开发者自定义程序的系统提示词,用户在调用AI对话时,系统会默认将上述提示词一同发给大模型,不需要用户每次手动编写。
实现方式也很简单,首先定义系统提示词常量,在调用ChatModel.chat时传入就好。注意ChatModel.chat是不区分提示词类型的。
1 | /** |
1 | AI 回复的消息:AiMessage { text = "你好,土豆丝!很高兴为你提供帮助。如果你有任何健康上的疑问或需要就医方面的建议,请告诉我你的症状或具体问题,我会尽力为你推荐合适的就诊科室。请描述一下你的情况吧。" toolExecutionRequests = [] } |
可以看到已经生效了
四、AI Service
白话解释:除了上面的通过chatModel的实现方式,langchain4j还提供了AI Service的实现方式,AI Service可以简单理解为在chatModel模式下做了一层封装,封装成了一个更高级的API,对于封装来讲越底层的越灵活,越高级的越方便,大家可以根据自己的场景自由选择。
首先先引入依赖
1
2
3
4
5<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.1.0</version>
</dependency>在resources目录下创建一个系统提示词文件system-prompt.txt,并在里面编写好系统提示词。
创建一个AI service接口
1 | public interface AiChatService { |
- 接口是无法直接调用的,所以还需要创建实现类,使用Factory模式进行创建。
1 |
|
代码说明:AI Servie的调用和我们之前先写的Service接口,在写接口的impl实现一样,都是通过spring对service bean进行注入,提供service接口调用,langchain4j的AiServices.create方法通过反射以及传入ChatModel帮我们生成好了一个接口的实现,也就是不需要我们手动的去实现impl,这就是前面说的高级封装。
- 测试一下
1 |
|
1 | 你好,土豆丝!很高兴为你提供帮助。请告诉我你或需要帮助的人目前的症状或已知的疾病情况,我会根据你的描述推荐合适的就诊科室。如果涉及紧急情况,请立即前往医院急诊科或拨打急救电话。 |
可以看到,通过Ai Service的方式实现成功了。
- 还有一种更方便的不需要手动创建bean的方式,先引入依赖
1 | <dependency> |
- 注释掉AiChatServiceFactory,不然bean会冲突。在AiChatService上加一个注解
1 |
|
然后直接调用也是可以的,但是不建议使用这种方式,因为对于AI Service来讲,后续还有一些参数需要配置,如果使用注解的方式,这些参数都需要在注解上进行指定,而且如果你的bean的创建需要一些自定义的流程,使用这这种方式将会很麻烦。这就是封装的代价,封装和灵活总要有一些取舍。
五、会话记忆ChatMemory
- 白话解释:我们在调用大模型时,可以简单理解为是无状态调用,也就是说大模型并不知道对话的历史,每次调用都是一个新的对话,包括提示词也好、之后使用的MCP也好,等等大模型都是没有记忆的。比如第一次调用大模型说我是土豆丝,再次调用问我是谁的时候,大模型是不知道的,所以需要有一个地方记录之前的对话并发个大模型,让大模型知道上下文的数据,方便之后的处理。
- 实现方式:可以自己记录找地方存起来,也可以通过LangChain4j框架为我们提供的方式进行实现。
1 |
|
AiServices可以通过构造器的方式对serivice进行构建,方便自定义的去配置功能组件,比如我们现在要使用ChatMemory,就可以直接构建一个并组装进去。
- 测试一下
1 |
|
1 | 第一次回复:你好,土豆丝!很高兴为你提供帮助。请告诉我你或你的家人目前遇到了什么健康问题,比如有什么症状或者已知的疾病,我将根据你提供的信息推荐合适的就诊科室。如果不确定具体症状,也可以描述一下具体情况,我会尽力给你建议。 |

可以看到会话记忆功能已经实现了。通过debug可以看到AiService是一个代理对象,对象里的chatMemoryService里已经存储了对话内容,注意不仅是用户发的消息,AI回复的消息也是会存储的。
- 会话记忆默认是存储在内容中的,如果需要持久化,比如存储到数据库中,LangChain4j也提供了持久化的方法。
简单介绍:创建一个ChatMemoryStore的实现类实现ChatMemoryStore,ChatMemory 也可以通过构造器的方式进行构造,包括最大条数,和chatMemoryStore,把chatMemoryStore实现类传入构造器就可以了,还是挺简单的。后续的文章介绍开发Agent智能体的时候在进行实现,
六、会话隔离
- 白话解释:在实际对话中,比如土豆丝和土豆片两个人都与大模型进行对话,那么如何进行区分呢,总不能土豆丝的消息回复到土豆片的对话里对吧。
所以为了进行区分,LangChain4j引入了memoryId这个参数,简单理解为会话id。
- 实现方式也很简单,AiChatService新增一个方法,增加memoryId参数并配置入参的注解
1 | @SystemMessage(fromResource = "system-prompt.txt") |
然后在AI Service进行构造
1 | @Bean |
- 测试一下
1 |
|
1 | 第一次回复:你好,土豆丝!很高兴为你提供帮助。请告诉我你或你需要帮助的人目前的症状或已知疾病,我会根据你的描述推荐合适的就诊科室。如果还有其他相关问题,也欢迎一并告知。 |

可以看到会话成功被隔离了,通过debug可以发现LangChain4j在chatMemories里对传入的不同的memoryId进行了区分处理。
七、结构化输出
白话解释:上面讲述的大模型返回的消息都是纯文本,如果想对大模型回复的消息做一些处理,比如转成json储存到数据库中,都需要对返回的消息进行结构化的处理
实现方式:
- JSON Schema:最精准,大模型本身提供的能力,但是稍有一些麻烦,需要自定义必须有哪些返回值,可以阅读上面的文档。
- Prompt + JSON Mode:可以阅读这篇文章https://glaforge.dev/posts/2024/11/18/data-extraction-the-many-ways-to-get-llms-to-spit-json-content/
- Prompt:通过提示词进行输出,最方便,相对于上面的两种不够稳定,但是也够用,本文介绍的就是这种方式
- 创建一个java类,用于接收并格式化消息,AIService新增一个chat方法返回值改为创建的类
1 |
|
- 测试一下
1 |
|
1 | Patient(name=土豆丝, disease=头晕, suggestionList=[儿科, 神经内科]) |
可以发现已经成功了,如果你发现有的时候这种方式无法成功的实现结构化输出,可以参考文档使用JSON Schema的方式进行实现,准确度和可靠性都是最高的一种模式。
八、RAG
啥是RAG:百度百科的定义,检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合检索和生成技术的模型。它通过引用外部知识库的信息来生成答案或内容。
白话解释:LLM大模型是通过数据进行训练的,它的通用性比较强,但是问一些比如关于企业内部的数据问题,它可能回答不知道,也可能产生幻读,乱给出一些答案。因此就需要引入外部的数据来供大模型进行判断。
但是外部数据格式不统一,可能是千奇百怪的,如何才能使得大模型可以检索这些数据呢,答案是“向量”,所有的数据都可以通过算法来转换成向量。
向量的数据格式大概长这样(421,123,123,5457,67,……)每一个数字都是一个维度,维度越多,对于数据的描述越精确(基本上), 根据向量算法可以找到与问题最匹配的数据。
也就是说需要处理一些大模型不知道的数据时,我们要先将数据转换成向量,然后才能进行答案的生成,数据转换的过程称之为embedding。
embedding也是各大模型平台提供的一个服务,可以通过调用平台接口,把数据转换成向量。
关于rag的实现方式LangChain4j有三种实现方式
- Easy RAG:也就是在依赖包中内嵌了一个小型的embedding实现,不需要调用外部就可以实现,但是效果一般,只做展示使用,没有太大价值。
- 核心RAG:常用实现方式,本文介绍的就是这种方式。
- 高级RAG:有一些高级特性,支持查询转换、查询路由、内容聚合等,感兴趣的可以阅读官方文档。
embedding也选择阿里的text-embedding-v4
1 | embedding-model: |
1 |
|
说明下代码
- FileSystemDocumentLoader是langchain4j提供的文档读取器,可以对本地的文件进行读取
- 切割:维度有很多种,比如按照段落、按照段落切割、按照单词切割、按照正则表达式切割等,本文采用的是按照段落切割。
对文档进行切割,参数为最大字符和重叠字符,为什么要对文档进行切割呢,因为在把文档转为向量时,相当于对文档进行拆解,就像章节一样,每部分讲解的内容是不一样的,这样在对问题进行查询时,匹配的才会更准确。重叠的作用是当文档切割时,可能会超出最大字符数。所以在切割时可能会导致切割后的结果不完整,重叠的含义就是前一个切分会有后一个切分的前100个字符的内容,后一个切分会有前一个切分的前100个字符的内容,保证内容的完整性。
- 自定义文档加载器:作用就是如何把文档转成向量,为文档碎片配置元信息方便匹配,配置段落分割器、embedding模型、Embedding存储位置;
- 自定义内容加载器:maxResults可以 自定义最多返回结果,自定义minScore匹配程度(在实际开发中需要不断调整)
- 我们在大模型问答时,有的时候可以看到大模型回答问题引用了哪些文档,这个功能可以使用Result作为AiService返回结果进行实现,而且还可以看到token的使用情况。
1 | Result<String> chatWithRag(String msg); |

九、Tool Caling
- 白话解释:LLM大模型本身只有推理能力,但是没有动手能力,RAG是为大模型加了一个外部知识库,而Tools相当于为大模型增加了手脚,相当于如虎添翼。实现原理是告诉大模型有哪些工具可以使用,比如在挂号时查询某个科室还有几个号,这种不使用工具大模型是完成不了的,所以基于这种场景催生了Tool。
整体的调用流程是,用户提问现在内科还有几个号,调用我们开发的AI程序,我们的AI程序会把用户的问题和我们自己开发的Tool工具一起发给大模型,大模型在思考之后,告诉我们的AI程序,需要调用Tool,AI程序调用Tool之后把调用的结果发给大模型,大模型拿到结果之后,将结果和回答组装成结果返回给我们的AI程序,AI程序对用户进行一个结果的展示。
- 实现方式很简单,创建一个Tool类,新增一个Tool方法,方法上增加一个@Tool注解,注解包含两个参数,name是定义Tool的名称,value是对这个工具进行描述。方法入参是对参数进行一个描述。以上建议使用英文,效果会更好一些。
在AiChatServiceFactory构建AIService时增加新增的Tools即可。AiServices.builder.tools(new RegistrationsNumTool())
1 | /** |
十、MCP
白话解释:Tool的工具服务标准,相当于把Tool从本地抽离,可以单独启动一套提供Tool的MCP的服务,这样做的好处一是解耦,二是可以直接搭建一个MCP平台,对外提供MCP服务。
本文主要讲解LangChain4j的使用,就不搭建本地的MCP服务了,直接使用智谱的MCP的联网查询服务
引入依赖,配置智谱平台API KEY,创建McpConfig,AiChatService构建AiServices.builder.toolProvider(mcpToolProvider)
1 | <!-- mcp依赖 --> |
1 | bigmodel: |
1 |
|
- 测试
1 |
|
1 | 网址 https://tools.tdsay.cn/ 是一个提供多种在线工具服务的网站,从搜索结果中可以看到它提供了如下几种工具:............. |
十、护栏机制
- 白话解释:所谓护栏就是在调用AI之前干什么,调用之后干什么,就和AOP一个意思。还有点filter的意思,可以实现多个护栏依次调用,根据返回结果进行处理,返回结果以及处理方式可以参考官方文档。
- 支持输入护栏和输出护栏,本文以输入护栏敏感词校验为例。
编写完护栏之后依旧AiChatService构建AiServices.builder.inputGuardrails(new SafeInputGuardrail())
1 | /** |
1 |
|
十一、SSE流式输出
白话解释:在我们使用deepseek或者gpt时,响应的结果就像打字机一样流式输出,实现的原理和LLM的原理有关,LLM本质是一个随机推理大模型。每次都是预测下一个输出字符是什么,所以可以实现这种流式效果,这种效果也方便告知用户我这边一直在处理结果,不用让用户一直等待。
注意:使用流式输出之后,不支持结构化输出。原因也很好理解,不是整体输出无法准确构建。
实现:引入流式输出依赖,配置流式输出大模型,AiService定义流式方法,AiServices构建流式输出model,AiServices.builder.streamingChatModel(qwenStreamingChatModel),创建Controller提供接口调用
1 | <!-- 流式输出 --> |
1 | streaming-chat-model: |
1 | Flux<String> chatStream( int memoryId, String msg); |
1 |
|
1 |
|
调用结果

十二、前端页面生成
- 由于是对话形式,前端就使用一个简单的页面进行实现就可以,可以使用Trea或者cursor直接生成前端项目代码,下面是提示词。
1 | 你是一位专业的前端开发,请帮我根据下列信息来生成对应的前端项目代码。 |
- 生成效果根据使用的不同模型会有所差别,但是这种比较简单的页面,微调几把也就实现了,下面是实现结果图。

总结
以上,就是关于LangChain4j的全部知识分享,还有一些没有讲到的内容可以查看LangChain4j的官方文档进行查阅。


