Langchain4j - MCP 和 Tool Calling 的对比及其具体案例

MCP vs Tool Calling

各自功能定位

在大模型应用开发中,MCP(Model Context Protocol,模型上下文协议)和 Tool Calling(也称 Function Calling 工具调用) 是目前连接 LLM 与 “外部世界基础设施” 的两大核心技术。这两者并不是简单的替代关系,而是代表了两种不同的架构方式。

  • Tool Calling 是大模型厂商(如 OpenAI、Anthropic、阿里百炼)提供的一种闭环能力。你在代码中给大模型提供一份 Java 方法的 JSON Schema(即工具说明书),大模型根据用户的输入,决定是否要调用这个方法,并为你吐出结构化的参数。具体的代码执行、网络请求,全部需要你的微服务应用自己去跑。它属于大模型 API 交互规范的一部分(如 messages 列表中穿插的 role: tool)。

  • MCP 是由 Anthropic 在 2024 年底发起、现已成为行业标准的开源网络协议。就像是 USB-C 提供了一种标准化的方式将你的设备连接到各种外围设备和配件一样,MCP 提供了一种标准化的方式将AI模型连接到不同的数据源和工具。MCP 把架构解耦成了三层:大模型客户端 (App)、MCP 路由器/网关、独立运行的 MCP 服务。任何机构都可以写一个标准的 MCP Server(比如:GitHub MCP、PostgreSQL MCP)。大模型只要支持 MCP 协议,就可以即插即用、直接调取这些外部服务。MCP 是标准化的、跨语言的分布式总线架构。








实际如何选择

我们知道 MCP 不仅能实现 Tools(动作),而且还支持 Resources(让模型直接读文件/数据库)、Prompts(共享提示词模板)。并且 MCP 相比 Function Calling,维护成本也小很多,只要 App 和 Server 都符合 MCP 协议,换模型完全无感。这是否意味着我们在实际的开发中都选择 MCP 就行了?目前不建议这样做。MCP 统一天下的想法很美好,但在企业级实际落地中,它有明显的代价:

  • 引入了额外的网络与体系复杂度:Function Calling 是在你的 Java 微服务进程内部反射执行的,性能极高。而 MCP 必须通过标准协议去走一次进程间通信(IPC)或网络通信(HTTP/WebSocket),这在高性能高并发场景下会带来额外的延迟和运维监控成本。
  • 生态和工具链成熟度:在 Java 生态(Spring / LangChain4j)中,Function Calling 的高阶 API(如 @Tool)已经成熟到极致,开箱即用。而 MCP 现阶段在 Python 和 Node.js 生态中最为活跃,Java 对应的官方生态和 SDK 还在快速演进中。

当下我认为实际选型指南可以总结为 “内部业务用 Function,外部生态/公共设施用 MCP”。目前坚定选择 Function Calling 的场景:

  • 强耦合的内部核心业务:如去本系统的订单表查数据、扣除用户钱包余额。这些逻辑紧贴你的微服务业务,不需要也不应该暴露给外部,直接用 @Tool 性能最高、开发最快。
  • 高并发、低延迟场景:不需要跨进程通信,完全受控于你的微服务线程池(如 boundedElastic)。

建议引入 MCP 的场景:

  • 对接成熟的公共基础设施:比如你的大模型应用需要直接读写 GitHub 仓库、查询外部 Slack 频道、查询标准的 PostgreSQL/ElasticSearch 数据库。那就别再自己手写 Function 了,直接去 GitHub 开源社区拉一个现成的 MCP Server 挂上,一分钟搞定。
  • 多语言团队协作:AI 团队用 Python 写了一套非常牛的 Rag(检索增强)和工具集,而你的主站业务是 Java。可以让 Python 团队把工具打包成标准 MCP Server 暴露出来,你的 Java 端直接利用 MCP 客户端无缝调用,实现跨语言解耦。

