让 Agent 接上“万能接口”:LangChain + MCP 实战,工具不再锁死在项目里

写在前面

前面手写的 mini-Cursor,工具都是写死在本项目里的:读文件、写文件、跑命令,清一色 Node.js。换个 Python 写的工具?接不了。换个项目想复用?得把代码复制一遍。

这不怪我们写得烂------这是传统 Tool Use 的结构性问题:工具和 Agent 高度耦合,语言、仓库、进程全绑在一起

MCP(Model Context Protocol)就是来解决这个病的。它给 Agent 和工具之间插了一个通用协议,相当于 AI 界的 USB-C:管你是 Node.js、Python、Java 还是 Rust 写的工具,只要暴露 MCP Server,Agent 就能调用。

今天我们就用 LangChain 的 @langchain/mcp-adapters,把这套协议跑通。

传统 Tool Use 的两大硬伤

手写 Agent 时,工具函数通常直接 import 进来,和主项目活在一个进程里。带来的问题很现实:

痛点 表现 后果
项目耦合 工具代码和 Agent 代码在一个仓库 想复用得复制粘贴,维护灾难
语言锁定 工具用 Node.js 写,Python 工具用不了 每个语言都得自己造轮子
进程内绑定 工具函数跑在主进程里 一个工具卡死,整个 Agent 陪葬
无统一描述 各家 schema 自己定 LLM 看不懂,调用成功率低

MCP 不解决"工具怎么实现",它解决"工具怎么被 Agent 发现、描述、调用"。实现是 Server 自己的事,协议是大家共同的语言。

MCP 是什么?给 Model 扩展 Context 的 Protocol

全称 Model Context Protocol,目标很直接:

标准化 LLM 与外部工具、资源之间的通信,让 Agent 能跨进程、跨语言调用能力。

它有两个传输层:

传输方式 场景 本质
stdio 本地调用 Agent 通过 spawn 启动 MCP Server 子进程,走标准输入输出通信
HTTP 远程调用 Agent 像访问普通服务一样,用 HTTP 连到远端 MCP Server

注意,MCP 不是让你" fetch 一个接口拿数据"。它是让外部进程注册成 MCP Server,然后向 Agent 暴露两样东西:

  • Tools:可被 LLM 调用的工具
  • Resources:可被 Agent 读取的上下文资源

也就是说,MCP 不是扩展了一个函数调用,而是扩展了 LLM 能看到的 Context(上下文)

核心三件套:Host / Client / Server

角色分工比想象中的简单:

角色 干啥
Host 你的 Agent 主程序 决策、调模型、维护 messages 循环
Client @langchain/mcp-adapters 里的 MultiServerMCPClient 负责和 Server 建连、拿工具、读资源
Server 外部进程(Node/Python/Java/Rust) 注册 tool/resource,等待被调用

一张图就能串起来:

java 复制代码
Agent Host (Node.js)
    ↓
MultiServerMCPClient
    ↓
stdio / HTTP
    ↓
MCP Server A (Node)    MCP Server B (Python)    MCP Server C (Remote)
    ↓                      ↓                       ↓
   Tools                 Tools/Resources          Resources

Agent 这一侧完全不用关心 Server 是用什么语言写的,只要协议对就行。

完整代码走一遍:从连接 Server 到跑 Agent Loop

代码不长,核心逻辑分四步:

js

javascript 复制代码
import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';

const model = new ChatOpenAI({
  modelName: 'deepseek-v4-pro',
  apiKey: process.env.DEEPSEEK_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: 'https://api.deepseek.com/v1',
  },
});

// 1. 启动 MCP Server 子进程并建立连接
const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: 'node',
      args: ['C:/.../my-mcp-server.mjs'],
    },
  },
});

// 2. 从 Server 动态获取 tools 和 resources
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();

// 3. 把 resources 内容读出来,塞进 SystemMessage 当上下文
let resourceContent = '';
for (const [serverName, resources] of Object.entries(res)) {
  for (const resource of resources) {
    const content = await mcpClient.readResource(serverName, resource.uri);
    resourceContent += content[0].text;
  }
}

const modelWithTools = model.bindTools(tools);

