跨进程调用工具:MCP + LangChain 让 Agent 不再“锁死”在单一语言

跨进程调用工具:MCP + LangChain 让 Agent 不再"锁死"在单一语言

摘要:本地编写的 Tool 只能在当前项目中使用,换一个项目就要重写。MCP 协议让 Tool 可以独立于 LLM,实现跨进程、跨语言调用。本文用 LangChain 的 MCP 适配器连接 MCP Server,获取 Tools 和 Resources,构建一个能跨进程调用工具的 Agent。

📑 目录

  • 本地 Tool 的复用困境
  • MCP 协议:让工具解耦的"桥梁"
  • 环境准备与依赖
  • MCP Server 端:注册 Tool 和 Resource
  • MCP Client 端:MultiServerMCPClient 连接 Server
  • 获取 Resources:将资源注入 SystemMessage
  • Agent 循环调用跨进程 Tool
  • 进程管理:为什么需要 close?
  • 一点总结
  • 互动讨论

本地 Tool 的复用困境

在之前开发 Agent 时,我们直接在项目里用 LangChain 的 tool() 函数定义 Tool:

javascript

php 复制代码
const readFileTool = tool(
    async ({ FilePath }) => { ... },
    { name: 'read_file', description: '...', schema: ... }
);

这种做法有两个明显的问题:

  1. Tool 只能在当前项目中使用,换一个项目就要复制粘贴,几乎没有复用性。
  2. 语言绑定:用 Node.js 写的 Tool,如果其他团队用 Python/Java/Rust,就需要重写。

理想的方案是:Tool 应该独立于 LLM,可以本地/远程、跨进程、跨语言调用

这就是 MCP(Model Context Protocol)要解决的问题。

MCP 协议:让工具解耦的"桥梁"

MCP 是 LLM 与工具、资源之间的通信协议。它的核心价值在于标准化

  • 标准化 LLM 与 Tool 和 Resource 之间的通信 → 解耦
  • 基于 stdio(标准输入输出流)进行本地进程通信,当一个进程(Agent)调用子进程时,通过 stdio 实现通信
  • 基于 HTTP 进行远程进程通信

不管是本地工具还是远程工具,Agent 想跨进程调用某个工具,通过 MCP 协议就可以完成。MCP 出现的背景是:Tool 已经编写了很多,但只能在不同项目中使用不同语言不断复写。

MCP 是给 Model 扩展 Context 上下文,让它做得更多(Tool),知道得更多(Resource)的 Protocol。

环境准备与依赖

项目使用 LangChain 的 MCP 适配器:

bash

bash 复制代码
pnpm i @langchain/mcp-adapters @modelcontextprotocol/sdk zod dotenv
  • @langchain/mcp-adapters:LangChain 官方提供的 MCP 适配器,让 LangChain Agent 可以连接 MCP Server
  • @modelcontextprotocol/sdk:MCP 协议的标准 SDK,用于编写 Server 端
  • zod:参数校验

MCP Server 端:注册 Tool 和 Resource

my-mcp-server.mjs 中,我创建了一个 MCP Server,注册了一个 Tool 和一个 Resource。

创建 Server 实例

javascript

javascript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0'
});

注册 Tool:query-user

javascript

css 复制代码
// 模拟数据库,实际项目可以换成真实的数据库连接
const database = {
    users:{
        '001':{ id:'001', name:'许', email:'x@qq.com', role:'admin' },
        '002':{ id:'002', name:'锋', email:'f@qq.com', role:'user' },
        '003':{ id:'003', name:'聂', email:'n@qq.com', role:'user' }
    },
};

server.registerTool(
    'query-user',
    {
        description: '查询数据库中的用户信息。输入用户ID,返回该用户的详细信息(姓名、邮箱、角色)',
        inputSchema: {
            userId: z.string().describe('用户ID,例如:001、002、003')
        }
    },
    async ({ userId }) => {
        const user = database.users[userId];
        if (!user) {
            return {
                content: [
                    { type: 'text', text: `用户ID ${userId}不存在。可用的ID:001,002,003` }
                ]
            };
        }
        return {
            content: [
                { 
                    type: 'text', 
                    text: `用户 ${user.id} 的信息是:姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`
                }
            ]
        };
    }
);

registerTool 接收三个参数:

  • 工具名称(LLM 通过名称识别和调用)
  • 配置对象:description(告诉 LLM 这个工具能做什么)和 inputSchema(用 Zod 定义参数结构)
  • 处理函数:异步执行查询逻辑,返回结果

在实际业务中,database 可以换成真实的数据库、API 调用或其他数据源。这里的处理逻辑是:如果查询到用户,返回用户信息;如果没查到,返回友好的错误提示,告诉调用方可用的 ID 有哪些。