总结来说就是:脏活、累活、通用活交给 MCP(买现成的外设);核心业务、高频调用、私有逻辑留给 Function Calling(自制集成芯片)。这里提供一个类似 maven repository 或 docker hub 的 MCP repository —— MCP.so,只要各大厂商开放了 MCP 协议,允许大模型的各种客户端来调用,基本都能在这里找到。


MCP 的架构和两种通信模式

核心架构:MCP 是一种 CS 架构,包含三个主体和两种资源。

  • MCP 主机(MCP Hosts)
    • 指的是发起 AI 请求并集成大模型能力的最终端侧的应用程序。
    • 典型代表有 Claude Desktop、Cursor、VS Code、或者你用 LangChain4j 自研的平台系统。
    • 它负责控制整个对话的生命周期,持有大模型的 API Key,知道什么时候该向用户展示什么界面。
  • MCP 客户端(MCP Clients)
    • 指的是内置在 MCP Host 内部的标准协议适配器。
    • 它是 Host 的 “对外触角”。Host 要和外界通信时,Client 会严格按照 MCP 协议(通常是基于标准的 JSON-RPC 2.0 规范)将大模型的意图翻译成统一的底层指令,派发给各个 Server。一个 Client 可以同时挂载、调度多个不同的 MCP Server。
  • MCP 服务器(MCP Servers)
    • 通常指独立运行的、轻量级的外设微服务进程。
    • 这是真正干脏活累活的地方。每个 Server 都是一个完全独立的进程,可以用 Python、Node.js、Go 或 Java 编写。它向 Client 暴露自己的能力说明书,并在接收到 Client 的 RPC 调用指令时,直接去操作其身后的底层资源。

MCP Server 本身不存核心业务,它是一个高效率的 “外交官”,专门连接两类资源:

  • 本地资源(Local Resources):如 Server A 可以直接读写你电脑本地的特定文件夹(File System)、查询本地部署的 PostgreSQL 数据库,或者读取某个本地控制台的日志。
  • 远程资源(Remote Resources):如 Server C 部署在你的电脑里,但它拥有连接公网的能力,它通过标准的 Web APIs 去叩开 GitHub、Notion、Slack 等远程 SaaS 软件的大门。

MCP 通常基于两种传输模式:

  • stdio(标准输入输出):如果 Server 和 Host 都在同一台电脑上(比如 Cursor 调度本地的一个 Python 脚本),它们直接通过系统的标准输入输出流进行极速进程间通信。
  • SSE(Server-Sent Events / HTTP / WebSocket):如果 Server 部署在远端服务器上,它们则通过网络长连接通信。

为了说明整个数据流向是怎么跑通的,我们来举一个简单的例子。假设你对 AI(Host)说:“帮我把我电脑里的发票文件(Local Data Source A)打印出来,并把结果通知到我们公司的 Slack 群里(Remote Service B)。”

  • Host 收到你的话,AI 大脑开始思考,发现自己没手没脚,得找帮手。
  • MCP Client 立刻通过 MCP Protocol(数据线)同时呼叫两个扩展坞:
    • 呼叫 Local MCP Server A:去把本地的数据拿给我。Server A 转身去 Data Source A 拿出文件,原路返回给 AI。
    • 呼叫 Local MCP Server B:去把这个结果发到群里。Server B 转身通过 Web APIs 把消息打向互联网上的 Remote Service B。
  • 两个扩展坞都干完活了,回复 AI “搞定”。AI 再转告你:“发票已处理并通知到群里了!”


MCP 的具体案例

准备工作

我们以 MCP.so 的 百度地图 为例,由于百度地图全面支持了 MCP 协议,我们把它变成我们自己构建的大模型应用的功能触角。在编码之前,个人需要 申请百度地图的开发者 AK,并配置 BAIDU_MAP_API_KEY 到环境变量,这里不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// https://github.com/baidu-maps/mcp
// npm install @baidumap/mcp-server-baidu-map
{
"mcpServers": {
"baidu-map": {
"command": "npx",
"args": [
"-y",
"@baidumap/mcp-server-baidu-map"
],
"env": {
"BAIDU_MAP_API_KEY": "xxx"
}
}
}
}


