☰
Java生态集成MCP:用LangChain4j接入大模型外部工具实践
2026/10/17 3:11:18 网站建设 项目流程

能在大模型应用里把“让模型调用外部工具”这件事做成一套统一协议,MCP(Model Context Protocol)确实是目前最被看好的方案。Java 生态这边,LangChain4j 从某个版本开始把 MCP 支持做成了独立模块,这让我在处理“模型 + 工具”集成时省了很多手工适配的功夫。这篇文章就记录我用 LangChain4j 的 MCP 模块,在模拟项目 X 里接入外部工具服务的完整过程,从协议理解、模块选型到实际代码、踩坑经历都有,适合已经在用 LangChain4j 做 AI 应用、想给模型接自定义工具的 Java 开发者。

1. 为什么要在 Java 生态里接 MCP 协议

1.1 先搞清楚 MCP 解决的是什么问题

大模型本身不会操作外部系统,比如查数据库、读文件、发请求,这些能力都得靠外部工具。过去每接一个工具,就要写一套专用的适配代码。工具一多,代码就变成一团乱麻。MCP 把“工具暴露”和“工具调用”两端的接口标准化了:工具提供方只要实现一个 MCP Server,暴露统一接口,所有支持 MCP 的客户端就能直接发现并调用这些工具。等于把点对点的“蜘蛛网”集成,简化成了“一个标准接口接所有”。

这个思路对 Java 后端开发者来说其实很熟悉,就像 JDBC 统一了数据库访问,ODBC 统一了 Windows 上的数据源。MCP 就是 AI 应用领域的“JDBC”,只是它统一的是大模型获取工具、数据、能力的方式。

1.2 LangChain4j 为什么值得关注

Java 生态里做 AI 应用,可选框架不算多。LangChain4j 是目前比较活跃的一个,它把很多大模型相关的操作都封装成了 Java 风格良好的 API,支持多种模型服务商,同时支持 AI 服务(AiServices)、提示词模板、对话记忆、RAG 等能力。MCP 模块推出之后,LangChain4j 基本上补上了“工具接入”这块最关键的一环。

我在最早接触 MCP 时有个明显感受:协议本身不复杂,但真要手工实现客户端,要用 JSON-RPC 管理状态、处理流式传输、维护连接生命周期,工作量大且容易出错。LangChain4j 的 MCP 模块把这些都封装好了,可以直接用熟悉的 Builder 方式构造客户端,然后像注册普通 Java 方法一样把工具绑定给 AI 服务。

2. MCP 协议核心机制与 LangChain4j 的对应关系

2.1 协议的分层结构和衔接方式

MCP 从逻辑上可以拆成三层:

  • 协议层:定义消息格式和交互流程,基于 JSON-RPC 2.0。
  • 传输层:规定消息怎么在客户端和服务端之间传递,当前常见的有 stdio、HTTP + SSE、Streamable HTTP。
  • 客户端/服务端模型:宿主应用(比如 AI 应用)是 MCP Client,提供能力的进程或服务是 MCP Server。

用生活类比来解释就是:协议层相当于“双方约定的交流语言”,传输层相当于“信件是走人工送还是快递公司送”,客户端和服务端则是“写信的人和收信的人”。MCP 只是约定好语言和邮寄方式,至于信里的内容怎么用,完全由两端自己决定。

2.2 几个关键的 JSON-RPC 方法

一次典型的 MCP 交互包含这几个环节:

阶段方法名作用
连接初始化initialize交换协议版本、能力声明、客户端/服务端信息
通知就绪notifications/initialized客户端告知服务端初始化已完成
获取工具列表tools/list返回当前服务端暴露的工具定义(JSON Schema)
调用工具tools/call传入工具名和参数,执行并返回结果

initialize里还有一个不可忽视的字段叫capabilities。客户端在握手时声明自己支持哪些能力(比如工具、资源、提示词),服务端也会返回它的能力范围。如果某一端不支持工具功能,后面获取工具列表就可能失败或返回空列表。我刚开始排查问题的时候,很大一部分时间都花在“功能正常但不返回工具列表”,后来发现是服务端配置里没打开工具能力。

2.3 LangChain4j 里的模块映射

LangChain4j 的 MCP 支持主要落在langchain4j-mcp模块,核心抽象包括:

  • McpServer:封装连接到一个 MCP Server,并负责通信生命周期。
  • McpClient:负责发送 JSON-RPC 请求,处理响应。
  • StdioMcpTransport和HttpMcpTransport:分别实现 stdio 和 HTTP + SSE 传输。
  • McpToolProvider:把 MCP 工具暴露成 LangChain4j 的 Tool 接口,供 AI 服务调用。