注册 Resource:使用指南

javascript

php 复制代码
server.registerResource(
    '使用指南',                    // 资源名称
    'docs://guide',               // URI,MCP 协议中资源的唯一标识
    {
        description: 'MCP Server 使用指南',
        mimeType: 'text/plain'
    },
    async () => {
        return {
            contents: [
                {
                    uri: 'docs//guide',
                    mimeType: 'text/plain',
                    text: `
                    MCP Server 使用指南
                    功能:提供用户查询等工具。
                    使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
                    `
                }
            ]
        };
    }
);

Resource 和 Tool 的关键区别:

  • Tool:可执行的函数,有副作用(查询、写入、操作外部世界)
  • Resource:只读的数据/文档内容,作为上下文提供给 LLM,让 LLM "知道"更多信息

注册 Resource 需要指定一个 URI(docs://guide),Client 通过这个 URI 来读取资源内容。mimeType 告诉 Client 返回内容的格式,text/plain 表示纯文本。

启动 Server(stdio 通信)

javascript

ini 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

StdioServerTransport 使用标准输入输出进行通信,适合本地进程间通信。启动后 Server 会进入"伺服状态",等待 Client 通过 stdin/stdout 发送请求。

MCP Client 端:MultiServerMCPClient 连接 Server

langchain-mcp-test.mjs 中,我用 LangChain 的 MultiServerMCPClient 连接 MCP Server。

创建 MCP Client

javascript

javascript 复制代码
import { MultiServerMCPClient } from '@langchain/mcp-adapters';

const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'my-mcp-server': {
            command: 'node',
            args: ['D:/workspace/yjs_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs']
        }
    }
});

MultiServerMCPClient 是 LangChain 提供的适配器,可以同时连接多个 MCP Server。每个 Server 通过 command + args 启动一个子进程------相当于 Client 进程会 fork 一个子进程来运行 Server。

获取 Tools 和 Resources

javascript

ini 复制代码
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();

getTools() 返回一个 Tool 数组,可以直接用于 model.bindTools(tools)。这些 Tool 背后是通过 MCP 协议跨进程调用的------调用时 Client 通过 stdio 向 Server 发送请求,Server 执行后返回结果。

listResources() 返回一个对象,键是 Server 名称,值是 Resource 列表,结构如下:

javascript

css 复制代码
{
    'my-mcp-server': [
        { uri: 'docs://guide', name: '使用指南', description: '...' }
    ]
}

获取 Resources:将资源注入 SystemMessage

Resource 作为只读数据,最好的用途是作为 SystemMessage 的一部分,成为 LLM 的上下文。

javascript

ini 复制代码
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;
    }
}

// 将资源内容作为 SystemMessage
const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query)
];

Object.entries() 把对象的 key 和 value 组成一个二维数组,key 在前面,value 在后面:

javascript

css 复制代码
const obj = { 'bytedance': ['AI全栈开发','Agent工程师'], 'tecent': ['后端开发'] };
for (let [key, value] of Object.entries(obj)) {
    console.log(key, value);
}
// 输出:bytedance ['AI全栈开发','Agent工程师']
//       tecent ['后端开发']

这种遍历方式比 for...in 更清晰,因为 for...in 会遍历原型链上的属性,而 Object.entries() 只处理对象自身的可枚举属性。

Resource 和 RAG 的定位不同。RAG 适合大规模知识检索(向量数据库),Resource 适合小规模静态文档,因为 LLM 的上下文窗口有限,Resource 需要控制大小。

补充内容

for...in vs for...of 的区别

这里有两种方式遍历对象:

javascript

javascript 复制代码
const obj = {
    'bytedance': ['AI全栈开发','Agent工程师'],
    'tecent': ['后端开发','Agent 工程师'],
    '163': ['前端开发']
};

// 方式一:for...in
for (let key in obj) {
    console.log(key, obj[key]);
}

// 方式二:for...of + Object.entries()
for (let [key, value] of Object.entries(obj)) {
    console.log(key, value);
}

两者输出结果相同,但底层机制完全不同:

特性 for...in for...of
遍历内容 对象的可枚举属性名(包括原型链上的) 可迭代对象的
适用对象 普通对象 数组、字符串、Map、Set、类数组等可迭代对象
配合 Object.entries() 不需要 需要,因为普通对象本身不可迭代
返回值 属性名(key) 元素值(value)
是否遍历原型链 ✅ 会(需要用 hasOwnProperty 过滤) ❌ 不会,只遍历自身元素

简单理解:

  • for...in 是给对象用的,遍历的是 key
  • for...of 是给数组/可迭代对象用的,遍历的是 value
  • Object.entries(obj) 将对象转换成 [[key, value], ...] 的二维数组,让它变成可迭代的