依赖配置

父项目配置请参考 父项目 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
<dependencies>
<!-- langchain4j 低阶 API 整合 spring -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>

<!-- langchain4j 高阶 API 整合 spring(比如 @AiService)-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>

<!-- langchain4j 整合第三方平台 -->
<!-- 此处以接入阿里百炼平台为例 https://docs.langchain4j.dev/integrations/language-models/dashscope -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>

<!--MCP client MCP支持--> 👈🏻
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>

<!--流式响应依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>

<!-- spring boot webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
</dependencies>


原生方式使用MCP

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
public static void main(String[] args) {
// LangChain4j 底层可以用多种技术发 HTTP 请求(如 langchain4j 提供的、Spring 提供的)。
// 脱离了 Spring 容器时,必须由我们亲手指明:“请用 langchain4j 提供的客户端”,否则程序会因为选择困难症而崩溃。
System.setProperty("langchain4j.http.clientBuilderFactory", "dev.langchain4j.http.client.jdk.JdkHttpClientBuilderFactory");

// 初始化大模型客户端(由于阿里百炼兼容 OpenAI 协议,这里直接借壳)
OpenAiStreamingChatModel streamingChatModel = OpenAiStreamingChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("QWEN_API_KEY"))
.modelName("qwen3.7-plus")
.logRequests(true)
.logResponses(true)
.build();

// 1. 构建 MCPTransport 协议
// StdioMcpTransport(标准输入输出传输层):这是 MCP 协议的核心。
// 它对 Java 来说就像一个“线缆”,通过操作系统的标准管道(stdin/stdout),把本地的 Node.js 进程强行挂载到 Java 进程下面。
// 除此之外 MCP 还支持 StreamableHttpMcpTransport、WebSocketMcpTransport 等不同的网络传输方式
String apiKey = System.getenv("BAIDU_MAP_API_KEY");
McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("npx", "-y", "@baidumap/mcp-server-baidu-map"))
.environment(Map.of(
"BAIDU_MAP_API_KEY", apiKey // 把百度地图的密钥当作环境变量塞给 Node 进程
))
.logEvents(true) // 开启后,Java 与 Node 进程之间每一次通过 JSON-RPC 传递数据的细节都会打印出来(生产环境注意关闭)
.build();

// 2. 构建 MCPTransport 客户端
// 根据刚才配置好的 “数据线”(transport),创建一个真正的 MCP 客户端实例
DefaultMcpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.initializationTimeout(Duration.ofSeconds(60)) // npx 下载可能慢,超时时间建议拉长到 60s
.toolExecutionTimeout(Duration.ofMinutes(2)) // 地图网络请求较慢,给工具执行留足 2 分钟的容错时间
.build();

// 3. 创建工具集
// 把 MCP 客户端识别到的工具库,打包成 LangChain4j 的 Provider
// MCP 客户端启动后,会向 Node 服务发一个 “请出示你的技能清单” 的请求。
// Node 会返回:[“我能查天气”、“我能查具体坐标”]。这个 Provider 就是用来集中接管这些技能的。
McpToolProvider mcpToolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.failIfOneServerFails(false) // 如果挂载了多个 MCP 服务,其中一个挂了,不影响其他服务的运行
.build();

// 4. 将工具集能力整合到 AiService 接口上
// 把大模型的大脑、地图的肉身(工具)融合成一个高阶服务
// 开发者在表面上看只是在调一个 Java 接口(BaidumapAssistant),但在底层,LangChain4j 已经织好了一张网:
// 大模型在聊天时如果需要地图数据,就会自动通过 mcpToolProvider 去调用后台的 Node 进程。
BaidumapAssistant baidumapAssistant = AiServices.builder(BaidumapAssistant.class)
.streamingChatModel(streamingChatModel)
.toolProvider(mcpToolProvider)
.build();


