打通大模型与外部世界的桥梁:LangChain4j 实战 Model Context Protocol(MCP) 1. 为什么 Java 后端需要 LangChain4j 接入 MCP如果你是一个写 Java 的后端最近大概率被两个词反复刷屏LangChain4j 和 Model Context ProtocolMCP。前者是 Java 生态里最像样的 LLM 应用框架后者是 Anthropic 提出、现在被越来越多工具链采纳的「大模型 ↔ 外部世界」通信协议。把这两个东西拼在一起你就能让大模型真正调用你本地的工具、数据库、文件系统而不是只会聊天。先说清楚 MCP 到底解决什么问题。在没有 MCP 之前你想让大模型调用一个外部能力通常有三种土办法一是把工具描述硬编码进 Prompt模型输出一段 JSON你自己解析再执行二是给每个工具单独写一套 HTTP 客户端、鉴权、序列化逻辑三是用 Function Calling但每家模型的 schema 格式还不一样。结果就是每接一个工具你就得重写一遍胶水代码工具一多维护成本直接爆炸。MCP 的思路是把这件事标准化。它本质上是 HTTP JSON-RPC 的语义层再叠加一套工具Tool、资源Resource、提示词Prompt的描述规范。大模型和外部系统通过同一套协议对话工具的描述、参数、返回值都有统一格式。对 Java 开发者来说这意味着你不再需要为每个模型厂商适配不同的调用约定只要 MCP Server 暴露了工具LangChain4j 就能自动把它转成模型能理解的 ToolSpecification。那 LangChain4j 在这里扮演什么角色它是客户端侧的封装。你不需要手写 JSON-RPC 的报文也不需要自己管理 SSE 长连接、心跳、重连。LangChain4j 提供了 McpTransport、McpClient、McpToolProvider 三层抽象把底层通信和上层 AiServices 隔离开。你写业务代码时只需要关心「我要让模型调用哪个工具」剩下的协议细节框架帮你处理。这套组合适合谁我总结下来是三类人第一类是做企业内部 AI 助手的后端需要让模型查询内部工单系统、日志平台、CMDB第二类是做 DevOps 工具的想让模型直接操作 GitHub、K8s、数据库第三类是想把存量 REST/RPC 服务快速 AI 化的团队把老接口包一层 MCP Server就能被大模型调用。这三类场景的共同点是工具多、变化快、不想为每个模型重写适配层。我试过用传统 Function Calling 的方式接五个工具光是 schema 对齐就花了一下午换模型还得再调一遍。换成 MCP 之后工具定义在 Server 侧统一维护客户端只负责连接和注册切换模型时业务代码几乎不动。这个体验差异是推动我写这篇实战的主要原因。接下来我会带你从零跑通一条完整链路用 Docker 拉起一个 MCP Server用 LangChain4j 写客户端注册工具然后执行一次真实的工具调用验证整条链路是否打通。中间会给出可复制的配置、依赖和代码以及我踩过的几个坑。2. TaoToken 统一 Key 通道的前置准备在动手写代码之前有一个前置问题必须先解决模型从哪来。LangChain4j 本身不提供模型它需要你接入一个 ChatModel 实现比如 OpenAiChatModel、AnthropicChatModel 等。这些实现都需要一个 API Key 和一个 Base URL。如果你同时用多个模型厂商Key 管理、额度监控、Base URL 切换会变成一件很烦的事。我的做法是用 TaoToken 作为统一的 Key/API 通道。它的定位是把多家模型的调用收敛到一个入口你只需要维护一个 Key就能在 LangChain4j 里切换不同的模型。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接用这个。具体到 LangChain4j 的配置你需要关注三个值Base URL、API Key、Model ID。这三个值在后面的 OpenAiChatModel.builder() 里会用到。Base URL 填 TaoToken 的 API 地址API Key 从控制台生成Model ID 填你要用的模型名。这样你的 Java 代码里就不需要硬编码某一家厂商的地址换模型只改 Model ID 一个字段。这里有个细节要注意LangChain4j 的 OpenAiChatModel 默认走 OpenAI 的接口格式而 TaoToken 兼容这个格式所以你可以直接用 OpenAiChatModel 来对接不需要额外写适配器。这也是我选它的原因之一——对 LangChain4j 来说它就是一个标准的 OpenAI 兼容端点零改造。如果你还没有 Key可以去控制台生成一个。生成之后建议先别急着写 Java 代码用 curl 验证一下通道是否通curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: gpt-4o-mini, messages: [{role: user, content: ping}] }如果返回里有 choices 字段说明通道没问题。这一步很重要因为后面 Java 代码报错时你需要能区分是「通道问题」还是「代码问题」。我踩过的坑就是一开始没验证通道结果 Java 里报 401排查了半天才发现是 Key 复制时多了个空格。另外提醒一句Key 不要写死在代码里用环境变量注入。后面 Docker 启动 MCP Server 时也会用到环境变量传递养成习惯能省很多事。TaoToken 的 Key 和 GitHub Token 这类敏感信息统一走环境变量代码里只读 System.getenv()。前置准备做完你手上应该有三样东西一个可用的 TaoToken API Key、一个确认能通的 Base URL、一个你想用的 Model ID。有了这三样就可以进入下一步开始配置 MCP Server 和 LangChain4j 客户端了。3. 可复制的 MCP Server 与 LangChain4j 配置这一节是整篇的核心我会给出可以直接复制的配置片段和代码。分三块Maven 依赖、MCP Server 的 Docker 启动配置、LangChain4j 客户端的工具注册代码。你按顺序抄下来就能跑。先说 Maven 依赖。LangChain4j 的 MCP 支持在单独的模块里不要只引核心包。我用的版本是 0.35.0你可以按需升级但注意 MCP 模块的 API 在小版本间有过变动建议先锁定这个版本跑通再升。dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-mcp/artifactId version0.35.0/version /dependency /dependencies接下来是 MCP Server。我用的是官方 servers 仓库里的 filesystem server因为它不需要额外的 Token适合验证链路。用 Docker 启动命令如下docker run -i --rm \ -v /tmp/mcp-data:/data \ mcp/filesystem /data这个命令的意思是以交互模式启动 filesystem server把宿主机的 /tmp/mcp-data 挂载到容器的 /dataServer 会把这个目录下的文件暴露成可调用的工具。注意 -i 不能省因为 stdio 传输依赖标准输入输出。--rm 是跑完自动清理调试阶段建议加上。如果你要接 GitHub把镜像换成 mcp/github并通过环境变量传 Tokendocker run -i --rm \ -e GITHUB_PERSONAL_ACCESS_TOKEN$GITHUB_TOKEN \ mcp/github现在到了 LangChain4j 客户端部分。这里要写全三件套Base URL、Key、Model ID。我用一个配置类来集中管理避免散落在各处。public class McpConfig { public static final String BASE_URL https://taotoken.net/api; public static final String API_KEY System.getenv(TAOTOKEN_API_KEY); public static final String MODEL_ID gpt-4o-mini; }然后是核心的客户端代码。注意 StdioMcpTransport 的 command 里docker 的路径要写绝对路径Windows 下是 docker.exe。logEvents(true) 建议打开调试时能看到协议层的收发报文。ChatModel model OpenAiChatModel.builder() .baseUrl(McpConfig.BASE_URL) .apiKey(McpConfig.API_KEY) .modelName(McpConfig.MODEL_ID) .build(); McpTransport transport new StdioMcpTransport.Builder() .command(List.of( /usr/local/bin/docker, run, -i, --rm, -v, /tmp/mcp-data:/data, mcp/filesystem, /data)) .logEvents(true) .build(); McpClient mcpClient new DefaultMcpClient.Builder() .transport(transport) .build(); McpToolProvider toolProvider McpToolProvider.builder() .mcpClients(mcpClient) .build(); Bot bot AiServices.builder(Bot.class) .chatModel(model) .toolProvider(toolProvider) .build();这里的 Bot 是一个接口你只需要声明方法签名LangChain4j 会在运行时生成实现interface Bot { String chat(String userMessage); }如果你用 Spring Boot可以把这些 Bean 拆到 Configuration 里。但要注意 McpClient 是重量级对象建议用 Bean 单例不要每次请求都新建。另外 mcpClient.close() 要放在应用关闭钩子里否则 Docker 子进程可能残留。配置写完之后检查三个点Base URL 是不是 https://taotoken.net/apiKey 是不是从环境变量读的Model ID 是不是你确认可用的。这三点对了链路就成功了一半。4. 验证请求与成功结果配置写完最激动人心的时刻就是跑一次真实调用。我准备了一个最小验证用例让模型列出 /tmp/mcp-data 目录下的文件然后读取其中一个文件的内容。这个用例能同时验证「工具发现」和「工具调用」两个环节。先在宿主机准备测试数据mkdir -p /tmp/mcp-data echo hello from mcp /tmp/mcp-data/demo.txt echo second file /tmp/mcp-data/note.md然后写主类执行调用public class McpFilesystemExample { public static void main(String[] args) throws Exception { ChatModel model OpenAiChatModel.builder() .baseUrl(McpConfig.BASE_URL) .apiKey(McpConfig.API_KEY) .modelName(McpConfig.MODEL_ID) .build(); McpTransport transport new StdioMcpTransport.Builder() .command(List.of( /usr/local/bin/docker, run, -i, --rm, -v, /tmp/mcp-data:/data, mcp/filesystem, /data)) .logEvents(true) .build(); McpClient mcpClient new DefaultMcpClient.Builder() .transport(transport) .build(); McpToolProvider toolProvider McpToolProvider.builder() .mcpClients(mcpClient) .build(); Bot bot AiServices.builder(Bot.class) .chatModel(model) .toolProvider(toolProvider) .build(); String answer bot.chat( 列出 /data 目录下的所有文件然后读取 demo.txt 的内容); System.out.println(answer); mcpClient.close(); } interface Bot { String chat(String userMessage); } }运行之后如果链路打通你会看到类似这样的输出/data 目录下有 2 个文件demo.txt 和 note.md。 demo.txt 的内容是hello from mcp同时在控制台能看到 logEvents 打印的协议报文包括 tools/list 的响应和 tools/call 的请求。这一步很关键它证明了三件事MCP Server 正常启动并暴露了工具LangChain4j 成功发现了这些工具并转成了 ToolSpecification模型正确选择了工具并传入了参数。如果模型没有调用工具而是直接回答「我无法访问文件系统」那说明工具注册没生效。这时候先看 logEvents 的输出里有没有 tools/list 的响应。如果没有说明 McpClient 没连上 Server如果有但模型没调用可能是 Model ID 不支持 Function Calling换一个支持工具调用的模型再试。成功跑通之后你可以试着把问题改复杂一点比如「读取 note.md 的内容然后统计 demo.txt 有多少个字符」。这会触发多次工具调用能验证 LangChain4j 的多轮工具编排能力。实测下来filesystem server 暴露的工具包括 read_file、write_file、list_directory 等模型会根据问题自动选择合适的工具。5. 本篇常见错误排查链路跑通之前大概率会遇到几个报错。我把踩过的坑按报错信息整理出来你对照着排查。第一个高频错误是 401 Unauthorized。这个通常不是 MCP 的问题而是模型通道的问题。检查三处TaoToken 的 Key 是否正确、Base URL 是不是 https://taotoken.net/api、Key 有没有多余空格。我遇到过一次是环境变量没导出System.getenv 返回 null结果请求头里 Authorization 是空的。用 echo $TAOTOKEN_API_KEY 确认一下。第二个错误是 local proxy failed 或 connection refused。这个多半是 Docker 没启动或者 docker 路径写错了。StdioMcpTransport 的 command 里第一个参数是 docker 可执行文件的绝对路径Linux/Mac 下是 /usr/local/bin/dockerWindows 下要写 docker.exe 的完整路径。如果你用 Podman把 docker 换成 podman 即可。另外确认 Docker Desktop 正在运行否则子进程启动会失败。第三个错误是 reading choices 相关的解析异常。这个说明请求发出去了但返回的 JSON 结构不符合预期。常见原因是 Model ID 填错了或者该模型不支持 OpenAI 的 chat completions 格式。换一个确认可用的 Model ID 再试。还有一种可能是 Base URL 末尾多了斜杠导致拼接出的路径变成 //v1/chat/completions有些网关会拒绝。第四个错误是 OAuth 或 403。这个一般出现在接 GitHub MCP Server 时Token 权限不足或过期。GitHub 的 Personal Access Token 需要 repo 权限且不能是只读的。检查 Token 是否过期以及环境变量 GITHUB_PERSONAL_ACCESS_TOKEN 是否正确传递给了容器。注意 docker run 的 -e 参数要写在镜像名之前。第五个错误是工具没被调用模型直接回答。这个不是报错但结果不对。排查顺序先看 logEvents 里有没有 tools/list 响应确认工具被发现再看模型是否支持 Function Callinggpt-4o-mini 是支持的但有些小模型不支持最后看 Prompt 是否明确有时候问题太模糊模型会选择不调用工具。还有一个隐蔽的坑Docker 容器里的路径和宿主机路径不一致。filesystem server 的参数是容器内的路径 /data而挂载是 -v /tmp/mcp-data:/data。如果你在 Prompt 里写宿主机的 /tmp/mcp-data模型会找不到。统一用容器内路径。最后提醒一点McpClient 用完要 close()否则 Docker 子进程会残留。如果你在 Spring Boot 里用记得加 PreDestroy 或者在 ApplicationRunner 里注册关闭钩子。残留的容器多了会占端口和内存。6. 从验证到落地下一步怎么走链路打通只是起点。真正落地到生产还有几件事要做。我把自己的经验按优先级列一下。第一件是工具白名单。MCP Server 暴露的工具可能很多但你不一定想让模型调用全部。比如 filesystem server 有 write_file生产环境你可能只想暴露读接口。McpToolProvider 支持 filterToolNames可以只放行指定工具McpToolProvider toolProvider McpToolProvider.builder() .mcpClients(mcpClient) .filterToolNames(read_file, list_directory) .build();第二件是多 Server 管理。实际项目里你往往要同时接多个 MCP Server比如一个管文件、一个管数据库、一个管 GitHub。McpToolProvider 支持传入多个 client也支持运行期动态增删。同名工具冲突时可以用 filter 指定优先级。第三件是资源暴露。MCP 除了工具还有 Resource 概念。你可以把数据库表、配置文件映射成资源让模型通过 list_resources 和 get_resource 访问。LangChain4j 提供了 resourcesAsTools 的转换器把资源也变成工具模型用起来更自然。第四件是日志和可观测性。logEvents(true) 只适合调试生产环境要接自己的日志系统。你可以实现 McpLogMessageHandler把协议层的日志打到 ELK 或 Loki方便排查线上问题。如果你打算长期做 Agent 类应用建议关注 TaoToken 的 Coding Plan它在多模型切换和额度管理上比单 Key 更省心。接入文档在 https://taotoken.net/doc API Keys 管理在 https://taotoken.net/api-keys 模型对话调试在 https://taotoken.net/chat 。这几个入口配合使用能覆盖从调试到上线的完整流程。最后说一个我自己的体会MCP 最大的价值不是「让模型调用工具」这件事本身而是把工具的定义和调用标准化了。以前你为每个模型写一套适配现在工具定义在 Server 侧维护一次所有支持 MCP 的客户端都能用。对 Java 团队来说LangChain4j 把这套协议封装得足够薄你几乎感觉不到协议的存在写起来就像在调本地方法。这种「零侵入」的体验是我愿意把它推荐给后端同事的原因。