一个常见理解误区是:LangChain4j 并不内置一个“MCP 服务端实现”,它默认当“客户端”。如果你的 Java 项目想对外提供 MCP 服务,需要单独引入 MCP Server 库,或者用其他语言的参考实现。这一点在动手前先想清楚,能避免后面走弯路。

3. 开发环境与 Maven 依赖配置

3.1 基础环境要求

做 MCP 相关的开发,建议环境是 JDK 17+,Maven 3.6 以上,并准备好一个可以访问的大模型 API 服务。HttpMcpTransport底层用 JDK 的HttpClient,JDK 11 以上就够用,但 17 更省心。如果你要调试 stdio 子进程,最好在 Linux 或 macOS 上操作,Windows 下遇到了账号空间和编码问题,后面会单独讲。

3.2 引入依赖的实际配置

先在项目的pom.xml里加langchain4j-mcp:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-mcp</artifactId> <version>1.x.x</version> </dependency>

1.x.x表示当前主版本,实际使用请根据自己的项目版本号调整,最好保持与langchain4j主模块版本一致,避免传递依赖冲突。

再加一个 JSON 序列化库,MCP 的 JSON-RPC 消息离不开它:

<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> </dependency>

如果只接 stdio 类型的服务端,上面的配置就够了。如果还要走 HTTP + SSE,注意HttpMcpTransport依赖的是 JDK 内置java.net.http.HttpClient,不需要额外引第三方 HTTP 库。但远程服务端处于外网时,务必保证你的网络环境能直接访问该地址。

我自己的习惯是每一步都先验证最小闭环,所以建议先加这两个依赖,后面再根据传输方式补充。

4. 实操一:基于 stdio 接入外部命令型工具

4.1 场景描述和如何启动一个 MCP Server

stdio 传输的含义是:客户端负责拉起一个子进程,然后通过标准输入(stdin)和标准输出(stdout)与子进程通信。这个传输方式最适合“本地工具”和“本地服务”。比如想在 Java 应用里让 AI 模型调用一个本地命令行工具,就适合用 stdio。

为了让操作可复现,我准备了一个模拟场景:某个本地 MCP 服务端,它能提供一个“查询天气”的工具。这个服务端通过npx启动,命令类似:

npx some-mcp-server --stdio

你需要把some-mcp-server替换成实际的 MCP Server 包名。自己在本地跑起来一个服务端后,才会有可联调的对象。

4.2 Java 侧连接 stdio 服务端的关键代码

在 LangChain4j 中创建并连接一个 stdio 类型的 MCP Server,代码可以这样写:

import dev.langchain4j.mcp.McpServer; import dev.langchain4j.mcp.client.transport.McpTransport; import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport; import java.time.Duration; import java.util.List; public class StdioMcpDemo { public static void main(String[] args) throws Exception { // 构建传输层,把启动命令按参数拆分传入 McpTransport transport = new StdioMcpTransport( new ProcessBuilder( "npx", "some-mcp-server", "--stdio" ).redirectErrorStream(true), Duration.ofSeconds(10) ); // 构建 MCP 客户端连接 McpServer server = McpServer.builder() .transport(transport) .build(); server.start(); // 列出这个服务端暴露了哪些工具 List<String> toolNames = server.tools() .stream() .map(tool -> tool.name()) .toList(); System.out.println("Available tools: " + toolNames); // 调用一个工具:查询城市天气 String city = "北京"; String result = server.callTool("query_weather", Map.of("city", city)); System.out.println("Weather result: " + result); server.stop(); } }

这段代码在执行时,会先启动一个子进程,然后通过 stdio 完成 MCP 的 initialize 流程。server.tools()会拿到服务端返回的工具定义列表,server.callTool()会触发tools/call请求,并把 JSON 参数传给服务端。LangChain4j 会自动完成 JSON-RPC 的封装和解封,所以写业务时像调用普通方法一样直观。

4.3 把 MCP 工具绑定到 AI 服务

光是直接调用工具还体现不出 MCP 的价值。真正常见的需求是:让模型自己决定“什么时候调用哪个工具”。LangChain4j 里对应的是AiServices,通过McpToolProvider把工具集绑定进去。

import dev.langchain4j.agent.tool.ToolSpecification; import dev.langchain4j.mcp.McpToolProvider; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.service.AiServices; // 假设这里是你配置好的模型实例 ChatModel chatModel = YourModelFactory.create(); // 用 MCP Server 创建工具提供器 McpToolProvider toolProvider = McpToolProvider.builder() .mcpServers(List.of(server)) .build(); // 定义一个 AI 服务接口 interface Assistant { String chat(String userMessage); } Assistant assistant = AiServices.builder(Assistant.class) .chatModel(chatModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .toolProvider(toolProvider) .build(); String answer = assistant.chat("北京今天适合出门吗?"); System.out.println(answer);

模型会基于用户的问题和工具定义来决定是否调用query_weather。整个过程里,模型发起的调用也是走 MCP 的tools/call,但对业务代码完全透明。这个设计让我觉得 LangChain4j 在工程化上做得确实灵活。

使用关键注意点:

  • 如果模型不会正确调用工具,优先检查工具的description是否写清楚了“什么时候用、参数是什么格式”。
  • McpToolProvider会为每个 MCP Server 里的工具生成 Tool 定义。工具特别多时,注意不要超过模型的上下文窗口。
  • AI 服务接口里的方法名不影响工具绑定,工具的触达靠的是 Tool 定义里的 name。

5. 实操二:基于 HTTP + SSE 接入远程工具服务

5.1 什么时候选用 HTTP 传输

如果 MCP Server 部署在另一台机器,或者你想让多个应用共享同一个工具服务,就不可能再用 stdio。这时用 HTTP + SSE 传输,客户端通过 HTTP 请求发送 JSON-RPC 消息,服务端通过 SSE(Server-Sent Events)推送响应或事件。

SSE 本质上是一次长连接。客户端建立一个流式请求,服务端持续往这个连接里写数据,直到事件结束。这种模式适合“一个请求返回一个结果”,即使结果分片到达也能连续推进。

5.2 配置 HTTP transport 的示例代码

假设远程 MCP Server 的地址为http://localhost:8080/mcp,且服务端实现了 Streamable HTTP 或 SSE 标准,Java 侧连接代码如下:

import dev.langchain4j.mcp.McpServer; import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport; import dev.langchain4j.mcp.client.transport.McpTransport; import java.net.URI; import java.time.Duration; import java.util.Map; public class HttpMcpDemo { public static void main(String[] args) throws Exception { McpTransport transport = HttpMcpTransport.builder() .sseUrl(URI.create("http://localhost:8080/mcp").toURL()) .timeout(Duration.ofSeconds(15)) .build(); McpServer server = McpServer.builder() .transport(transport) .build(); server.start(); // 调用远程工具 String result = server.callTool("remote_tool_name", Map.of("input", "demo")); System.out.println(result); server.stop(); } }

HTTP 传输的好处是跨平台、跨语言,部署和防火墙策略简单;坏处是引入了网络延迟和连接管理成本。实际业务里,如果工具服务不在本地且频繁调用,建议在框架层面做连接复用,不要每次调用都新建McpServer。

5.3 远程服务的安全与鉴权考虑

接远程 MCP 服务时,第一优先级是安全。MCP 本身允许在 initialize 阶段携带认证信息,HTTP 传输也支持在 header 里传 token。LangChain4j 的 HTTP transport 支持自定义 header,这里给出一个参考做法:

HttpMcpTransport transport = HttpMcpTransport.builder() .sseUrl(URI.create("http://your-server/mcp").toURL()) .header("Authorization", "Bearer your-token-here") .build();

我有一次把内部工具服务暴露到测试环境,第一版忘加鉴权,结果日志里出现不明来源的调用记录。那次之后我养成了一个习惯:任何 MCP Server 暴露到非本机环境,都必须至少加一层 token 校验,最好配合 IP 白名单和调用频率限制。工具服务是直接操作业务系统的入口,比普通 HTTP 接口更需要注意防护。

6. 常见问题与排查技巧实录

6.1 协议版本不匹配与握手失败

现象:连本地 stdio 服务端时,server.start()抛出异常,日志提示 initialize 返回的协议版本不被支持。

原因:服务端实现的 MCP 协议版本和客户端要求的版本不一致。

处理方式:

  • 查看双方日志,确认 initialize 请求和响应里的protocolVersion。
  • 升级或降级其中一方,保证版本一致。
  • LangChain4j 这类客户端通常会带一个默认版本,如果服务端版本老,可以参考框架文档里的兼容说明。

这类问题最麻烦的在于日志不直观,我排查时往往需要同时抓服务端标准输出和客户端异常栈,才能定位到具体是哪一侧版本声明有问题。

6.2 stdio 下子进程启动失败或输出乱码

现象:ProcessBuilder 启动子进程时抛IOException,或工具返回内容出现乱码。

原因:命令路径不对、参数拆分错误、运行账号权限不足,或平台编码不一致(Windows 尤甚)。

我用 ProcessBuilder 时踩过一次坑:把整条命令直接作为一个字符串传进去,结果子进程根本起不来。正确做法是用构造列表的写法,让 ProcessBuilder 自己处理参数转义。Windows 上建议把redirectErrorStream(true)打开,把 stderr 合并到 stdout,否则日志很难排查。编码问题通常可以通过统一为 UTF-8 环境变量解决。

6.3 工具列表获取成功但模型不调用工具

现象:server.tools()能返回工具列表,但让模型回答问题、需要查工具时,模型没走工具调用。

原因:工具描述不够明确,或者模型厂商的工具调用能力在特定参数下被关闭。

排查路径:

  • 打印模型收到的 Tool 定义,确认 name 和描述完整。
  • 检查模型配置里是否禁用了 function calling。
  • 把工具的description写全,明确“当用户询问 XX 时,必须调用此工具”。

很多情况下是描述含糊。比如我一开始写了“weather tool”,模型根本不知道什么时候该用;改成“查询指定城市当前天气,参数 city 为城市中文名”之后,几乎每次问相关话题模型都会触发调用。

6.4 超时与并发控制问题

现象:远程 MCP Server 调用时频繁超时;高并发场景下,连接占满或响应错乱。

问题根源:HTTP 传输需要网络往返,如果服务端处理慢,客户端的默认超时设置就会触发;连接没有做池化,多线程调用容易堆积。

参考建议如下表:

问题类型建议做法
单次调用超时调整 transport 的超时参数,并结合服务端实际响应耗时设置合理阈值
并发调用量大对McpServer做连接复用,避免每次创建新实例
数据量过大精简 MCP 工具返回内容,服务端可考虑对结果做截断
日志过多生产环境关闭 JSON-RPC 的 debug 级日志,否则日志会炸

6.5 常见问题速查表

现象可能原因快速处理
start()卡住不动握手未完成,服务端未响应检查服务端进程是否存活,抓 initialize 日志
tools/list返回空服务端能力未开启工具检查服务端配置里的 capabilities
调用工具返回空结果参数 schema 不符,服务端解析失败打印实际请求 JSON,与服务端工具定义对比
本地 stdio 启动崩溃命令路径错误或账号权限受限先用终端手动跑一遍启动命令
远程连接被拒绝鉴权失败或网络不通先 curl 测试接口连通性,再检查 header 鉴权

7. 实战心得与后续扩展建议

7.1 先手工验证,再接框架

无论接 stdio 还是 HTTP,我强烈建议你在一开始用独立的小程序或者测试脚本模拟一次 JSON-RPC 握手。比如手工给服务端发送一个 initialize 请求,观察响应内容。这样做有几个好处:你能直观理解协议流程,排查问题时能区分是框架问题还是服务端问题;同时也能确认服务端本身是否稳定,排除“服务端一启动就崩”这类干扰项。

7.2 工具命名与描述是真正的细节

MCP 工具是暴露给模型看的,模型的“判断依据”完全来自工具名和描述。命名建议用蛇形(query_weather、get_user_info),不要用驼峰。描述里写清楚触发条件和参数含义。我试过把description写成“按需查询”,效果很差;改成“用户询问天气且提供了城市名时,必须使用此工具”之后,触发率明显提升。

7.3 安全底线不能妥协

无论项目多小,MCP Server 一旦离开本机,就必须考虑鉴权、限流、日志审计。工具背后往往是真实操作,比如文件读写、数据更新、外部请求。工具调用的风险比普通接口大,因为模型可能通过 prompt 注入被诱导,恶意参数会让服务端做出预期外的操作。我的底线是:工具的入参必须严格校验,不允许把用户输入直接拼进系统命令。

7.4 这个能力还能怎么扩展

MCP 的覆盖面不只是工具。资源(Resources)和提示词(Prompts)也在协议范围内。LangChain4j 的 MCP 模块目前工具消费最成熟,但如果你需要让模型读取外部数据源,不一定要走 RAG 管线,也可以看 MCP 资源能力是否能满足。后续我打算在另一个项目里,把内部知识库做成一个 MCP Server,让本地 AI 助手直接通过 MCP 访问数据,这样既复用现有协议,又避免为每次接入写死代码。

MCP 的生态还在快速演进,LangChain4j 这块也在持续更新,往后版本可能出现新的传输方式或工具类型。但只要掌握了协议分层、握手流程、工具调用模式这几个核心点,无论生态怎么变,你都能很快迁移。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询