// 通过门闩锁和响应式流处理异步打印
CountDownLatch latch = new CountDownLatch(1);
try {
System.out.println("--- 开始向大模型提问 ---");
// a. 获取响应式流,注意此时大模型并没有真正被触发,它只是返回了一根准备放水的水管(Flux 流对象)
Flux<String> chatTokenStream = baidumapAssistant.chat("今天北京的天气如何");
// b. 订阅这个流(真正触发执行)
chatTokenStream.subscribe(
// 监听器 A - Consumer:每当大模型从云端吐出一个字(Token),就会触发这个回调
token -> {
System.out.print(token);
System.out.flush();
},
// 监听器 B - ErrorConsumer:流传输过程中一旦发生异常(如网络中断、地图鉴权失败),会触发这里
error -> {
System.err.println("\n发生异常: " + error.getMessage());
latch.countDown(); // 出错时解锁 errorConsumer
},
// 监听器 C - CompleteConsumer:大模型把话全部说完了,流正常关闭时会触发这里
() -> {
System.out.println("\n\n--- 文本流传输结束 ---");
latch.countDown(); // 正常结束时解锁 completeConsumer
}
);
// c. 门闩卡死主线程,主线程在这里安营扎寨,直到上面的 CompleteConsumer 帮它把大门打开
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("--- 正在关闭 MCP 客户端管道 ---");
mcpClient.close();
}
}


高级方式使用 MCP

配置文件

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
33
34
35
36
37
38
39
40
41
42
43
44
45
server:
port: 8080
servlet:
encoding:
# 避免流式输出中文乱码
charset: UTF-8
enabled: true
force: true

# https://docs.langchain4j.dev/tutorials/spring-boot-integration
langchain4j:
open-ai:
# 向容器中注入了一个 chatModel 对象
chat-model:
base-url: http://localhost:11434/v1
api-key: api-key-xxx
model-name: "qwen3:4b"
log-requests: true
log-responses: true
timeout: PT300S # 允许网络层等待300 秒(LangChain4j 的 Duration 格式)
# 向容器中注入了一个 openAiStreamingChatModel 对象
streaming-chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${QWEN_API_KEY}
model-name: qwen3.7-plus
log-requests: true
log-responses: true

#第三方社区LLM
#langchain4j:
# community:
# dashscope:
# chat-model:
# api-key: ${QWEN_API_KEY}
# model-name: qwen3.7-plus
# enable-search: true
# streaming-chat-model:
# api-key: ${QWEN_API_KEY}
# model-name: qwen3.7-plus
# enable-search: true

logging:
level:
root: INFO
dev.langchain4j: DEBUG


声明 AiService

BaidumapAssistant

1
2
3
4
5
6
7
8
9
10
public interface BaidumapAssistant {

@SystemMessage("""
你是一个地图助手。
【铁律】当用户询问地理位置、POI、路线时,你【必须】且【只能】通过调用 `search_location` 工具来获取数据。
如果工具没有返回数据,或者调用工具失败,请直接老老实实回答:“对不起,我由于网络原因无法读取地图插件,无法为您提供准确坐标。”
绝对绝对禁止凭空捏造任何经纬度和地址!
""")
Flux<String> chat(@UserMessage String question);
}

McpBaiduMapConfiguration:

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
import demo02.service.BaidumapAssistant;
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.stdio.StdioMcpTransport;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.List;
import java.util.Map;

