LangChain4j接入MCP:Java Agent工具调用标准化实战

📅 发布时间:2026/10/17 16:13:05
LangChain4j接入MCP:Java Agent工具调用标准化实战
最近在给内部一个 Java 写的助手系统接大模型工具调用一开始大家各写各的 Tool 方法十几个服务接完之后维护成本直线上升。后来我把工具接入统一迁到 LangChain4j 的 MCPModel Context Protocol方案上代码量不增反减后续再加新工具基本不用动宿主工程。这篇文章把 LangChain4j 接 MCP 的完整路线写清楚MCP 解决了什么、服务端怎么来、客户端怎么装配、一次真实调用怎么跑通、以及我实测中踩到的坑。作为 LangChain4j 系列里偏协议集成的一篇内容会比较贴近实战适合正在用 Java 做 LLM 应用想把外部工具和数据源标准化接进 Agent 的开发者参考。1. MCP 到底把 Java 接入工具的什么问题解决了1.1 没有 MCP 之前的日子在 LangChain4j 里要让大语言模型调用你的业务能力最常见的写法是给一个方法贴上 Tool 注解Tool(获取指定订单的状态) public String getOrderStatus(String orderId) { return orderService.queryStatus(orderId); }然后把它塞给 AiServicesAssistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderToolService()) .build();这个模式在单体 Demo 里很顺手但做产品时痛点会一个一个冒出来。第一个痛点是工具实现和宿主代码焊死了。这个工具方法写在哪个类里就只能跟着那个类走换个项目想复用要么复制代码要么抽公共包。第二个痛点是工具没有任何统一 schema。A 团队返回 JSONB 团队返回纯字符串参数命名、类型、必填规则全靠人肉对齐模型调用错了也只能报错。第三个痛点是跨团队、跨语言基本没法谈共享。Java 服务里写好的工具Python 团队要用只能重新实现一遍。这些痛点本质上不是 LangChain4j 的问题而是整个行业当时缺一个“工具接入标准”。各家框架都有自己的 Tool、Function、ToolExecutor接法千奇百怪工具却没办法流转起来。1.2 MCP 的协议层思路一次定义处处调用MCP 做的事情说穿了就是定义了一套标准协议让 AI 应用和“工具提供方”之间用统一的方式对话。协议里有两个核心角色MCP Host运行大模型应用的主程序对 LangChain4j 来说就是咱们写的 Java 服务。MCP Server暴露能力的独立服务可以是一个本地子进程也可以是一个远程 HTTP 服务。Host 通过一个 MCP Client 和 Server 通信通信内容遵循 JSON-RPC 格式。Server 对外暴露三类原语原语作用Tools工具可被模型调用的函数包含名称、描述、JSON Schema 参数定义Resources资源带 URI 的可读取数据比如文件、日志、数据库记录Prompts提示词模板可复用的 prompt 模板服务端定义后直接给 Host 用我用一个日常类比来理解这件事早年每台电脑接打印机都要装厂商专用驱动换一台打印机就得重来。后来有了通用驱动协议打印机随便换驱动不用动。MCP 就是大模型应用界的通用驱动协议。工具提供方只要按协议实现一次 Server所有支持 MCP 的 Host 都能用Host 这边只要实现一个标准 Client就能接所有协议兼容的工具。这套思路对工具方极其友好以前写一个工具要同时适配各种框架的注解和回调现在只要把工具描述成协议里的 schema剩下的交给 MCP 生态去适配。1.3 LangChain4j 在 MCP 里的位置ToolProvider 是关键LangChain4j 能顺利接 MCP核心在于它的ToolProvider机制。传统方式里模型能调什么工具是在构建 Assistant 时用 .tools(...) 静态注册的注册之后就固定下来。而 ToolProvider 是一个更灵活的动态工具源接口它可以在模型发起调用时按需提供工具列表和工具执行逻辑。langchain4j-mcp 这个模块做的事情就是把一个 MCP Client 包装成 ToolProvider。一旦包装完成MCP Server 里暴露的所有工具在 LangChain4j 眼里就和本地 Tool 方法平起平坐模型该调用调用该传参传参完全感受不到工具运行在另一个进程里。这个设计让 LangChain4j 站在了一个比较舒服的位置底层协议走的是标准 MCP上层模型交互走的是 AiServices 那套成熟抽象。工具即插即用不需要为每一种 Server 单独写胶水代码。2. 动手前先搞懂依赖选型与 MCP Server 从哪来2.1 依赖引哪几个版本怎么对LangChain4j 的 MCP 支持需要引入两个依赖dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-mcp/artifactId version1.x以你自己项目实际使用的版本为准/version /dependency dependency groupIdio.modelcontextprotocol/groupId artifactIdjava-sdk/artifactId version和 langchain4j-mcp 期望的版本对齐/version /dependency第一个是 LangChain4j 的 MCP 适配模块。第二个是 MCP 官方 Java SDK负责真正的协议通信、客户端构建、会话管理。langchain4j-mcp 不是从零实现 MCP它是在官方 Java SDK 之上做了一层 ToolProvider 适配。这里有个容易被忽略的点这两个依赖的版本必须对齐。langchain4j-mcp 在某个版本里基于 java-sdk 的某个 API 编译如果你单独把 java-sdk 升到更高版本运行时很可能会出现 NoSuchMethodError。后面踩坑章节会细讲。选版本的时候我的建议是直接用你当前 LangChain4j 主版本配套的 MCP 模块版本而不是追最新。MCP 协议本身还在演进稳定比新功能重要。2.2 快速准备一个能跑的 MCP Server客户端代码写之前得先有一个能连的 MCP Server。准备方式有三种按“跑通速度”排序方式一直接用 npx 拉现成示例包MCP 官方提供了一批示例 Server放在 npm 生态里。开发调试最省事的方式就是直接用 npx 命令拉起一个npx -y modelcontextprotocol/server-everything这条命令会在本地启动一个标准输入输出stdio模式的 MCP Server暴露一大堆示例工具拿来测试客户端接入非常合适。第一次执行时 npx 会先下载包可能会等十几秒到几十秒属于正常现象。方式二自己用 Java SDK 写一个小 Server如果你不想依赖 Node 生态也可以用 MCP 官方 Java SDK 写一个最简单的 Server。MCP Server 的本质就是按协议处理两类请求tools/list 返回工具清单tools/call 执行工具并返回结果。你只需要选择一种传输方式stdio 或 HTTP然后注册工具处理逻辑即可。自己写的好处是能完全控制工具行为也方便加日志跟踪但初期不建议一上来就折腾这个先用现成包把链路跑通再说。方式三连一个远程 Streamable HTTP ServerMCP 还支持远程调用。很多团队会把内部工具部署成一个 HTTP 模式的 MCP Server客户端只需要给一个 URL就能像连数据库一样连上它String serverUrl https://your-mcp-server.example.com/mcp; Mcp.Client httpClient McpClient.builder(TransportType.HTTP, serverUrl) .build();远程模式适合跨机器、跨语言共享工具但前提是你能确认这个端点是可信的且通信链路做了鉴权。2.3 stdio 与 Streamable HTTP本地调试和远程部署怎么选MCP 传输方式里最常用的是 stdio 和 Streamable HTTP。我做选型时的参考维度如下维度stdioStreamable HTTP进程关系Server 作为子进程被 Host 拉起Server 是独立部署的 HTTP 服务适用场景本地开发、单机部署、内网服务跨机器、跨团队、多应用共用启动成本每次由客户端拉起冷启动可能慢服务常驻启动成本由服务方承担运维复杂度依赖 Host 管理子进程生命周期需要额外部署、鉴权、限流调试体验日志直接输出到宿主控制台直观依赖服务端日志往往要多跳一跳我的习惯是开发阶段一律用 stdio。调试的时候可以在 MCP Server 里打日志日志直接跟着宿主进程走排查问题极方便。等真正要上线给多个应用共享时再考虑把高频工具部署成远程 HTTP 模式。3. 核心装配把 MCP 工具挂进 LangChain4j 的完整代码3.1 创建 MCP 客户端参数和初始化第一步是创建 MCP 客户端。以 stdio 模式为例import io.modelcontextprotocol.spec.McpClient; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.TransportType; StdioServerParameters params StdioServerParameters.builder() .command(npx) .args(-y, modelcontextprotocol/server-everything) .build(); Mcp.Client client McpClient.builder(TransportType.STDIO, params) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize();这里有两件事值得注意。第一initialize() 必须显式调用。这一步完成 MCP 握手机制客户端会发送 initialize 请求Server 返回协议版本和服务能力信息。跳过这一步直接去拿工具列表通常会因为状态不对而失败。第二requestTimeout 尽量调大一点。如果 Server 是 npx 拉起来的第一次启动要现场下包十秒都不一定够。默认超时往往扛不住这种冷启动场景我直接给到 30 秒。3.2 McpToolProvider 内部干了什么客户端建好之后需要用 LangChain4j 的工具提供器把它接进 AiServicesMcpToolProvider mcpToolProvider McpToolProvider.builder() .mcpClients(List.of(client)) .build();这句话背后的转换逻辑拆开看是三件事拉取工具清单。McpToolProvider 构建之后会向已连接的所有 MCP 客户端发 tools/list 请求拿到 Server 暴露的每一个工具定义包括名称、描述、参数 schema。转换成 ToolSpecification。LangChain4j 模型层不关心 MCP 协议它只认自己的 ToolSpecification。McpToolProvider 会把 MCP 的工具定义逐字段翻译过去。代理工具调用。模型决定调用某个工具时McpToolProvider 拦截请求向 MCP Server 发 tools/call拿到执行结果后再回传给模型。这个机制决定了 MCP Server 新增一个工具宿主工程几乎不需要改代码。再次构建 Assistant 时新的工具定义会自动被拉取并暴露给模型。3.3 一个能跑的最小 Java 工程把上面的内容串起来一个最小可跑的客户端接入长这样import dev.langchain4j.mcp.McpToolProvider; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.service.AiServices; import java.time.Duration; import java.util.List; interface Assistant { String chat(String userMessage); } public class McpDemo { public static void main(String[] args) { // 1. 构建 stdio 模式的 MCP 客户端 StdioServerParameters params StdioServerParameters.builder() .command(npx) .args(-y, modelcontextprotocol/server-everything) .build(); Mcp.Client client McpClient.builder(TransportType.STDIO, params) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize(); // 2. 把 MCP 客户端包装成 LangChain4j 的 ToolProvider McpToolProvider mcpToolProvider McpToolProvider.builder() .mcpClients(List.of(client)) .build(); // 3. 构造一个支持 function calling 的模型 // 这里用你实际使用的模型服务构造 ChatLanguageModel 即可 ChatLanguageModel model ...; // 4. 装配 Assistant Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .toolProvider(mcpToolProvider) .build(); // 5. 和助手对话模型会自动决定是否调用 MCP Server 上的工具 String answer assistant.chat(帮我查一下某城市今天的天气); System.out.println(answer); } }需要注意模型构造部分我没有写死具体实现。你可以用任何支持 function calling 的 ChatLanguageModel不管是自部署模型还是云端接口构造方式以你实际用的模型服务为准。只要模型支持工具调用这段代码的装配逻辑完全通用。3.4 多 Server 汇聚与生命周期管理实际项目里一个 Agent 往往需要挂多个 MCP Server。McpToolProvider 的 builder 支持传入多个客户端McpToolProvider mcpToolProvider McpToolProvider.builder() .mcpClients(List.of(weatherClient, databaseClient, fileClient)) .build();所有客户端暴露的工具会被合并成一个工具池模型面对的是一个统一的工具集合。这样组织代码的好处是按职责拆分 Server宿主工程只做工具汇聚和编排。生命周期方面我的建议是MCP Client 尽量做成单例和应用同生命周期。每个 MCP Client 背后可能拉着一个子进程如果业务请求每次进来都 new 一个 Client服务跑半天就会堆出一堆孤儿进程。开发时把 Client 挂在服务启动阶段退出时统一 close能省掉大量排查进程泄漏的时间。4. 从“模型想用工具”到“拿到真结果”完整调用链路与验证4.1 跑一次最小 Demo先看现象代码写完后直接跑 main 方法然后把日志级别打开logger nameio.modelcontextprotocol levelDEBUG/让 MCP 协议层的日志输出到控制台。你会发现输入那句“帮我查一下某城市今天的天气”之后整个调用过程是有明显阶段的。我实测时看到的第一个现象是模型并没有直接去“查天气”而是先返回了一个 ToolCall。日志里能看到模型认为应该调用某个工具并且带上了参数。真正执行工具的是框架和 MCP Server模型本身只是表达了“我打算用这个工具”。这个认知很重要很多人误以为模型会直接触发工具执行实际上模型永远只负责决策执行是框架干的事。4.2 链路拆解模型请求、工具调用、结果回填一次完整的 MCP 工具调用拆开看是四个阶段第一阶段模型请求阶段。用户输入经过 AiServices 进入模型请求。模型看到系统里已经注册了一堆工具定义会判断当前问题是否需要调用工具。如果需要它就在响应的 toolCalls 部分返回一个调用意图携带工具名和参数但不会直接执行。第二阶段框架择路阶段。LangChain4j 收到模型返回的 ToolCall 后会根据工具名去 ToolProvider 里找对应实现。因为 McpToolProvider 已经把所有 MCP 工具注册进去了这里会直接命中并生成一次 MCP 协议调用。第三阶段协议执行阶段。MCP Client 向 Server 发送 tools/call 请求。Server 执行真实业务逻辑比如查询数据库、调用第三方接口然后把结构化结果返回。第四阶段结果回填阶段。LangChain4j 把 Server 返回的工具执行结果作为 ToolMessage 放回会话上下文再次调用模型。这时模型掌握了工具的真实输出会基于这个结果组织一段自然语言答复给你。我理解这套链路时脑子里始终绷着一根弦MCP Server 和模型之间没有直接关系。模型只看到工具的名字、描述和参数 schema至于工具在哪台机器上跑、用什么语言实现模型完全感知不到。所有的转发、等待、结果回填都是 LangChain4j 和 MCP 协议栈在工作。4.3 MCP 工具和本地 Tool 共存的一个写法接完 MCP 之后再接本地工具需要注意写法上的区分。MCP 工具通过 ToolProvider 动态提供本地工具通过 .tools(...) 静态注册两者可以同时指定Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .toolProvider(mcpToolProvider) .tools(new LocalTools()) .build();我在项目里的经验是外部通用能力走 MCP内部私有逻辑走本地 Tool。比如订单系统、用户中心这些内部服务直接由领域服务提供方法即可而天气查询、文件处理、外部 API 对接这类相对通用能力放 MCP Server 里统一管理。这种混排方式对代码组织很友好。本地 Tool 方法可以自由使用 Spring 注入的 Bean调试起来也直接MCP 工具则天然具备跨系统复用能力。两者互补而不是互相替代。5. 跑通之后值得记下来的坑5.1 版本错位导致 NoSuchMethodError这是接入 MCP 最常见的运行时问题。现象是应用启动没问题一发起工具调用就抛 NoSuchMethodError报错信息指向某个 MCP 内部类的方法。根因基本可以锁定在 langchain4j-mcp 和 java-sdk 版本不匹配。langchain4j-mcp 是围绕某个 java-sdk 版本编译的你用 Maven 单独指定了一个更高的 java-sdk 版本协议层 API 已经变了运行时就容易踩到不存在的旧方法。解决方式很简单langchain4j-mcp 的 pom 里通常声明了它依赖的 java-sdk 版本用 Maven 的依赖树检查一下把 java-sdk 版本调到和它一致。或者干脆只在 pom 里写 langchain4j-mcp让传递依赖自动决定 java-sdk 版本。5.2 npx 冷启动把 initialize 拖超时用 npx 拉 Server 时第一次执行需要下载 npm 包这个过程在网络一般的情况下可能超过一分钟。而 Client 默认的 requestTimeout 往往只有几秒到十几秒initialize 阶段直接超时。处理手段有几个按优先级排列把 requestTimeout 拉大到 30 秒以上至少保证首次冷启动能扛过去。在正式跑程序之前先手动执行一次npx -y modelcontextprotocol/server-everything并立刻退出让 npm 包缓存到本地。如果 Server 用的包是你们团队自己的可以提前打成可执行文件或者用全局安装方式绕开 npx 的下载逻辑。这个坑只出现在 stdio 模式的本地子进程场景。远程 HTTP 模式 Server 常驻反而不存在冷启动问题。5.3 工具挂上了模型却“视而不见”工具列表明明拉取到了日志里也能看到 ToolProvider 返回了一堆工具定义但模型回答问题时压根不调用。这种情况我排查过多次原因一般集中在三处一是工具名太抽象。MCP Server 里如果工具名长得像内部变量名模型很难理解它的用途。工具名要短、要直白描述里写清楚“什么时候该调用它”不要在描述里堆修饰词。二是模型本身不支持 function calling。工具调用能力不是所有模型都有有些模型你给它塞一堆工具 schema它直接忽略当作普通聊天处理。开发期一定要先用具备 function calling 能力的模型验证跑通之后再根据预算换模型。三是挂载的工具太多。一个 Provider 汇聚了几十个工具模型在计算时会有选择困难也可能因为上下文空间被工具定义挤占而放弃调用。控制单次请求暴露的工具数量比把所有工具一股脑交给模型更实际。5.4 stdio 子进程泄漏与 close 管理MCP stdio 模式的 Server 是宿主进程的子进程。如果宿主反复创建 Client 而不关闭子进程会越积越多最终出现“僵尸进程一堆机器负载莫名其妙升高”的诡异现象。正确做法是复用 Client 单例不要每次请求都构建。应用退出前关闭 Client比如 Spring Boot 里注册一个 DisposableBean 或在 shutdown hook 中调用 client.close()。如果确实需要动态按需创建 Client那就要成对管理创建后放到一个会话池里会话结束统一关。MCP Server 是有真实资源开销的独立进程不是线程池里一个任务那么简单。把它当成连接池资源来管理问题就能避免大半。5.5 远程 MCP Server 别乱接权限要收敛远程 HTTP 模式用起来很方便但也容易把安全边界搞丢。MCP Server 暴露的工具对于模型来说都是可执行能力如果 Server 里有删除数据、执行命令这类高危操作而你又没有做权限限制等于把高危能力直接交给了模型的随机决策。我给自己定了几条规矩只连可信内网的 MCP Server不在生产环境直接连外部地址。工具权限遵循最小化原则Server 只暴露当前业务真正需要的能力。敏感操作的工具在 Server 侧加参数校验、操作确认或审计日志而不是寄希望于模型“不会误调用”。工具接入越顺畅越要留意边界。毕竟协议本身不判断工具危险性判断责任在接入方。最后说说我现在的习惯。做 Java 侧的 LLM 应用时凡是内部、私有、和业务强绑定的工具我仍然直接用 Tool 写在服务类里调试直观性能开销小。凡是需要跨系统复用、或者要开放给多个应用共享的能力我统一挂到 MCP Server 上配置文件里列 Server 清单由 McpToolProvider 批量接进来。MCP 的 Resources 和 Prompts 在 LangChain4j 里的支持还在起步等这几个原语稳定了接数据资源和共享提示词还会有更多玩法。前期先用工具把链路跑通收益就已经很明显了。