// 4. 跑一个标准的 ReAct Agent 循环
async function runAgentWithTools(query, maxIterations = 30) {
  const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query),
  ];

  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待 AI 思考,第${i}轮...`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    if (!response.tool_calls?.length) {
      console.log(`\nAI 最终回复:\n${response.content}`);
      return response.content;
    }

    console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
    console.log(chalk.bgBlue(`工具调用:${response.tool_calls.map(t => t.name).join(', ')}`));

    for (const toolCall of response.tool_calls) {
      const foundTool = tools.find(t => t.name === toolCall.name);
      if (foundTool) {
        const toolResult = await foundTool.invoke(toolCall.args);
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id,
        }));
      }
    }
  }

  return messages[messages.length - 1].content;
}

await runAgentWithTools('MCP Server 的使用指南是什么?');

// 5. 关闭所有子进程和通信通道
await mcpClient.close();

逐段拆解:

步骤 代码 作用
连接 Server new MultiServerMCPClient(...) 按配置启动子进程,建立 stdio 通信
拿工具 mcpClient.getTools() 把 Server 注册的 tool 转成 LangChain Tool 对象
读资源 listResources() + readResource() 把 resource 内容读出来,当上下文喂给 LLM
绑定工具 model.bindTools(tools) 让 LLM 知道"我能调这些外部工具"
ReAct 循环 for + invoke + ToolMessage 和手写 mini-Cursor 的结构一模一样
释放资源 mcpClient.close() 关掉子进程和 stdio 通道,否则脚本挂死不退出

Tools vs Resources:一个能动,一个能读

很多人一开始会把 Tools 和 Resources 搞混。区别很清晰:

能力 Tools Resources
方向 Agent → Server(调用) Server → Agent(读取)
LLM 怎么用 通过 tool_calls 主动调用 直接塞进 SystemMessage 当上下文
典型场景 查用户、写数据库、发邮件 Server 使用说明、私有文档、配置模板
由谁触发 LLM 决策 开发者预读

截图里那个 my-mcp-server.mjs 注册了一个 query_user 工具,还暴露了 resource。Agent 问"MCP Server 的使用指南是什么?"时,resource 里的指南被读出来塞进 SystemMessage,LLM 就能基于它回答。

这就是 MCP 设计精妙的地方:Tools 扩展了 Agent 的行动能力,Resources 扩展了 Agent 的知识边界。一个能动,一个能读,合起来才是真正的 Context 增强。

MCP 带来了什么变化?

维度 手写 Tool(无 MCP) MCP 化之后
工具位置 和 Agent 同仓库、同进程 独立进程,可本地可远程
语言限制 Agent 用什么语言,工具就得用什么 Node Agent 调 Python 工具完全可行
复用方式 复制代码 改一行配置,连上 Server
工具发现 手动 import getTools() 动态拉取
上下文注入 自己写 prompt readResource() 自动读
维护成本

5 个踩坑提醒

1. mcpClient.close() 一定要调。 MCP Server 是子进程,stdio 通道不关闭,你的 Node 脚本退出时进程还赖着。很多新手以为"任务跑完就完事了",结果终端一直挂在那儿。

2. Resource 读出来的结构是数组。 readResource() 返回的是数组,内容在 [0].text 里。直接当字符串用会拿到 [object Object],场面一度尴尬。

3. Tools 里找工具别用 find 就完事。 代码里 tools.find(t => t.name === toolCall.name) 如果找不到,foundTool 是 undefined,直接 invoke 会崩。建议兜底返回错误字符串给 LLM。

4. 多个 Server 同名 tool 会打架。 MultiServerMCPClient 能连多个 Server,如果两个 Server 都注册了同名工具,默认名字会冲突。要么改 Server 端 tool name,要么客户端做 namespace 隔离。

5. stdio 子进程路径写死是大坑。 配置里 args: ['C:/Users/...'] 写绝对路径,换台机器直接跪。生产环境用相对路径或环境变量,别把个人电脑路径提交到仓库。

写在最后

从手写 mini-Cursor 到接入 MCP,本质没变:还是 ReAct 循环,还是 ToolMessage 闭环,还是 LLM 想、代码做。变的是工具的边界------以前工具是项目里的几个函数,现在工具可以是任意语言、任意进程、任意机器上的服务。

Agent 的战斗力,一半看 LLM 的脑子,一半看工具的覆盖范围。MCP 就是把"工具生态"这件事标准化了。以前每个 Agent 框架各玩各的,现在好了,协议一通,万物互联。

相关推荐
妙码生花1 小时前
使用git更新ai-go-admin框架
前端·人工智能·git·golang·typescript·php
YUJIANYUE1 小时前
查立得万用查分电脑版(web环境+查询系统免安装单文件一键运行包)
前端·jvm
小聪7082 小时前
基于node实现一个轻量化web引擎:elpis-core
前端
子非鱼a2 小时前
【WEB】[SWPU2019]Web1
java·服务器·前端
涛涛ing2 小时前
9 月第一周,前端圈又炸了四次
前端
葡萄城技术团队2 小时前
从自然语言到表格操作:SpreadJS 表格智能体如何执行一个任务
前端
Neighbor_OldY3 小时前
【实战复盘】文件上传漏洞检测与应急处置:校验绕过、图片马与WebShell的排查修复指南
运维·前端·web安全
金花顺3 小时前
android_media_AudioTrack_setup
前端
晓天衡宇•评测社区3 小时前
Seedance 2.5 登顶图生视频榜单,Wan 3.0 在游戏与教育场景进入前列
前端·javascript·网络