传统互联网开发,万物皆可HTTP;AI开发,万物皆可MCP。整篇纯干货,特别干!
一、JAVA AI开发
为什么用JAVA做AI开发?在大众印象中,AI 开发几乎等同于 Python —— TensorFlow、PyTorch、Hugging Face……这些主流框架都以 Python 为主。
但事实上,Java 在 AI 领域并非“边缘角色”,而是在企业级、生产系统和大规模工程化场景中扮演着不可替代的角色。
Java 不是训练大模型的首选,但它完全可以胜任任何 AI 应用场景。
目前java有Spring Ai、Langchain4j等出色的AI应用开发框架。
二、Langchain4j开发
Langchain4j,类似 Python 的 LangChain,专注 LLM 应用开发。具有轻量、灵活、支持多种模型等优势,适合快速构建 AI Agent、RAG、Function Calling、MCP调用等场景。
本文所有实例使用的langchain4j版本为1.10.0-beta18,也是目前最新的已发行版。
<properties>
<langchain4j.version>1.10.0-beta18</langchain4j.version>
<langchain4j.spring.version>1.10.0-beta18</langchain4j.spring.version>
</properties>
开发过程中,若遇到问题,可查看官方文档:
https://docs.langchain4j.dev/
也有中文官方文档,但更新滞后:
https://docs.langchain4j.info/
开源代码地址:
https://github.com/langchain4j/langchain4j
若开发过程中遇到本文未提及的问题,可查看官方文档和官方代码实例。
1、Spring boot开发基础依赖
使用Langchain4j开发,spring boot项目需要如下基础依赖:
<!-- AiService的相关依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
若外接第三方大模型(如Deepseek、千问)或第三方模型部署平台(如阿里百炼)等,需要增加依赖:
<!--LangChain4j 针对 open ai 的 Spring Boot starter-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
若对接本地部署的大模型(如ollama),则可选如下依赖:
<!--LangChain4j 针对 ollama 的 Spring Boot starter-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
若应用需要流式响应,则需要增加依赖:
<!-- LangChain4j 流式调用依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- spring boot 流式http -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2、和AI简单对话
准备好spring boot项目,引入依赖,我们就可以通过程序和AI大模型对话了。
(1)配置
基于open-ai的基础配置如下(这里以阿里百炼举例,根据实际使用场景修改base-url和api-key即可):
langchain4j:
open-ai:
chat-model:
#阿里百炼
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: xxxxxxxx
model-name: qwen3-max
log-requests: true
log-responses: true
若需要更详细的参数配置,可以查看dev.langchain4j.openai.spring.Properties。
基于ollama的基础配置如下:
langchain4j:
ollama:
chat-model:
base-url: http://192.168.2.131:11434
model-name: qwen3:8b
log-requests: true
log-responses: true
若需要更详细的参数配置,可以查看dev.langchain4j.ollama.spring.Properties。
(2)使用ChatModel对话
可使用ChatModel直接和LLM对话,代码如下(注:此处的@SaIgnore是由于我的项目中使用了Sa-Token,与本实例无关,可自行去掉):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.annotation.SaIgnore;
import dev.langchain4j.model.chat.ChatModel;
@SaIgnore
@RestController
public class ChatController {
@Autowired
private ChatModel chatModel;
@GetMapping("/chat")
public String model(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return chatModel.chat(message);
}
}
启动程序后,浏览器输入(注:我这里使用了网关转发,中间的”/ai”是网关路由,若是普通项目把”/ai”去掉即可,ip也可换成自己的IP或localhost):
http://192.168.2.131:8080/ai/chat?message=你好
调用结果如图所示:

查看程序控制台日志:
2026-01-17 09:23:27[0;39m [32m[XNIO-1 task-2][0;39m [34mINFO [0;39m [1;35md.l.h.client.log.LoggingHttpClient
[0;39m - HTTP request:
- method: POST
- url: http://192.168.2.131:11434/api/chat
- headers: [Content-Type: application/json]
- body: {
"model" : "qwen3:8b",
"messages" : [ {
"role" : "user",
"content" : "你好"
} ],
"options" : {
"stop" : [ ]
},
"stream" : false,
"tools" : [ ]
}
[31m2026-01-17 09:23:31[0;39m [32m[XNIO-1 task-2][0;39m [34mINFO [0;39m [1;35md.l.h.client.log.LoggingHttpClient
[0;39m - HTTP response:
- status code: 200
- headers: [Content-Length: 1047], [Content-Type: application/json; charset=utf-8], [Date: Sat, 17 Jan 2026 01:23:31 GMT]
- body: {"model":"qwen3:8b","created_at":"2026-01-17T01:23:31.9754106Z","message":{"role":"assistant","content":"👋 你好!有什么我可以帮助你的吗?","thinking":"好的,用户发来“你好”,我需要回应。首先,保持友好和热情是关键。可以简单回应,比如“你好!有什么我可以帮助你的吗?”。不过,用户可能希望更自然一些,所以可以加点表情符号,比如“👋 你好!有什么我可以帮助你的吗?”。这样既亲切又不会显得太正式。另外,考虑到用户可能只是打招呼,不需要太复杂的回复,保持简洁。同时,要确保语气友好,让用户感觉被欢迎。可能用户之后会提出问题,所以保持开放式的提问,鼓励他们继续交流。检查是否有拼写错误,确保回复正确。最后,发送回复。\n"},"done":true,"done_reason":"stop","total_duration":4828681600,"load_duration":1660473700,"prompt_eval_count":11,"prompt_eval_duration":50315400,"eval_count":150,"eval_duration":3096149700}
以上输出基于ollama本地部署的qwen3:8b模型。
和模型对话,若需要了解更多细节,可参考官方文档:
https://docs.langchain4j.dev/tutorials/chat-and-language-models
3、用Ai Service与大模型对话
使用Ai Service与大模型对话,我们需要一个接口,还涉及@SystemMessage、@UserMessage等注解。
(1)新建对话接口
创建一个对话接口:
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface IMyAiService1 {
@SystemMessage("你是位和蔼可亲的电商客服助手,只回答电商相关问题,当用户问题不涉及电商时,你需要提示用户你只解答电商问题。")
String chat(@UserMessage String message);
}
定义的接口中,我们用到了@SystemMessage注解和@UserMessage注解。这是两个核心的提示工程(Prompt Engineering)注解,它们用于声明式地定义大模型(LLM)对话中的角色与输入内容结构,让你无需手动拼接 prompt,就能精确控制 AI 的行为。
- @SystemMessage的作用是定义 AI 的“身份、性格、行为准则”,属于系统级指令(system prompt)。
- @UserMessage的作用是标记用户输入参数,构成用户提问内容。
在当前案例中,我们用字符串指定了@SystemMessage,把用户的输入作为@UserMessage。在实际使用过程中,可以使用文件加载,如:
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface IMyAiService1 {
@SystemMessage(fromResource = "system-message.txt")
String chat(@UserMessage String message);
}
更多@SystemMessage细节可参考文档:
https://docs.langchain4j.dev/tutorials/ai-services#systemmessage
更多@UserMessage细节可参考文档:
https://docs.langchain4j.dev/tutorials/ai-services#usermessage
(2)使用AiServices创建Ai Service
新建一个配置类,用于创建Ai Service。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.lovecto.yuen.agents.service.IMyAiService1;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
@Configuration
public class AiServiceConfig {
@Autowired
private ChatModel chatModel;
/**
* 使用AiServices builder 创建AiService
* @return
*/
@Bean
public IMyAiService1 myAiService() {
return AiServices.builder(IMyAiService1.class)
.chatModel(chatModel)
.build();
}
}
这里使用AiServices builder构建Ai Service,构建的就是我们刚刚创建的IMyAiService1,构建后将其托管到Spring容器。
更多Ai Service相关的文档可参看:
https://docs.langchain4j.dev/tutorials/ai-services
(3)创建http接口看对话效果
在我们之前的ChatController中加入如下代码:
@Autowired
private IMyAiService1 aiService1;
......
@GetMapping("/chat2")
public String aiServiceChat(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return aiService1.chat(message);
}
完整代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.lovecto.yuen.agents.service.IMyAiService1;
import cn.dev33.satoken.annotation.SaIgnore;
import dev.langchain4j.model.chat.ChatModel;
@SaIgnore
@RestController
public class ChatController {
@Autowired
private ChatModel chatModel;
@Autowired
private IMyAiService1 aiService1;
/**
* 基于chatModel直接对话
* @param message
* @return
*/
@GetMapping("/chat")
public String model(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return chatModel.chat(message);
}
/**
* 基于Ai service对话
* @param message
* @return
*/
@GetMapping("/chat2")
public String aiServiceChat(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return aiService1.chat(message);
}
}
打开浏览器输入:
http://192.168.2.131:8080/ai/chat2?message=你好
效果如下:

对话日志如下:
2026-01-17 10:04:33[0;39m [32m[XNIO-1 task-2][0;39m [34mINFO [0;39m [1;35md.l.h.client.log.LoggingHttpClient
[0;39m - HTTP request:
- method: POST
- url: http://192.168.2.131:11434/api/chat
- headers: [Content-Type: application/json]
- body: {
"model" : "qwen3:8b",
"messages" : [ {
"role" : "system",
"content" : "你是位和蔼可亲的电商客服助手,只回答电商相关问题,当用户问题不涉及电商时,你需要提示用户你只解答电商问题。"
}, {
"role" : "user",
"content" : "你好"
} ],
"options" : {
"stop" : [ ]
},
"stream" : false,
"tools" : [ ]
}
[31m2026-01-17 10:04:39[0;39m [32m[XNIO-1 task-2][0;39m [34mINFO [0;39m [1;35md.l.h.client.log.LoggingHttpClient
[0;39m - HTTP response:
- status code: 200
- headers: [Content-Length: 1308], [Content-Type: application/json; charset=utf-8], [Date: Sat, 17 Jan 2026 02:04:39 GMT]
- body: {"model":"qwen3:8b","created_at":"2026-01-17T02:04:39.4895362Z","message":{"role":"assistant","content":"您好呀!😊 欢迎来到我们的电商客服中心!我是您的小助手,有什么问题需要帮助吗?比如订单查询、退换货政策、商品咨询等等都可以问我哦~","thinking":"好的,用户打招呼说“你好”,我需要以和蔼可亲的电商客服身份回应。首先,要保持友好和专业的态度,欢迎用户并询问是否有需要帮助的地方。同时,要确保只回答电商相关的问题,如果用户的问题不涉及电商,需要提示他们。现在用户只是打招呼,没有具体问题,所以应该先回应问候,然后引导用户提出具体问题。保持语气亲切,使用适当的emoji增加亲和力,但不要过多。需要简洁明了,避免冗长。检查是否有需要补充的信息,比如是否需要提及退货政策、订单查询等常见服务,但用户没有提到这些,所以暂时不需要。确保回复符合角色设定,不涉及非电商内容。现在组织语言,确保自然流畅。\n"},"done":true,"done_reason":"stop","total_duration":5900416400,"load_duration":1630246900,"prompt_eval_count":50,"prompt_eval_duration":37736300,"eval_count":202,"eval_duration":4144829300}
(4)使用@AiService注解
除了使用AiServices builder的方式,我们还可以使用@AiService注解定义Ai Service。
新建一个对话接口IMyAiService2:
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "ollamaChatModel")
public interface IMyAiService2 {
@SystemMessage("你是位和蔼可亲的电商客服助手,只回答电商相关问题,当用户问题不涉及电商时,你需要提示用户你只解答电商问题。")
String chat(@UserMessage String message);
}
在controller中加入chat3接口后的代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.lovecto.yuen.agents.service.IMyAiService1;
import cn.lovecto.yuen.agents.service.IMyAiService2;
import cn.dev33.satoken.annotation.SaIgnore;
import dev.langchain4j.model.chat.ChatModel;
@SaIgnore
@RestController
public class ChatController {
@Autowired
private ChatModel chatModel;
@Autowired
private IMyAiService1 aiService1;
@Autowired
private IMyAiService2 aiService2;
/**
* 基于chatModel直接对话
* @param message
* @return
*/
@GetMapping("/chat")
public String model(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return chatModel.chat(message);
}
/**
* 基于AiServices builder的Ai service对话
* @param message
* @return
*/
@GetMapping("/chat2")
public String aiServiceChat(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return aiService1.chat(message);
}
/**
* 基于@AiService的Ai service对话
* @param message
* @return
*/
@GetMapping("/chat3")
public String chat3(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return aiService2.chat(message);
}
}
运行程序后,浏览器输入:
http://192.168.2.131:8080/ai/chat3?message=怎么比价
效果如图:

可见和使用AiServices builder的效果是一样的,我们在开发过程中可灵活选用不同的构建方式。
4、会话记忆(Memory)
和大模型的对话是无状态的,要让大模型更好的理解最新的对话内容,给出对话历史的摘要信息是必要的。
记忆(Memory)和历史(History)有相似,但又不同。History是对话的完整历史内容,而Memory是对话历史的关键信息。
到目前,我们和AI对话都是没有记忆的。我们在浏览器输入:
http://192.168.2.131:8080/ai/chat3?message=我刚刚问的电商问题是什么?
如下图所示:

大模型并不具备记忆,它不知道我们之前问了什么。
(1)让AiService具备记忆
我们在上面提到的AiServiceConfig中创建一个ChatMemoryStore和ChatMemory:
@Bean
public ChatMemoryStore chatMemoryStore() {
return new InMemoryChatMemoryStore();
}
@Bean
public ChatMemory chatMemory(ChatMemoryStore chatMemoryStore) {
return MessageWindowChatMemory.builder()
.chatMemoryStore(chatMemoryStore)
.maxMessages(20)
.build();
}
使用@AiService注解创建一个对话接口IMyAiService3,接口中引入了chatMemory(值就是我们刚刚定义的Bean名称chatMemory):
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "ollamaChatModel", chatMemory = "chatMemory")
public interface IMyAiService3 {
@SystemMessage("你是位和蔼可亲的电商客服助手,只回答电商相关问题,当用户问题不涉及电商时,你需要提示用户你只解答电商问题。")
String chat(@UserMessage String message);
}
在ChatController中加入如下代码:
@Autowired
private IMyAiService3 aiService3;
/**
* 具有会话记忆的Ai Service
* @param message
* @return
*/
@GetMapping("/chat4")
public String chat4(@RequestParam(value = "message", defaultValue = "Hello") String message) {
return aiService3.chat(message);
}
浏览器输入:
http://192.168.2.131:8080/ai/chat4?message=电商怎么比价
等响应完毕后,再把message修改为“我刚刚问的电商问题是什么”:
http://192.168.2.131:8080/ai/chat4?message=我刚刚问的电商问题是什么
响应效果如下图:

说明我们的应用已经具备了记忆。
(2)让AiService区分不同用户的会话记忆
刚刚我们的实例中并没有区分会话,A用户浏览器输入内容与Ai会话,B用户也从浏览器输入内容通Ai会话,他们的会话记忆都交叉到了一起,如何区分呢?
在AiServiceConfig中增加代码创建一个ChatMemoryProvider:
@Bean
public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore chatMemoryStore) {
return new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
return MessageWindowChatMemory.builder()
.id(memoryId)
.chatMemoryStore(chatMemoryStore)
.maxMessages(20)
.build();
}
};
}
chatMemoryProvider方法中的get方法传入memoryId并根据id区分不同的ChatMemory。
我们创建一个新的对话接口IMyAiService4,如下:
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "ollamaChatModel", chatMemoryProvider = "chatMemoryProvider")
public interface IMyAiService4 {
@SystemMessage("你是位和蔼可亲的电商客服助手,只回答电商相关问题,当用户问题不涉及电商时,你需要提示用户你只解答电商问题。")
String chat(@MemoryId Long memoryId, @UserMessage String message);
}
IMyAiService4中我们没有使用IMyAiService3中用到的chatMemory,而是使用chatMemoryProvider,有了chatMemoryProvider就可以不用chatMemory了。同时我们还使用了一个新的注解@MemoryId,这个注解的作用就是用于标识和大模型对话的记忆的ID,传入有该注解的参数,chatMemoryProvider就可以根据memoryId获取对应的chatMemory了。
我们继续修改ChatController,加入如下代码:
@Autowired
private IMyAiService4 aiService4;
/**
* 能区分不同会话记忆的 Ai Service
* @param message
* @param memoryId
* @return
*/
@GetMapping("/chat5")
public String chat5(@RequestParam(value = "message", defaultValue = "Hello") String message, @RequestParam(value = "memoryId") Long memoryId) {
return aiService4.chat(memoryId, message);
}
启动程序后,浏览器输入:
http://192.168.2.131:8080/ai/chat5?memoryId=1&message=哪一个电商平台最好
此处memoryId为1,回答如下:

再打开一个新的浏览器窗口,输入如下:
http://192.168.2.131:8080/ai/chat5?memoryId=2&message=我刚刚问的是什么
此处的memoryId为2,回答如下:

由于memoryId为2的用户没有提问过,所以有上图的回答。说明memoryId为1的会话不会被memoryId为2的用户知道。
再在浏览器输入:
http://192.168.2.131:8080/ai/chat5?memoryId=1&message=我刚刚问的是什么
此时的memoryId为1,回答如下:

memoryId为1的会话实现了记忆。
我们通过这个实例说明了通过chatMemoryProvider就和注解@MemoryId能实现不同用户与大模型会话记忆的隔离。
(3)会话记忆持久化
我们之前的实例都是基于InMemoryChatMemoryStore,实现单个应用的内存会话记忆。当我们重新启动应用后,之前的对话记忆就消失了;又或者我们的应用分布式部署(集群多个实例部署)时,每个应用实例保持的记忆只跟当前应用接收到的http请求有关,这是不合理的。所以我们需要实现会话的持久化,重启程序不影响,每个应用实例都能共享会话记忆。此处我们以使用Redis作为会话持久化举例。
我们创建一个类RedisChatMemoryStore实现ChatMemoryStore接口:
import java.time.Duration;
import java.util.List;
import org.springframework.stereotype.Component;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
@Component
public class RedisChatMemoryStore implements ChatMemoryStore{
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = RedisUtils.getCacheObject(memoryId.toString());
return ChatMessageDeserializer.messagesFromJson(json);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
RedisUtils.setCacheObject(memoryId.toString(), ChatMessageSerializer.messagesToJson(messages), Duration.ofDays(7));
}
@Override
public void deleteMessages(Object memoryId) {
RedisUtils.deleteObject(memoryId.toString());
}
}
这里RedisUtils工具类只是封装了一些存、改、删的操作,可根据自己情况修改实现。在存取过程中,需要使用ChatMessageSerializer和ChatMessageDeserializer进行序列化和反序列化。
修改我们的chatMemoryProvider,如下:
@Autowired
private RedisChatMemoryStore redisChatMemoryStore;
/**
* 构建chatMemoryProvider
*
* @return
*/
@Bean
public ChatMemoryProvider chatMemoryProvider() {
ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
ChatMemory m = MessageWindowChatMemory.builder().id(memoryId).maxMessages(30)
.chatMemoryStore(redisChatMemoryStore).build();
return m;
}
};
return chatMemoryProvider;
}
就这样,我们就实现了会话的持久记忆以及分布式记忆。
在当前实例中,我们的会话在redis中只保留7天。但是在实际项目开发中,我们可能需要把用户同系统交互的所有消息更持久的存储。除了使用redis加速外,还会存储到mysql、es、其他数据库等。
5、RAG知识库构建
RAG(Retrieval-Augmented Generation,检索增强生成) 是当前 AI 应用中最核心、最实用的技术之一,尤其在构建企业级知识问答系统、智能客服、AI 助手等场景中发挥着关键作用。
它是一种将 大语言模型(LLM)与外部知识库结合 的技术架构,让 AI 在回答问题时不仅能依赖自身训练数据,还能“查阅资料”后给出更准确、可解释的答案。
RAG工作原理:
知识库准备工作:
[1] 文本分块 → 将知识文档切分为小片段
↓
[2] 向量化 → 使用 Embedding 模型转为向量
↓
[3] 存入向量数据库 → 如 Pinecone、Weaviate、Milvus
用户提问检索增强:
[1] 用户问题文本分块 → 将用户问题切分为小片段
↓
[2] 相似性检索 → 用户提问也被向量化,找出最相关片段(向量匹配度)
↓
[3] 注入 Prompt → 把检索到的知识作为上下文交给 LLM 生成答案
构建向量数据库向量化模型依赖:
<!-- 向量化模型依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
构建向量数据库文档解析依赖:
<!-- 文档解析器 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-markdown</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-yaml</artifactId>
<version>${langchain4j.version}</version>
</dependency>
(1)使用milvus构建知识库
构建RAG知识库,此处以milvus为例,增加如下依赖:
<!-- LangChain4j 向量数据库milvus 依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus</artifactId>
<version>${langchain4j.version}</version>
</dependency>
创建向量化模型(可以使用外部的向量化大模型,也可以使用内嵌模型,此处以内嵌模型AllMiniLmL6V2EmbeddingModel为例):
/**
* MiniLM 嵌入模型
*/
@Bean
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
创建milvus向量数据库:
/**
* Milvus向量数据库配置
*/
@Bean
public EmbeddingStore<TextSegment> milvusEmbeddingStore() {
return MilvusEmbeddingStore.builder()
.host(milvusHost)
.port(milvusPort)
.databaseName(milvusDatabase)
.collectionName(milvusCollectionName)
.dimension(milvusDimension)
.build();
}
创建文档注入器(此处会从/src/main/resources目录下的content目录读取文档内容存储到知识库,实际开发过程中需要单独做一个RAG管理功能实现知识库的添加和更新,这里只做举例用):
/**
* 文档注入器
*/
@Bean
public EmbeddingStoreIngestor embeddingStoreIngestor(EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 50))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 1、加载知识库
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
ingestor.ingest(documents);
return ingestor;
}
构建向量检索逻辑(设置向量化模型、设置向量数据库、设置检索条件):
/**
* 构建向量数据库检索对象
*
* @return
*/
@Bean
public ContentRetriever contentRetriever(EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.minScore(0.5)
.maxResults(3)
.build();
}
上面的代码,我放到AgentsConfig中,AgentsConfig用于配置milvus和chatMemoryProvider,完整代码如下:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.lovecto.yuen.agents.memory.RedisChatMemoryStore;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.ClassPathDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore;
@Configuration
public class AgentsConfig {
@Autowired
private RedisChatMemoryStore redisChatMemoryStore;
// Milvus配置
@Value("${milvus.host}")
private String milvusHost;
@Value("${milvus.port}")
private Integer milvusPort;
@Value("${milvus.database-name}")
private String milvusDatabase;
@Value("${milvus.collection-name:knowledge_base}")
private String milvusCollectionName;
@Value("${milvus.dimension:384}")
private Integer milvusDimension;
/**
* 构建chatMemoryProvider
*
* @return
*/
@Bean
public ChatMemoryProvider chatMemoryProvider() {
ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
ChatMemory m = MessageWindowChatMemory.builder().id(memoryId).maxMessages(30)
.chatMemoryStore(redisChatMemoryStore).build();
return m;
}
};
return chatMemoryProvider;
}
/**
* MiniLM 嵌入模型
*/
@Bean
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
/**
* Milvus向量数据库配置
*/
@Bean
public EmbeddingStore<TextSegment> milvusEmbeddingStore() {
return MilvusEmbeddingStore.builder()
.host(milvusHost)
.port(milvusPort)
.databaseName(milvusDatabase)
.collectionName(milvusCollectionName)
.dimension(milvusDimension)
.build();
}
/**
* 文档注入器
*/
@Bean
public EmbeddingStoreIngestor embeddingStoreIngestor(EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 50))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 1、加载知识库
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
ingestor.ingest(documents);
return ingestor;
}
/**
* 构建向量数据库检索对象
*
* @return
*/
@Bean
public ContentRetriever contentRetriever(EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.minScore(0.5)
.maxResults(3)
.build();
}
}
(2)创建带RAG知识库的Ai Service
我们创建一个Ai Service,具备流式对话和RAG知识库增强:
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
//chatModel = "ollamaChatModel",
streamingChatModel = "ollamaStreamingChatModel",
//streamingChatModel = "openAiStreamingChatModel",
//chatMemory = "chatMemory", //配置会话记忆对象
chatMemoryProvider = "chatMemoryProvider",
contentRetriever = "contentRetriever"
)
public interface IChatAiService {
/**
* 和AI聊天
* @param message
* @return
*/
@SystemMessage(fromResource = "system-message.txt")
Flux<String> chat(@MemoryId Long memoryId, @UserMessage String message);
}
在这个IChatAiService中,我们加入了contentRetriever,从而实现RAG(检索与增强);同时我们未使用chatModel,而是使用streamingChatModel实现流式响应(注意需要配置中把chat-model换成streaming-chat-model才生效,其他配置可不用修改)。
(3) 创建http接口实现聊天效果
我们新建一个AiChatController,实现流失对话聊天,并试试RAG功能。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.lovecto.yuen.agents.service.IChatAiService;
import reactor.core.publisher.Flux;
@RestController
public class AiChatController {
@Autowired
private IChatAiService chartAiService;
@GetMapping(value = "/getMemoryId")
public R<Long> getMemoryId() {
return R.ok(System.currentTimeMillis()) ;
}
@GetMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> model(Long memoryId, String message) {
Flux<String> r = chartAiService.chat(memoryId, message);
return r;
}
}
启动程序后,在浏览器输入:
http://192.168.2.131:8080/ai/chat?memoryId=1&message=痒点和痛点有什么不同
响应如下:

接下来,再看看我们提供的知识内容中的片段:

对比AI的回答和知识库的回答,是不是如此的相似。
我们通过这个实例实现了知识库的挂载,同时使用了流式响应。
6、工具调用
之前的实例,都是讲如何和大模型对话,接下来我们讲下如何让大模型调用工具干活。
我们创建一个RetailTool的组件,根据商品名称查询库存:
@Component
public class RetailTool {
@DubboReference
private IChainStockApiService stockApiService;
@Tool("根据门商品名称查询库存")
public List<ErpChainStock> queryChainByPhone(@P("商品名称") String productName) {
// LoginUser user = LoginHelper.getLoginUser();
// Long chainId = user.getDeptId();
// Map<String,String> map = new HashMap<>();
// map.put("productNameLike", productName);
// PagerInfo pager = new PagerInfo(10, 1);
// ResultPage<ErpChainStock> resultPage = stockApiService.query(chainId, map, pager);
// return resultPage.getList();
return TenantHelper.dynamic(TenantConstants.DEFAULT_TENANT_ID, ()->{
Long chainId = 1863398543189741570l;
Map<String,String> map = new HashMap<>();
map.put("productNameLike", productName);
PagerInfo pager = new PagerInfo(10, 1);
ResultPage<ErpChainStock> resultPage = stockApiService.query(chainId, map, pager);
return resultPage.getList();
});
}
}
接下来,你只需要在你的IChatAiService中加入tools即可:
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
//chatModel = "ollamaChatModel",
streamingChatModel = "ollamaStreamingChatModel",
//streamingChatModel = "openAiStreamingChatModel",
//chatMemory = "chatMemory", //配置会话记忆对象
tools = "retailTool" ,//工具调用
chatMemoryProvider = "chatMemoryProvider",
contentRetriever = "contentRetriever"
)
public interface IChatAiService {
//代码略
}
在浏览器输入:
http://192.168.2.131:8080/ai/chat?memoryId=2&message=我需要查库存
效果如图:

让我提供商品名称,接下来,我输入:
http://192.168.2.131:8080/ai/chat?memoryId=2&message=红海口味王
效果如图:

告诉我库存-22,库存可能不足,说明我们的工具调用成功。
需要注意的是,如果是本地部署的大模型,要让应用能正常调用工具,提示词尽可能的具体、目标明确,从而降低工具调用出错的概率。
7、多智能体或工作流
开发多智能体或智能工作流,需要如下依赖:
<!-- 多智能体支持 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
<version>${langchain4j.version}</version>
</dependency>
(1)创建一个写笑话的agent
package cn.lovecto.yuen.agents.agent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
public interface JokeWriter {
/**
* <pre>
* 1、@UserMessage:写的是系统Prompt模板
* (1)限制:不超过3句话
* (2)要求:只返回笑话内容本身
* 2、@V("topic"):把上下文中的topic变量注入到{{topic}}占位符中
* 3、@Agent:告诉Langchain4j,generateJoke是一个Agent方法
* </pre
* @param topic
* @return
*/
@UserMessage("你是一名富有创造力的笑话作者。请围绕给定的主题创作一个不超过 3 句话的笑话草稿。" +
"只返回笑话内容本身,不要包含任何解释或其他文字。主题是:{{topic}}。")
@Agent(description = "根据给定主题生成一个笑话")
String generateJoke(@V("topic") String topic);
}
(2)创建一个修改笑话的agent
package cn.lovecto.yuen.agents.agent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
public interface JockEditor {
/**
* <pre>
* 1、接收2个变量
* (1)joke:上一阶段创作的笑话
* (2)audience: 目标读者(例如"女生","男生","学生"等)
* 2、Prompt中的约束
* (1)输出长度:小于3句
* (2)风格要求:生动好笑、有吸引力、通俗易懂
* (3)不要解释,只给结果文本
* </pre>
* @param joke
* @param audience
* @return
*/
@UserMessage(" 你是一名专业的笑话编辑。请将下面的笑话改写得更适合目标读者:{{audience}}。" +
"笑话长度请控制在 3 句话以内,要生动好笑、有吸引力,并且通俗易懂。" +
"只返回改写后的笑话内容,不要包含任何解释或多余文字。原始笑话如下:{{joke}}")
@Agent(description = "根据目标读者对故事进行润色与改写")
String editJoke(@V("joke") String joke,
@V("audience") String audience);
}
(3)多agent配置类
package cn.lovecto.yuen.agents.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.lovecto.yuen.agents.agent.JockEditor;
import cn.lovecto.yuen.agents.agent.JokeWriter;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.UntypedAgent;
import dev.langchain4j.model.openai.OpenAiChatModel;
@Configuration
public class AgentsConfig {
@Autowired
private OpenAiChatModel chatModel;
/**
* 笑话创作 Agent,输出写到 agenitc scope 的 "joke" key
*
* @return
*/
@Bean
public JokeWriter jokeWriter() {
return AgenticServices.agentBuilder(JokeWriter.class)
.chatModel(chatModel)
.outputKey("joke")
.build();
}
/**
* 笑话修改 Agent,最终结果写到 "modifiedJoke"
*
* @return
*/
@Bean
public JockEditor jockEditor() {
return AgenticServices.agentBuilder(JockEditor.class)
.chatModel(chatModel)
.outputKey("modifiedJoke")
.build();
}
/**
* 多agent顺序执行
*
* @param jokeWriter 根据topic生成 joke 初稿
* @param jockEditor 根据audience 和 joke 初稿,修改后产出 modifiedJoke
* @return
*/
@Bean
public UntypedAgent jokePipeline(JokeWriter jokeWriter, JockEditor jockEditor) {
return AgenticServices.sequenceBuilder()
.subAgents(jokeWriter, jockEditor)
.outputKey("modifiedJoke")
.build();
}
}
(4)对话入口
package cn.lovecto.yuen.agents.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.annotation.SaIgnore;
import dev.langchain4j.agentic.UntypedAgent;
@SaIgnore
@RestController
public class ChartController {
@Autowired
private UntypedAgent agent;
@GetMapping("/joke")
public String joke(String topic, String audience) {
return (String)agent.invoke(Map.of("topic", topic, "audience", audience));
}
}
(5)调用实例
浏览器输入:
http://192.168.2.131:9999/joke?topic=%E4%B8%8A%E7%8F%AD&audience=%E7%A8%8B%E5%BA%8F%E5%91%98
如图:

(6)调用日志如下
2026-01-12T11:55:20.082+08:00 INFO 37844 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...97], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen3-max",
"messages" : [ {
"role" : "user",
"content" : "你是一名富有创造力的笑话作者。请围绕给定的主题创作一个不超过 3 句话的笑话草稿。只返回笑话内容本身,不要包含任何解释或其他文字。主题是:上班。"
} ],
"stream" : false
}
2026-01-12T11:55:23.073+08:00 INFO 37844 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding], [x-request-id: 0f161a6d-ad49-9638-bf1d-899e10083c14], [x-dashscope-call-gateway: true], [content-type: application/json], [content-length: 573], [req-cost-time: 2767], [req-arrive-time: 1768190117311], [resp-start-time: 1768190120078], [x-envoy-upstream-service-time: 2766], [date: Mon, 12 Jan 2026 03:55:19 GMT], [server: istio-envoy]
- body: {"choices":[{"finish_reason":"stop","index":0,"message":{"content":"闹钟响了,我按掉它继续睡——毕竟梦里我已经打卡上班了。 \n老板问我为什么迟到,我说:“在梦里加班太投入,现实里起不来。” \n他点点头:“那你今晚梦里记得把周报写了。”","role":"assistant"}}],"created":1768190117,"id":"chatcmpl-0f161a6d-ad49-9638-bf1d-899e10083c14","model":"qwen3-max","object":"chat.completion","usage":{"completion_tokens":54,"prompt_tokens":52,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":106}}
2026-01-12T11:55:23.075+08:00 INFO 37844 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...97], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen3-max",
"messages" : [ {
"role" : "user",
"content" : " 你是一名专业的笑话编辑。请将下面的笑话改写得更适合目标读者:程序员。笑话长度请控制在 3 句话以内,要生动好笑、有吸引力,并且通俗易懂。只返回改写后的笑话内容,不要包含任何解释或多余文字。原始笑话如下:闹钟响了,我按掉它继续睡——毕竟梦里我已经打卡上班了。 \n老板问我为什么迟到,我说:“在梦里加班太投入,现实里起不来。” \n他点点头:“那你今晚梦里记得把周报写了。”"
} ],
"stream" : false
}
2026-01-12T11:55:26.004+08:00 INFO 37844 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding], [x-request-id: 66863539-8a70-991f-8eb2-938f77c92bb2], [x-dashscope-call-gateway: true], [content-type: application/json], [content-length: 582], [req-cost-time: 2884], [req-arrive-time: 1768190120124], [resp-start-time: 1768190123009], [x-envoy-upstream-service-time: 2882], [date: Mon, 12 Jan 2026 03:55:22 GMT], [server: istio-envoy]
- body: {"choices":[{"finish_reason":"stop","index":0,"message":{"content":"闹钟响了,我按掉继续睡——毕竟梦里我已经 push 代码到主干了。 \n老板问我为啥迟到,我说:“在梦里 debug 太投入,现实起不来。” \n他点点头:“那你今晚梦里记得把周报 merge 进去。”","role":"assistant"}}],"created":1768190120,"id":"chatcmpl-66863539-8a70-991f-8eb2-938f77c92bb2","model":"qwen3-max","object":"chat.completion","usage":{"completion_tokens":60,"prompt_tokens":128,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":188}}
需要了解更多关于智能体或工作流的开发,可参考文档:
https://docs.langchain4j.dev/tutorials/agents
8、MCP 客户端开发
我这里先说使用langchain4j开发mcp客户端,下文会说到基于Spring Ai的mcp服务端的开发。
使用langchain4j开发mcp客户端,需要如下依赖:
<!-- 引入mcp依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
<exclusions>
<exclusion>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-http-client-jdk</artifactId>
</exclusion>
</exclusions>
</dependency>
(1)写智能体接口
package cn.lovecto.yuen.agents.agent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
public interface OrderAgent {
@UserMessage("你是一名订单售后助手。可根据订单号查询订单状态。订单号是:{{orderNo}}。")
@Agent(description = "根据物流状态查询无论信息")
String getExpressByState(@V("orderNo") String orderNo);
}
(2)智能体配置类
package cn.lovecto.yuen.agents.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.lovecto.yuen.agents.agent.OrderAgent;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.UntypedAgent;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.model.ollama.OllamaChatModel;
@Configuration
public class OrderAgentConfig {
@Autowired
private OllamaChatModel chatModel;
@Bean
public OrderAgent orderAgent(McpToolProvider toolProvider) {
return AgenticServices.agentBuilder(OrderAgent.class)
.chatModel(chatModel)
.toolProvider(toolProvider)
.outputKey("orderState")
.build();
}
@Bean
public UntypedAgent orderPipeline(OrderAgent orderAgent) {
return AgenticServices.sequenceBuilder()
.subAgents(orderAgent)
.outputKey("orderState")
.build();
}
}
(3)Langchain4j MCP配置类
package cn.lovecto.yuen.agents.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
@Configuration
public class Langchain4jMcpConfig {
@Bean
public McpTransport mcpTransport() {
McpTransport transport = StreamableHttpMcpTransport.builder()
.url("http://192.168.2.131:9988/mcp")
.logRequests(true) // 打印日志
.logResponses(true)
.build();
return transport;
}
@Bean
public McpClient mcpClient(McpTransport mcpTransport) {
McpClient mcpClient = DefaultMcpClient.builder()
.key("MyMCPClient")
.transport(mcpTransport)
.build();
return mcpClient;
}
@Bean
public McpToolProvider toolProvider(McpClient mcpClient) {
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.build();
return toolProvider;
}
}
(4)对话入口
package cn.lovecto.yuen.agents.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.annotation.SaIgnore;
import dev.langchain4j.agentic.UntypedAgent;
@SaIgnore
@RestController
public class OrderController {
@Qualifier("orderPipeline")
@Autowired
private UntypedAgent orderPipeline;
@GetMapping("/getOrderState")
public String getOrderState(String msg, String orderNo) {
return (String)orderPipeline.invoke(Map.of("msg", msg, "orderNo", orderNo));
}
}
(5) 演示
在浏览器中输入
http://192.168.2.131:9999/getOrderState?msg=%E5%B8%AE%E6%88%91%E6%9F%A5%E4%B8%8B%E8%AE%A2%E5%8D%95%E7%8A%B6%E6%80%81&orderNo=88800098
结果如下:

调用日志:
2026-01-12T18:19:23.831+08:00 INFO 18744 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: http://192.168.2.131:11434/api/chat
- headers: [Content-Type: application/json]
- body: {
"model" : "qwen3:0.6b",
"messages" : [ {
"role" : "user",
"content" : "你是一名订单售后助手。可根据订单号查询订单状态。订单号是:88800098。"
} ],
"options" : {
"stop" : [ ]
},
"stream" : false,
"tools" : [ {
"type" : "function",
"function" : {
"name" : "getOrderState",
"description" : "根据订单号查询订单状态",
"parameters" : {
"type" : "object",
"properties" : {
"ordrNo" : {
"type" : "string"
}
},
"required" : [ "ordrNo" ]
}
}
} ]
}
2026-01-12T18:19:24.636+08:00 INFO 18744 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [Content-Type: application/json; charset=utf-8], [Date: Mon, 12 Jan 2026 10:19:24 GMT], [Content-Length: 784]
- body: {"model":"qwen3:0.6b","created_at":"2026-01-12T10:19:24.635758Z","message":{"role":"assistant","content":"","thinking":"好的,用户需要查询订单状态,订单号是88800098。我需要先确认是否有相关的工具可用,根据提供的工具,有一个getOrderState函数,需要根据订单号来返回状态。所以应该直接调用这个函数,参数是ordrNo,然后将结果返回给用户。不需要额外的信息或解释,直接执行即可。\n","tool_calls":[{"id":"call_vyhjd3zt","function":{"index":0,"name":"getOrderState","arguments":{"ordrNo":"88800098"}}}]},"done":true,"done_reason":"stop","total_duration":801927600,"load_duration":91255700,"prompt_eval_count":167,"prompt_eval_duration":71498200,"eval_count":112,"eval_duration":607885200}
2026-01-12T18:19:24.636+08:00 INFO 18744 --- [yuen-ahents] [ XNIO-1 task-2] MCP : Request: {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"getOrderState","arguments":{"ordrNo":"88800098"}}}
2026-01-12T18:19:24.638+08:00 INFO 18744 --- [yuen-ahents] [ient-2-Worker-2] MCP : Response: {"jsonrpc":"2.0","id":6,"result":{"content":[{"type":"text","text":"\"待发货\""}],"isError":false}}
2026-01-12T18:19:24.640+08:00 INFO 18744 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: http://192.168.2.131:11434/api/chat
- headers: [Content-Type: application/json]
- body: {
"model" : "qwen3:0.6b",
"messages" : [ {
"role" : "user",
"content" : "你是一名订单售后助手。可根据订单号查询订单状态。订单号是:88800098。"
}, {
"role" : "assistant",
"tool_calls" : [ {
"function" : {
"name" : "getOrderState",
"arguments" : {
"ordrNo" : "88800098"
}
}
} ]
}, {
"role" : "tool",
"content" : "\"待发货\""
} ],
"options" : {
"stop" : [ ]
},
"stream" : false,
"tools" : [ {
"type" : "function",
"function" : {
"name" : "getOrderState",
"description" : "根据订单号查询订单状态",
"parameters" : {
"type" : "object",
"properties" : {
"ordrNo" : {
"type" : "string"
}
},
"required" : [ "ordrNo" ]
}
}
} ]
}
2026-01-12T18:19:25.184+08:00 INFO 18744 --- [yuen-ahents] [ XNIO-1 task-2] d.l.http.client.log.LoggingHttpClient : HTTP response:
- status code: 200
- headers: [Content-Type: application/json; charset=utf-8], [Date: Mon, 12 Jan 2026 10:19:25 GMT], [Content-Length: 915]
- body: {"model":"qwen3:0.6b","created_at":"2026-01-12T10:19:25.1838748Z","message":{"role":"assistant","content":"订单状态:待发货。如需查看订单详情或进行操作,请随时联系客服。","thinking":"好的,用户给了一个订单号88800098,并让我查询状态。我需要先确认用户的需求,他们可能想知道订单现在处于哪个状态,比如是否已发货或等待处理。根据之前的工具调用,已经成功查询到“待发货”,所以现在应该向用户反馈这个结果。用户可能想知道具体的步骤,比如如何处理或等待,所以需要简洁明了地回复,并提供进一步帮助的选项。保持友好和专业的语气,确保用户满意。\n"},"done":true,"done_reason":"stop","total_duration":542052100,"load_duration":114188500,"prompt_eval_count":212,"prompt_eval_duration":9413900,"eval_count":127,"eval_duration":396786200}
三、Spring AI开发
Spring AI,Spring 家族官方项目,强调企业集成,与 Spring Boot 天然融合,配置驱动。
这里我们用Spring Ai开发一个MCP Server作为实例,同时使用上面的langchain4j实现的mcp client调用该Server,演示效果在前文已经描述。
1、在mcp server项目模块中引入依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.lovecto</groupId>
<artifactId>yuen-agents</artifactId>
<version>${yuen.version}</version>
</parent>
<artifactId>yuen-agents-mcp</artifactId>
<dependencies>
<!-- 引入mcp webflux 依赖, 不需要引入 spring boot web依赖-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
<version>2.0.0-M1</version>
</dependency>
<!-- 引入测试模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
</dependencies>
</project>
2、配置
我这里protocol选择STATELESS(无状态),type为SYNC。
server:
port: 9988
spring:
application:
name: yuen-mcp-service
ai:
mcp:
server:
enabled: true
version: 1.0.0
type: SYNC
protocol: STATELESS
streamable-http:
mcpEndpoint: /mcp
3、创建订单查询工具
此处只是距离,随机返回订单状态。
package cn.lovecto.yuen.mcp.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import cn.hutool.core.util.RandomUtil;
import java.util.List;
@Service
public class OrderService {
@Tool(description = "根据订单号查询订单状态")
public String getOrderState(String ordrNo) {
List<String> stateList = List.of("待付款", "待发货", "已发货", "待签收", "已收货",
"待评价", "已完成", "待退款", "退款中", "已退款", "已取消");
return stateList.get(RandomUtil.randomInt(stateList.size()));
}
}
4、mcp工具配置
package cn.lovecto.yuen.mcp.config;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.lovecto.yuen.mcp.service.OrderService;
@Configuration
public class ToolsConfig {
@Bean
public ToolCallbackProvider expressTools(OrderService orderService) {
return MethodToolCallbackProvider.builder()
.toolObjects(orderService)
.build();
}
}
这里使用ToolCallbackProvider,会自动扫描OrderService类的含@Tool注解的方法并把它们注册为MCP Tool,供客户端的大模型调用。
6、启动类:
package cn.lovecto.yuen.mcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YuenMcpApplication {
public static void main(String[] args) {
SpringApplication.run(YuenMcpApplication.class, args);
}
}
启动运行mcp 服务端。
LoveCTO

