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

0 阅读6分钟

写在前面

前面手写的 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,等待被调用

一张图就能串起来:

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

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();

逐段拆解:

步骤代码作用
连接 Servernew 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 搞混。区别很清晰:

能力ToolsResources
方向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
工具发现手动 importgetTools() 动态拉取
上下文注入自己写 promptreadResource() 自动读
维护成本

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 框架各玩各的,现在好了,协议一通,万物互联。