这就是为什么在遍历对象时,for...of + Object.entries() 更推荐------它只处理对象自身的属性,不会意外遍历到原型链上的属性。for...in 虽然写法简单,但如果不做过滤,可能拿到意想不到的属性。

Agent 循环调用跨进程 Tool

获取 Tools 后,绑定到模型,进入 Agent 循环:

javascript

ini 复制代码
const modelWithTools = model.bindTools(tools);

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 || !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;
}

tools.find() 匹配成功时不会再往后遍历,返回第一个匹配项。这是 find 方法的特点,适合查找唯一工具。如果找不到对应的 Tool,foundToolundefined,这里会跳过不处理。

这里的 tools 是通过 MCP 协议跨进程获取的,调用 foundTool.invoke() 时,LangChain 的适配器会通过 MCP 协议把请求发送到 Server 进程,Server 执行后返回结果。整个过程对 Agent 来说是透明的------它只知道调用了一个 Tool,不知道这个 Tool 在另一个进程里运行。

进程管理:为什么需要 close?

运行 node langchain-mcp-test.mjs 后,程序执行完任务后不会自动退出

原因在于进程关系:

  • 主进程(Client) :运行 langchain-mcp-test.mjs
  • 子进程(Server) :由 Client 通过 command: 'node' 启动,运行 my-mcp-server.mjs

MCP Server 是一个"伺服"进程------启动后会一直等待请求,不会主动退出。主进程完成任务后,由于子进程还在运行,主进程不会自动退出(需要等待子进程结束)。

解决方案是调用 mcpClient.close()

javascript

csharp 复制代码
await mcpClient.close();

close() 会关闭所有子进程与通信通道,释放进程资源。如果忘记调用,脚本会一直挂着不退出,占用内存和进程资源。

一点总结

通过这个项目,我理解了:

  1. 本地 Tool 的局限性:只能在当前项目使用,不能跨语言、跨进程复用。
  2. MCP 协议:标准化了 LLM 与 Tool/Resource 的通信,实现解耦。
  3. MCP Server:可以注册 Tool 和 Resource,通过 stdio(本地)或 HTTP(远程)通信。
  4. MultiServerMCPClient:LangChain 提供的适配器,让 Agent 可以连接多个 MCP Server。
  5. Resource 作为上下文 :通过 listResources + readResource 获取文档内容,注入 SystemMessage。
  6. 进程管理 :MCP Server 是常驻进程,需要主动 close() 释放资源。
  7. Object.entries() 遍历对象 :比 for...in 更清晰,只处理自身属性。

互动讨论

  1. 本地 Tool 和 MCP Tool 的核心区别是什么? 为什么 MCP 能实现跨进程调用?
  2. Tool 和 Resource 在 MCP 中的定位有什么不同? 分别适合什么场景?
  3. Object.entries()for...in 遍历对象的区别是什么? 为什么 Object.entries() 更推荐?
  4. 为什么 MCP Server 需要 close() 才能让进程退出? 如果不关闭会有什么后果?
  5. RAG 和 MCP Resource 都可以给 LLM 提供知识,它们各自适合什么场景?

📌 一点心得:MCP 让 Tool 从"项目内部函数"变成了"可独立部署的服务单元"。这种解耦不仅提升了复用性,还让 Agent 可以跨语言、跨进程调用工具,真正实现了工具的"资产化"。

相关推荐
带刺的坐椅2 小时前
SolonCode v2026.7.13 发布:多工作区协同、推理水平可控、子代理任务分组、工程体验再进化
llm·agent·codex·solon-ai·claudecode·opencode
张彦峰ZYF2 小时前
大模型LLM ACA - ACP认证考试真题冲刺演练二答案参考
人工智能·lora·llm·embedding·rag·few-shot
掰头战士3 小时前
你本地的LLM有时会胡乱使用tools?不妨用MCP规范LLM工具调用!
node.js·llm·全栈
Token炼金师3 小时前
注意力的六脉:QKV 数学、头设计、RoPE、Flash、掩码与剪枝 —— Transformer 核心机制全解
人工智能·深度学习·llm
触底反弹3 小时前
🧠 LangChain Agent 入门:为什么直接调大模型 API 远远不够?
人工智能·node.js·llm
BraveWang4 小时前
【LangChain 1.x】04、模型调用方式|invoke、stream、batch、异步调用
langchain
喵叔哟4 小时前
第二周概述
人工智能·langchain
meilindehuzi_a5 小时前
远程 MCP 项目实战:LangChain 连接高德地图、Chrome DevTools 与文件系统
人工智能·langchain·chrome devtools