前言 这次的定语比较多,又是 简易 又是 client 的
如果你去翻阅 langchain4j 有关 MCP 的文档(点击这里 ),你会发现有关它的内容比起 RAG 少的可怜
在我看来,应该是以下几个原因有关
langchain4j 并没有实现完整的 MCP 协议。在目前版本(1.0.1)的 langchain4j 中是不存在 MCP Server 这个组件的
langchain4j 对于 MCP Client 的理解和 Function Calling 异曲同工,有一些逻辑在其他模块已经实现了
我其实是有些纳闷的,因为我最初就是想用 langchain4j 把现有的服务构建成一个 MCP Server ,至于 MCP Client 我想许多客户端都可以充当这个角色
以上推论主观性很强,如果有错误,可以联系我的邮箱
langchain4j 中的 MCP Client 在 langchain4j 中,McpClient 本质还是一个 tool
从官方文档中给的例子中可以看出来一些端倪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 McpTransport transport = new HttpMcpTransport .Builder() .sseUrl("http://localhost:3001/sse" ) .logRequests(true ) .logResponses(true ) .build(); McpClient mcpClient = new DefaultMcpClient .Builder() .key("MyMCPClient" ) .transport(transport) .build(); McpToolProvider toolProvider = McpToolProvider.builder() .mcpClients(mcpClient) .build(); Bot bot = AiServices.builder(Bot.class) .chatModel(model) .toolProvider(toolProvider) .build();
先是构建了一个基于 SSE 的传输对象 transport
,然后封装成一个 McpClient
客户端,再包装成一个工具供应商 McpToolProvider
,最后交给 AiServices
构建一个服务组件
AiServices
除了有 toolProvider
属性,还有 tools
属性。而这个 tools
属性就是 Function Calling 用来配置 tool
的
看一下源码(dev.langchain4j.service.tool.ToolService#createContext
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ToolServiceContext createContext (Object memoryId, UserMessage userMessage) { if (this .toolProvider == null ) { return this .toolSpecifications.isEmpty() ? new ToolServiceContext (null , null ) : new ToolServiceContext (this .toolSpecifications, this .toolExecutors); } List<ToolSpecification> toolsSpecs = new ArrayList <>(this .toolSpecifications); Map<String, ToolExecutor> toolExecs = new HashMap <>(this .toolExecutors); ToolProviderRequest toolProviderRequest = new ToolProviderRequest (memoryId, userMessage); ToolProviderResult toolProviderResult = toolProvider.provideTools(toolProviderRequest); if (toolProviderResult != null ) { for (Map.Entry<ToolSpecification, ToolExecutor> entry : toolProviderResult.tools().entrySet()) { if (toolExecs.putIfAbsent(entry.getKey().name(), entry.getValue()) == null ) { toolsSpecs.add(entry.getKey()); } else { throw new IllegalConfigurationException ( "Duplicated definition for tool: " + entry.getKey().name()); } } } return new ToolServiceContext (toolsSpecs, toolExecs); }
逻辑大概是
如果 toolProvider
为空,则只包装了 tools
来源的工具
toolSpecifications 和 toolExecutors 是 tools 属性处理得到的
否则,先将 toolSpecifications
和 toolExecutors
添加到容器中做准备
重点在于 provideTools
方法,它将 McpClient
的 tool
列举出来(通过 MCP
协议的实现 dev.langchain4j.mcp.client.McpClient#listTools
)并返回了 ToolProviderResult
dev.langchain4j.mcp.McpToolProvider#provideTools(dev.langchain4j.service.tool.ToolProviderRequest, java.util.function.BiPredicate<dev.langchain4j.mcp.client.McpClient,dev.langchain4j.agent.tool.ToolSpecification>)
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected ToolProviderResult provideTools (ToolProviderRequest request, BiPredicate<McpClient, ToolSpecification> mcpToolsFilter) { ToolProviderResult.Builder builder = ToolProviderResult.builder(); for (McpClient mcpClient : mcpClients) { try { mcpClient.listTools().stream().filter(tool -> mcpToolsFilter.test(mcpClient, tool)) .forEach(toolSpecification -> { builder.add(toolSpecification, (executionRequest, memoryId) -> mcpClient.executeTool(executionRequest)); }); } catch (IllegalConfigurationException e) { throw e; } catch (Exception e) { if (failIfOneServerFails) { throw new RuntimeException ("Failed to retrieve tools from MCP server" , e); } else { log.warn("Failed to retrieve tools from MCP server" , e); } } } return builder.build(); }
这里就可以看出来,其实本质上和普通的 Function Calling
没有什么区别
将 ToolProviderResult
中的 tool
添加到第二步准备好的容器内
剩下就是和 Function Calling
一样,把 tool
的描述交给大模型,让大模型判断应该使用哪些工具来协助完成用户的提问
项目演示 目标 利用 langchain4j 的 MCP Client
能力,使用 高德地图 和 腾讯地图 的 MCP Server
来实现一个旅行规划
准备
大模型换用其它的国内模型也没问题,我习惯用 deepseek 了
其实百度地图也提供了它们的 MCP Server,不过它们的 key 申请需要认证开发者
编码 pom 依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <?xml version="1.0" encoding="UTF-8" ?> <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 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > top.wuhunyu.mcp</groupId > <artifactId > langchain4j-mcp-client-example</artifactId > <version > 0.0.1</version > <properties > <maven.compiler.source > 21</maven.compiler.source > <maven.compiler.target > 21</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <spring-boot.version > 3.4.0</spring-boot.version > <langchain4j.version > 1.0.1</langchain4j.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <scope > import</scope > <type > pom</type > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-bom</artifactId > <version > ${langchain4j.version}</version > <scope > import</scope > <type > pom</type > </dependency > </dependencies > </dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-reactor</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-open-ai-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-mcp</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <scope > provided</scope > </dependency > </dependencies > </project >
application.yml 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 spring: application: name: langchain4j-mcp-client-example langchain4j: open-ai: chat-model: base-url: https://api.deepseek.com/v1 api-key: [替换成你的 deepseek api key ] model-name: deepseek-chat log-requests: true log-responses: true streaming-chat-model: base-url: ${langchain4j.open-ai.chat-model.base-url} api-key: ${langchain4j.open-ai.chat-model.api-key} model-name: deepseek-chat log-requests: ${langchain4j.open-ai.chat-model.log-requests} log-responses: ${langchain4j.open-ai.chat-model.log-responses} mcp: amap: sse-url: https://mcp.amap.com/sse?key=[替换成你的 高德地图 api key] client-name: amap-client client-version: 0.0 .1 qq: sse-url: https://mcp.map.qq.com/sse?key=[替换成你的 腾讯地图 api key] client-name: qq-client client-version: 0.0 .1
组件配置 1 2 3 4 5 6 public interface MapClientService { @SystemMessage("你是一个旅行规划专家,你可以根据用户的需求,为用户规划出旅行路线。") Result<String> plan (@MemoryId Long userId, @UserMessage String userMessage) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Bean("mapService") public MapClientService mapService ( ChatModel chatModel, StreamingChatModel streamingChatModel, ChatMemoryProvider chatMemoryProvider, RetrievalAugmentor retrievalAugmentor, @Qualifier("amapMcpClient") McpClient amapMcpClient, @Qualifier("qqMcpClient") McpClient qqMcpClient, DateTool dateTool ) { final var mcpToolProvider = new McpToolProvider .Builder() .failIfOneServerFails(false ) .mcpClients(List.of( amapMcpClient, qqMcpClient )) .build(); return AiServices.builder(MapClientService.class) .chatModel(chatModel) .streamingChatModel(streamingChatModel) .chatMemoryProvider(chatMemoryProvider) .retrievalAugmentor(retrievalAugmentor) .toolProvider(mcpToolProvider) .tools(dateTool) .build(); }
只贴出了最关键的部分
注意除了 toolProvider
配置 MCP Client
以外,还添加了一个 tools
。这个日期工具只提供了一个基本的当前日期时间功能
1 2 3 4 5 6 7 8 9 10 11 @Component("dateTool") public class DateTool { private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); @Tool("查询当前的日期时间,以 yyyy-MM-dd HH:mm:ss 格式返回") public String now () { return LocalDateTime.now().format(dateTimeFormatter); } }
执行结果 让我们调用 MapClientService#plan
看看效果。我现在想要知道这个周末在深圳的旅游规划,提示词如下
1 请帮我规划一下这周周末深圳的两天旅行规划,要求精确到小时的安排
这是最后的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 以下是深圳周末两天(2025年6月14日-15日)的详细旅行计划,具体到每小时的活动安排: --- ### **第一天:6月14日(星期六)** #### **上午** - **08:00-09:00** :早餐 推荐地点:深圳本地早茶店(如点都德、陶陶居) 活动:品尝广式早茶,如虾饺、叉烧包等。 - **09:00-11:00** :深圳湾公园 活动:散步、骑行,欣赏深圳湾海景和红树林生态。 - **11:00-12:00** :前往华侨城创意文化园 交通:打车或地铁(约20分钟) 活动:参观艺术展览、创意小店。 #### **中午** - **12:00-13:30** :午餐 推荐地点:华侨城创意园内的特色餐厅(如“旧天堂书店”或“Gaga鲜语”) 活动:享用午餐,稍作休息。 #### **下午** - **13:30-15:30** :世界之窗 交通:打车或地铁(约15分钟) 活动:参观世界著名景点的微缩模型,拍照打卡。 - **15:30-17:00** :欢乐海岸 活动:购物、散步,欣赏水秀表演(如时间合适)。 - **17:00-18:00** :前往深圳湾万象城 交通:步行或打车(约10分钟) 活动:购物或休闲。 #### **晚上** - **18:00-19:30** :晚餐 推荐地点:深圳湾万象城的高端餐厅(如“炳胜品味”) 活动:享用晚餐。 - **19:30-21:00** :深圳湾灯光秀(如开放) 活动:欣赏深圳湾夜景和灯光表演。 - **21:00-22:00** :返回酒店 交通:打车或地铁 活动:休息。 --- ### **第二天:6月15日(星期日)** #### **上午** - **08:00-09:00** :早餐 推荐地点:酒店早餐或附近咖啡馆。 - **09:00-11:00** :大梅沙海滨公园 交通:打车或自驾(约40分钟) 活动:沙滩漫步、游泳(如天气允许)。 - **11:00-12:00** :东部华侨城 活动:参观茶溪谷或大峡谷,体验自然风光。 #### **中午** - **12:00-13:30** :午餐 推荐地点:东部华侨城内的餐厅 活动:享用午餐。 #### **下午** - **13:30-15:30** :中英街 交通:打车(约30分钟) 活动:购物、了解历史。 - **15:30-17:00** :盐田海滨栈道 活动:散步,欣赏海景。 - **17:00-18:00** :返回市区 交通:打车或地铁 活动:休息。 #### **晚上** - **18:00-19:30** :晚餐 推荐地点:市区内的海鲜餐厅(如“79号渔船”) 活动:享用晚餐。 - **19:30-21:00** :深圳平安金融中心云际观景台 活动:俯瞰深圳夜景。 - **21:00-22:00** :返回酒店或前往机场/车站 交通:打车或地铁 活动:结束行程。 --- ### **注意事项** 1. **交通** :深圳地铁覆盖广泛,建议使用地铁或打车。2. **天气** :提前查看天气,携带防晒或雨具。3. **门票** :部分景点(如世界之窗、东部华侨城)需提前购票。如果需要调整或补充,请告诉我!
其他问题 发现了一处 bug:okhttp 的响应流未关闭,控制台打印了警告日志
1 2 2025-06-10 17:37:01.957 [OkHttp ConnectionPool] WARN okhttp3.OkHttpClient - A connection to https://mcp.map.qq.com/ was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE); 2025-06-10 17:37:01.957 [OkHttp ConnectionPool] WARN okhttp3.OkHttpClient - A connection to https://mcp.map.qq.com/ was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
问题源码(dev.langchain4j.mcp.client.transport.http.HttpMcpTransport#execute
)如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private CompletableFuture<JsonNode> execute (Request request, Long id) { CompletableFuture<JsonNode> future = new CompletableFuture <>(); if (id != null ) { messageHandler.startOperation(id, future); } client.newCall(request).enqueue(new Callback () { @Override public void onFailure (Call call, IOException e) { future.completeExceptionally(e); } @Override public void onResponse (Call call, Response response) throws IOException { int statusCode = response.code(); if (!isExpectedStatusCode(statusCode)) { future.completeExceptionally(new RuntimeException ("Unexpected status code: " + statusCode)); } if (id == null ) { future.complete(null ); } } }); return future; }
Response
对象未在使用后调用其 close
方法关闭连接
修复成如下应该就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private CompletableFuture<JsonNode> execute (Request request, Long id) { CompletableFuture<JsonNode> future = new CompletableFuture <>(); if (id != null ) { messageHandler.startOperation(id, future); } client.newCall(request).enqueue(new Callback () { @Override public void onFailure (Call call, IOException e) { future.completeExceptionally(e); } @Override public void onResponse (Call call, Response response) throws IOException { try (response) { int statusCode = response.code(); if (!isExpectedStatusCode(statusCode)) { future.completeExceptionally(new RuntimeException ("Unexpected status code: " + statusCode)); } if (id == null ) { future.complete(null ); } } } }); return future; }
提了一个 issue ,本想要提一个 pr,但 langchain4j 的源码在我本地没跑起来,这个机会让给其他有缘人吧
项目代码 GitHub
参考 langchain4j 官网