@Configuration
public class McpBaiduMapConfiguration {

@Bean(destroyMethod = "close") // 当应用关闭时,Spring 容器会自动把拉起的 node 管道进程彻底 kill 干净,优雅下线
public McpClient baiduMapMcpClient() {
String apiKey = System.getenv("BAIDU_MAP_API_KEY");
McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("npx", "-y", "@baidumap/mcp-server-baidu-map"))
.environment(Map.of(
"BAIDU_MAP_API_KEY", apiKey
))
.logEvents(true) // 确保这个开着,它会在控制台打印所有跟 Node 进程交互的 JSON-RPC 原始报文(生产环境注意关闭)
.build();

return new DefaultMcpClient.Builder()
.transport(transport)
.initializationTimeout(Duration.ofSeconds(60)) // npx 下载可能慢,超时时间建议拉长到 60s
.toolExecutionTimeout(Duration.ofMinutes(2))
.build();
}

@Bean
public ToolProvider mcpToolProvider(McpClient baiduMapMcpClient) {
return McpToolProvider.builder()
.mcpClients(List.of(baiduMapMcpClient))
.failIfOneServerFails(false)
.build();
}

@Bean
public BaidumapAssistant mapAssistant(
StreamingChatModel streamingChatModel,
ToolProvider mcpToolProvider) {
return AiServices.builder(BaidumapAssistant.class)
.streamingChatModel(streamingChatModel)
.toolProvider(mcpToolProvider)
.build();
}
}


业务测试类

BaidumapMcpController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/mcp/badiu-map")
public class BaidumapMcpController {

private final BaidumapAssistant mapAssistant;
public BaidumapMcpController(BaidumapAssistant mapAssistant) {
this.mapAssistant = mapAssistant;
}

@GetMapping(value = "chat", produces = MediaType.TEXT_PLAIN_VALUE)
public Flux<String> chat(@RequestParam String q) {
return mapAssistant.chat(q);
}
}

控制台日志

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
HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...xY], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen3.7-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个地图助手。\n【铁律】当用户询问地理位置、POI、路线时,你【必须】且【只能】通过调用 `search_location` 工具来获取数据。\n如果工具没有返回数据,或者调用工具失败,请直接老老实实回答:“对不起,我由于网络原因无法读取地图插件,无法为您提供准确坐标。”\n绝对绝对禁止凭空捏造任何经纬度和地址!\n"
}, {
"role" : "user",
"content" : "今天北京的天气如何"
}, {
"role" : "assistant",
"tool_calls" : [ {
"id" : "call_8d62477e9d78406b8423f29b",
"type" : "function",
"function" : {
"name" : "map_weather",
"arguments" : "{\"location\": \"116.404,39.915\"}"
}
} ]
}, {
"role" : "tool", 👈🏻
"tool_call_id" : "call_8d62477e9d78406b8423f29b",
"content" : "{...}"
"stream" : true,
"stream_options" : {
"include_usage" : true
},
"tools" : [ { 👈🏻
"type" : "function",
"function" : {
"name" : "map_geocode",
"description" : "地理编码服务",
"parameters" : {
"type" : "object",
"properties" : {
"address" : {
"type" : "string",
"description" : "待解析的地址(最多支持84个字节。可以输入两种样式的值,分别是:1、标准的结构化地址信息,如北京市海淀区xxx【推荐,地址结构越完整,解析精度越高】2、支持“*路与*路交叉口”描述方式,如北一环路和阜阳路的交叉路口第二种方式并不总是有返回结果,只有当地址库中存在该地址描述时才有返回。)"
}
},
"required" : [ "address" ]
}
}
}, ...
{
"type" : "function",
"function" : {
"name" : "map_poi_extract",
"description" : "POI智能标注",
"parameters" : {
"type" : "object",
"properties" : {
"textContent" : {
"type" : "string",
"description" : "描述POI的文本内容"
}
},
"required" : [ "textContent" ]
}
}
} ]
}

HTTP response:
- status code: 200
- headers: [:status: 200], [content-type: text/event-stream;charset=utf-8], ...
- body: null

根据查询结果,今天(4月30日)北京的天气情况如下:

## 🌧️ 当前天气
- **天气状况**:小雨
...