热爱技术,追求卓越
不断求索,精益求精

用java基于langhain4j开发AI智能体,看这篇就够了

传统互联网开发,万物皆可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=你好

调用结果如图所示:

image

查看程序控制台日志:

2026-01-17 09:23:27 [XNIO-1 task-2] INFO  d.l.h.client.log.LoggingHttpClient
 - 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" : [ ]
}

2026-01-17 09:23:31 [XNIO-1 task-2] INFO  d.l.h.client.log.LoggingHttpClient
 - 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=你好

效果如下:

image

对话日志如下:

2026-01-17 10:04:33 [XNIO-1 task-2] INFO  d.l.h.client.log.LoggingHttpClient
 - 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" : [ ]
}

2026-01-17 10:04:39 [XNIO-1 task-2] INFO  d.l.h.client.log.LoggingHttpClient
 - 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=怎么比价

效果如图:

image

可见和使用AiServices builder的效果是一样的,我们在开发过程中可灵活选用不同的构建方式。

4、会话记忆(Memory)

和大模型的对话是无状态的,要让大模型更好的理解最新的对话内容,给出对话历史的摘要信息是必要的。

记忆(Memory)和历史(History)有相似,但又不同。History是对话的完整历史内容,而Memory是对话历史的关键信息。

到目前,我们和AI对话都是没有记忆的。我们在浏览器输入:

http://192.168.2.131:8080/ai/chat3?message=我刚刚问的电商问题是什么?

如下图所示:

image

大模型并不具备记忆,它不知道我们之前问了什么。

(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=我刚刚问的电商问题是什么

响应效果如下图:

image

说明我们的应用已经具备了记忆。

(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,回答如下:

image

再打开一个新的浏览器窗口,输入如下:

http://192.168.2.131:8080/ai/chat5?memoryId=2&message=我刚刚问的是什么

此处的memoryId为2,回答如下:

image

由于memoryId为2的用户没有提问过,所以有上图的回答。说明memoryId为1的会话不会被memoryId为2的用户知道。

再在浏览器输入:

http://192.168.2.131:8080/ai/chat5?memoryId=1&message=我刚刚问的是什么

此时的memoryId为1,回答如下:

image

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=痒点和痛点有什么不同

响应如下:

image

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

image

对比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=我需要查库存

效果如图:

image

让我提供商品名称,接下来,我输入:

http://192.168.2.131:8080/ai/chat?memoryId=2&message=红海口味王

效果如图:

image

告诉我库存-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

如图:

image

(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

结果如下:

image

调用日志:

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 服务端。

赞(0)
未经允许不得转载:LoveCTO » 用java基于langhain4j开发AI智能体,看这篇就够了

热爱技术 追求卓越 精益求精