LangChain + MCP 实战:为 Agent 打造标准化的万能工具接口 做过 Agent 的人应该都有同一种感觉功能做得越多工具越多代码就越臃肿。今天接一个内部搜索引擎明天接一个订单查询服务后天再接一个数据库查询器每一个都要写一遍工具封装、参数定义、鉴权逻辑、异常处理。更崩溃的是一旦换框架或者改项目结构这些工具代码基本全部作废等于白干。我很久之前就想要是 Agent 能像电脑一样有个“USB 接口”工具插上就能用拔下来就能换那该多好。直到 MCP 出现之后这个想法才真正落地。这篇实战笔记我就围绕 LangChain MCP 这个组合来写。MCP 是 Model Context Protocol 的缩写相当于给 Agent 开了一个标准化的“万能接口”把工具从具体项目代码里解放出来。你会看到为什么传统方式工具会“锁死”、MCP 的核心设计思路、如何在 LangChain 里接入 MCP Server以及我把一个文件操作服务接到 Agent 上的完整过程。当中还穿插了我踩过的坑和排查经验希望能帮你少走弯路。1. 为什么 Agent 的工具会“锁死”1.1 传统工具接入方式的痛点在 LangChain 里把一个工具交给 Agent最常规的做法是定义一个函数然后加上tool装饰器或者在初始化Tool对象时传入name、description、func这些参数。代码如下from langchain_core.tools import tool tool def query_order(order_id: str) - str: 根据订单号查询订单状态 return f订单 {order_id} 的状态是已发货 tools [query_order]这种写法本身没有任何问题项目里接三五个工具用来跑 Demo、做二次开发都很快。但真实业务场景远没有这么简单我总结了一下至少会遇到三类问题。第一是工具和能力重复封装。比如说企业内部有一个统一的文件存储网关A 项目里写了一套FileToolB 项目里又写了一套逻辑几乎一样只是包了一层不同框架的壳。放在单体项目里还能忍放到微服务架构或者多个 Agent 项目里重复代码会指数级增长。第二是工具升级会拖累 Agent 项目。工具的逻辑一旦变化比如字段改了、鉴权方式换了你得重新改 Agent 项目代码重新部署 Agent。Agent 和工具被强耦合成了一个整体改一个工具可能要重启整个服务这对线上 Agent 来说很致命。第三是工具的“可发现性”几乎为零。传统方式中Agent 能调用的工具集合是写死在代码里的新增工具要改代码删除工具也要改代码外部系统能力无法被 Agent 动态感知。换句话说工具被“锁死”在了项目里Agent 的边界就是代码的边界。1.2 换个思路让 Agent 面向“协议”编程当你被上面三个问题反复折磨之后自然会想到一个办法能不能把工具的“定义”和“实现”分离开工具提供方负责实现能力同时暴露一份标准的、机器可读的说明名字、参数、返回格式而 Agent 项目只需要按照这份说明动态去调用即可。工具不再是以代码形式“焊”在 Agent 里而是以一个服务的形式存在随时接入、随时被调用不同 Agent 项目可以复用它。这个思路其实就是 MCP 正在做的事情。MCP 全称 Model Context Protocol它的定位是“Agent 与工具之间的标准化通信协议”。你可以把它理解为 AI 世界的 USB 接口协议鼠标、键盘、U 盘各不相同但它们都能插到同一个 USB 接口上因为它们遵循同一套协议。MCP 也一样它统一了 Agent 调用工具时的握手、认证、调用、返回等流程让任何 MCP 兼容的工具都能被任何 MCP 兼容的 Agent 框架直接使用。LangChain 作为目前生态最成熟的 Agent 开发框架之一在很早之前就做了 MCP 适配。利用这套适配层我可以在 LangChain 项目里把我本机运行的文件操作服务、远程部署的 ERP 查询服务、第三方提供的存算服务全都接进来它们各自独立演进互不干扰。工具的“锁死”问题就从根上解决了。2. MCP 的核心概念与 LangChain 接入方式2.1 MCP 的三大核心组件要上手 MCP先要把三个角色分清楚MCP Client、MCP Server 和宿主程序。MCP Client 是运行在 Agent 项目里的一个代理客户端负责和 MCP Server 建立连接、发送请求、接收结果。在 LangChain 场景里通常会通过langchain-mcp-adapters这个库把一个 MCP Client 适配成若干个 LangChain 能识别的Tool对象。MCP Server 是工具的实现方它暴露出一组可被标记为“工具”的函数并且通过 MCP 协议向客户端描述这些工具的功能和参数。一个典型的 MCP Server 可以运行在本地子进程上stdio 传输也可以部署成远程 HTTP 服务Streamable HTTP 或 SSE 传输。宿主程序就是承载 Agent 主逻辑的应用它不直接和工具打交道而是通过 MCP Client 来间接完成。三者关系大致是这样的宿主程序Agent 主流程连接 MCP ClientMCP Client 去连接 MCP ServerMCP Server 实际执行工具逻辑。拿我的文件操作服务为例Agent 主程序是 LangChain 大模型MCP Client 就是langchain-mcp-adapters创建的会话MCP Server 则是我用 Python 写的一个本地小服务负责读文件、列目录这些操作。2.2 LangChain 工具机制是怎么和 MCP 对齐的LangChain 对工具的抽象核心是BaseTool它要求任何工具都必须提供name工具名、description工具说明给模型看的、args_schema参数结构定义和_run/_arun执行逻辑。模型在推理时会通过description判断该不该用这个工具再通过args_schema生成符合规范的参数 JSON。MCP 的世界里一个工具同样带有name、description、inputSchema这些元信息以及一个可被调用的call方法。这里的inputSchema用的是 JSON Schema 格式和 LangChain 里的 Pydantic 参数模型本质上是一回事都是对参数的标准化描述。有意思的是两边对齐得非常自然。langchain-mcp-adapters拿到 MCP Server 暴露出来的工具列表之后会把每个 MCP 工具转换成BaseTool子类MCP 的name变成 LangChain 的nameMCP 的description变成 LangChain 的description连inputSchema都会被转换成 Pydantic 模型传给args_schema。对上层代码来说它完全感知不到工具来自 MCP这才是这套方案丝滑的关键。2.3 用哪几个关键依赖在我当前实践环境中核心依赖主要是这几个langchain-core提供Tool、AgentExecutor这些基础抽象。langchain-openai我用的是兼容 OpenAI Chat Completions 接口的大模型所以通过它来加载模型。如果你用别的模型就换成对应的 LangChain 集成包mcp官方 Python SDK用于构建 MCP Server 和 Client。langchain-mcp-adapters社区官方提供的适配层在第 3 章我会重点演示它的用法。安装方式很简单pip install langchain-core langchain-openai mcp langchain-mcp-adapters注意这几个库版本迭代都很快尤其是langchain-mcp-adapters和mcpAPI 在一些小版本之间就可能变化。我写这篇时使用的版本大致是langchain-core 0.3.x、mcp 1.8.x、langchain-mcp-adapters 0.1.x如果你跑的时候发现接口对不上优先去翻对应版本的官方示例。3. 完整实操写一个 MCP 文件服务并接入 LangChain3.1 场景设计和环境准备我先交代一下为什么要做文件操作这个例子。文件读写是 Agent 最常见的工具需求之一但它也最能体现“锁死”问题如果直接把read_file、list_dir写在 Agent 项目里下次别的项目想用就得复制代码而如果做成 MCP Server它就能同时服务多个项目。所以这个实战分成两半一半是 MCP Server服务端它独立于 Agent 项目存在另一半是 Agent 客户端通过 LangChain 动态加载服务端暴露的工具。两者之间用 stdio 通信也就是由 Agent 客户端启动一个 Python 子进程来运行 Server读写进程的标准输入输出完成通信。环境上我是用 Python 3.11 跑的。先把依赖装上然后新建两个文件file_server.py和agent_client.py放在同一个目录下。3.2 第一步实现 MCP ServerMCP 官方 SDK 提供了一套非常友好的高层封装我通常直接基于FastMCP来写 Server。为什么用FastMCP因为它能用装饰器自动从函数签名中提取工具描述和参数 schema省去了手工定义 JSON Schema 的功夫。file_server.py的完整代码如下import os from pathlib import Path from mcp.server.fastmcp import FastMCP # 创建一个 MCP Server 实例名称为 FileOps mcp FastMCP(FileOps) mcp.tool() def read_text_file(path: str) - str: 读取指定文本文件的内容并返回字符串。 Args: path: 文件的绝对路径或相对路径支持 ~ 符号。 path os.path.expanduser(path) with open(path, r, encodingutf-8) as f: return f.read() mcp.tool() def list_directory(path: str .) - list[str]: 列出指定目录下的所有文件和子目录名称。 Args: path: 目录的绝对路径或相对路径默认为当前目录。 path os.path.expanduser(path) return [p.name for p in Path(path).iterdir()] mcp.tool() def get_file_size(path: str) - int: 返回指定文件的大小单位字节。 Args: path: 文件的绝对路径或相对路径。 path os.path.expanduser(path) return os.path.getsize(path) if __name__ __main__: mcp.run(transportstdio)这里我设置了三个工具读取文本文件、列出目录内容、获取文件大小足够演示 Agent 自动选择工具并完成多步调用了。关于 transport 参数我专门说一下。在mcp.run里我传了transportstdio这代表 Server 通过标准输入输出和 Client 通信。好处是零网络配置、本地进程隔离最适合开发调试和本地单机场景。如果后期要部署到远程可以改成transportstreamable-http或者transportsse并把它挂到一个 HTTP 服务上代码主体不必大改这又是 MCP 的一个实用之处。需要特别注意的是Server 代码里不要用print输出任何调试信息。因为 stdio 通道就是数据通信的主通道一旦把别的字符串混进 stdout客户端解析就会失败。要打印日志请用logging模块并输出到 stderr这一点很多第一次用 MCP 的朋友都会踩到。3.3 第二步在 LangChain 中加载 MCP 工具Server 写好了接下来看 Client 侧怎么把 MCP 工具变成 LangChain 能调用的 Tool。agent_client.py的核心逻辑如下import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI async def main(): # 1. 配置子进程参数让客户端启动 python file_server.py server_params StdioServerParameters( commandpython, args[file_server.py], ) # 2. 建立与 MCP Server 的 stdio 连接与会话 async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # 3. 必须先初始化握手等 Server 就绪 await session.initialize() # 4. 通过适配器把 MCP 工具加载为 LangChain 工具 tools await load_mcp_tools(session) print(MCP 暴露的工具:, [t.name for t in tools]) # 5. 初始化支持 function calling 的模型 llm ChatOpenAI( modelgpt-4o, api_keysk-xxxx, base_urlhttps://api.openai.com/v1, temperature0, ) # 6. 构造 ReAct 风格的 Tool Calling Agent prompt ChatPromptTemplate.from_messages([ (system, 你是一个乐于助人的助手可以根据工具返回结果回答用户问题。), (human, {input}), (placeholder, {agent_scratchpad}), ]) agent create_tool_calling_agent(llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, verboseTrue) # 7. 给出一个需要连续调用多个工具的任务 result await executor.ainvoke({ input: 请查看 /tmp/demo 目录下有哪些文件然后读取其中的 notes.md 内容并告诉我这个文件有多大。 }) print(最终结果:, result[output]) if __name__ __main__: asyncio.run(main())这段代码可以把执行过程拆成七步来看。第一步是StdioServerParameters它告诉客户端怎么拉起 Server 子进程。这里command是pythonargs是[file_server.py]意思是运行python file_server.py。第二步是stdio_client(server_params)它会真正创建一个子进程并返回读写流。第三步是ClientSession和initialize这是 MCP 协议要求的握手动作类似 HTTP 的GET /探活不握手就不能后续通信。第四步是整个方案的关键load_mcp_tools(session)。它会在会话里发起tools/list请求把 MCP Server 上注册的所有工具都拉下来然后转换成langchain_core.tools.BaseTool列表。从这一行代码开始MCP 的工具就正式变成了 LangChain 的工具。第五步和第六步是常规的 LangChain Agent 构造过程初始化模型创建 Agent。第七步是我的任务设计我没有直接指定调用哪个工具而是用自然语言描述了一个“先看目录再读文件再拿大小”的复合任务。模型在推理时会自动决定先调用list_directory拿到目录结果后再调用read_text_file最后调用get_file_size。这一步最能体现 Agent 的智能性它看到的不是写死的顺序而是工具的描述然后动态编排。3.4 实测运行看 Agent 怎么自己“接线”我在/tmp/demo目录下预先放了一个notes.md文件里面的内容是 “hello mcp”。然后运行客户端脚本python agent_client.py执行完你会发现控制台输出大概会分成几段先是收到MCP 暴露的工具: [list_directory, read_text_file, get_file_size]表示 3 个工具都被成功加载接着是 Agent 的思考过程模型先调用了list_directory发现notes.md存在然后调用read_text_file读取内容最后调用get_file_size拿到字节数。这样就实现了一个最小闭环Agent 通过 MCP 协议发现了工具并动态完成了一个多步任务。整个过程里Agent 项目代码完全没有写死任何文件操作实现它只知道“有一个叫 FileOps 的服务提供了三个工具”具体工具怎么跑、逻辑怎么变是 Server 侧的事。这里我特别想强调一点传统方式里Agent 是在启动时就拿到了一个固定的工具列表而在 MCP 模式下工具列表来自一个对外部服务的动态查询结果只要 Server 侧改了工具列表Client 下次启动时拿到的工具列表就会自动变化。新增工具不需要改 Agent 代码。这才是“万能接口”真正的价值。3.5 关于 stdio 与远程模式的取舍如果你只在自己的电脑上调试stdio 是最省事的。它不需要监听端口不需要考虑跨域由 Agent 直接拉起子进程所有通信都在本机完成。但生产环境往往不会这么简单。当 MCP Server 需要被多个 Agent 实例共享或者 Server 在另一台机器上的时候我建议把 transport 切到远程模式。方向上无非是两种一种是走 Streamable HTTP把 MCP Server 包装成一个 HTTP 服务Client 通过统一入口访问另一种是走 SSE使用 Server-Sent Events 做单向推送和请求响应。官方一直在推进 Streamable HTTP 替换旧版 HTTP所以新项目我更推荐直接看 Streamable HTTP 的文档。Server 端改动并不大只要在mcp.run里换一个 transport再用 uvicorn 之类的方式启动即可Client 端也不再使用stdio_client而是换成streamablehttp_client。这种切换能力本身就说明 MCP 对工具接入做了很好的抽象工具使用者不在乎你是在本机还是远程反正都是一个标准协议。4. 避坑指南与问题排查实录4.1 我遇到过的五个典型问题这套方案看着简单真正跑起来还是会遇到不少问题。我把典型的几类整理成了表格方便你遇到的时候直接对号入座。现象可能原因解决办法运行后一直卡住没有输出Client 启动 Server 子进程失败或者 Server 侧抛了异常先手动执行python file_server.py确认服务能启动再检查StdioServerParameters的command是否指向正确的 Python 环境报错Connection closed或EOF子进程非正常退出通常是编码问题或未捕获异常把 Server 里的文件读写统一指定encodingutf-8用logging输出到 stderr 排查异常加载到的工具列表是空的session.initialize()没成功或 Server 没有注册任何mcp.tool()确认initialize在load_mcp_tools之前检查 Server 是否真的定义了工具函数模型回答“我没有可用的工具”模型本身不支持 function calling或者工具描述不清晰、没有传给 Agent换一个支持 tool calling 的模型检查AgentExecutor的tools参数是否传入了从 MCP 加载的工具中文内容乱码文件编码和读写编码不一致统一使用 UTF-8写文件时也加上encodingutf-84.2 容易被忽略的坑不要在 Server 里 print这个坑我在前文提过但因为它太典型了我再展开一次。MCP 的 stdio 模式本质上是把进程的标准输出 stdout 当作网络通道来用。如果你在mcp.tool()的函数体里写了print(debug)这个字符串会混进协议数据流轻则让 Client 解析报错重则导致整个 session 挂掉。很多人第一次跑不通过第一反应是去 Agent 侧加日志结果在 Agent 侧看到一堆乱七八糟的字符串完全不知道是哪来的。我建议一开始就在 Server 侧用logging.basicConfig(streamsys.stderr, levellogging.INFO)来输出日志这样既能排查代码又不会污染通信通道。4.3 工具描述对模型决策的影响MCP 会把mcp.tool()下面那段 docstring 原样变成工具描述而这段描述就是模型选择工具的“说明书”。我发现很多人写 docstring 很随意比如只写一句“读文件”那模型在该用read_text_file的时候可能选错或者干脆不用工具。从实操角度来说docstring 要包括这几块工具是做什么的、什么时候应该调用、每个参数的含义和单位。我上面的例子就是照着这个习惯写的所以模型才能准确地在多步任务里做选择。另外还有一个小技巧工具的数量不宜过多。如果一次加载 50 个 MCP 工具token 占用会非常大而且模型的选择准确率会下降。一般控制在 5-10 个效果最好。如果工具很多建议按业务域拆成多个 MCP Server让每个 Agent 只加载自己需要的那几个。5. LangChain 和 LangGraph怎么选才不纠结5.1 两者关系LangGraph 是 LangChain 生态里的流程编排方案5.2 什么场景用 LangChain MCP什么场景上 LangGraph每次我一聊 LangChain肯定会有人问 LangGraph 是不是要用起来甚至还有人说“LangChain 过时了直接学 LangGraph”。这类说法其实没搞清楚两者的关系。LangChain 是一个大而全的生态里面有模型封装、检索、工具抽象、记忆等能力LangGraph 则更聚焦在“图状态机”式的流程编排上。可以说LangGraph 属于 LangChain 生态但它专注的方向是复杂流程的显式控制。对应到 MCP 集成上我的选择逻辑很直接如果只是做一个标准 ReAct 或者 Tool Calling 的 Agent也就是我们前面写的那种“模型根据工具描述决定调用顺序”的玩法那用 LangChain 自带的create_tool_calling_agent就够了代码量少心智负担低。但如果任务里需要强制指定某些工具先执行、带条件分支、多轮人工审批、或者多个 Agent 之间要共享工具状态那 LangGraph 会更合适。你在 LangGraph 里定义状态图节点的执行逻辑可以复用同样的 MCP 工具列表只是流程控制权从“模型自由发挥”变成了“图和节点说了算”。我用 LangGraph 接 MCP 的经验是load_mcp_tools返回的BaseTool可以直接当普通工具传给 LangGraph 的节点两者是天然兼容的。所以回到最关心的问题要不要因为 MCP 而上 LangGraph我的答案是不用。MCP 解决的是工具接入的标准化问题LangGraph 解决的是 Agent 流程的编排复杂度问题二者不是同一个层面。MCP 本身不挑框架你完全可以在 LangChain 里先把工具接入跑通等流程复杂到 LangChain 的 Agent 不好表达时再引入 LangGraph。5.3 怎么应对“框架过时论”关于“LangChain 过时”这个说法我的看法是技术生态确实变化快但工具的抽象思路不会过时。MCP 的出现反而让 LangChain 这类框架有了新的定位——它不再只是一个“工具调用胶水层”而是变成了 MCP 生态的消费者和编排层。就算有一天你不用 LangChain转向直接调用 MCP Client 或另一个框架你在 MCP 协议层面的积累依然有效。因为 MCP 是一个协议不是某个库。协议层面的标准化比任何框架都更有生命力。6. 从“数据结构”到“协议接口”一种思维转变6.1 传统集成是点对点连接MCP 集成是标准接口连接6.2 接入 MCP 后工具边界变得清晰把这件事想通之后我发现最根本的变化不是技术上的而是思维上的。过去做 Agent 集成我们做的是点对点的连接Agent 项目直接依赖工具实现工具和 Agent 是“你和我的关系”。而用 MCP 之后Agent 项目面对的是一个个独立的能力节点它只依赖一套协议工具实现完全被协议挡住了。这种思维转变带来的实际好处我在项目上体会很深。以前新增一个工具要评审代码、回归测试、重新部署现在如果新工具符合 MCP 规范开发完直接注册到对应 ServerAgent 那边什么都不用改下次会话里就能动态发现。工具的边界一下子变得非常清晰谁提供服务谁负责实现细节一目了然。6.3 给未来接入更多 MCP Server 的建议最后基于我这段时间的实践给准备大规模接入 MCP 的朋友三个建议。第一个建议是从“一个 Server 一个能力域”开始设计不要把所有工具都堆到一个 Server 里。比如文件操作放一个 Server搜索放一个 Server数据库放一个 Server。这样每个 Server 的工具描述不会太长也便于复用和权限控制。第二个建议是仔细打磨每个工具的描述和参数说明。这个我在 4.3 里详细说过因为模型对工具的理解完全依赖这些描述写得清楚一点整个 Agent 的准确率都会提升。第三个建议是提前考虑安全和审计。MCP 让 Agent 能动态发现工具但如果 Server 暴露了不该暴露的能力相当于给 Agent 多开了权限。生产环境里我建议在 MCP Server 加一层访问控制并记录每次调用日志。协议再方便也不能省掉安全审查。在做这个项目的过程中我最大的体会是MCP 不只是一个技术工具它重构了 Agent 和外部能力之间的关系。以前我写的每个 Agent 项目都像一座孤岛工具代码只能在岛内使用换个项目就要重新造一遍现在的做法更像是建立起一套标准货运体系每个工具能力打包好贴上标准标签任何 Agent 都能按协议取用。这个转变带来的收益远不只是少写几行代码。如果你正准备让 Agent 接入更多工具我建议尽早试试 MCP尤其在 LangChain 生态里它几乎能无缝融入上手成本远比你想象的低。