前言

在传统的大模型(LLM)应用开发中,我们经常会为模型配置工具(Tools)。然而,传统的 Tool 往往面临着严重的工程痛点:它们通常硬编码在某一个具体的项目中,无法跨项目复用;同时,如果工具是用 Node.js 编写的,当其他使用 Java、Python 或 Rust 的项目想要调用该工具时,往往需要重新实现一遍业务逻辑。这种工具与模型、工具与开发语言之间的强耦合,极大地限制了 AI Agent 的工程化落地。

为了打破这种工程壁垒,Model Context Protocol(模型上下文协议,简称 MCP) 应运而生。MCP 是一种标准化大模型与工具、数据资源之间通信的开放协议。它将工具和数据源独立于大模型之外,实现了本地或远程的跨进程、跨语言调用。

本文将从零开始,深入探讨 MCP 协议的核心思想,并结合实际代码,完整展示如何从零构建一个 MCP Server,以及如何通过 LangChain 驱动 Agent 实现全自动的工具调用流,帮助读者真正理解其背后的设计原理与落地实践。


环境准备与依赖解析

在正式进入架构与代码讲解之前,我们需要准备好开发环境。本项目基于 Node.js 环境运行,运行以下命令可以一次性安装本项目所需的所有核心依赖:

pnpm i dotenv @langchain/core @langchain/openai @langchain/mcp-adapters zod chalk @modelcontextprotocol/sdk

为了让读者清晰了解每一项依赖在整个系统中所承担的角色,我们将其功能梳理如下:

  • @modelcontextprotocol/sdk:MCP 官方提供的软件开发工具包(SDK),用于构建 MCP 服务端(Server)或客户端(Client),封装了协议底层的序列化与通信细节。
  • @langchain/mcp-adapters:LangChain 官方提供的 MCP 适配器,其核心作用是作为桥梁,将 MCP Server 提供的工具和资源无缝转换为 LangChain 能够直接识别和驱动的标准格式。
  • @langchain/core:LangChain 核心组件库,提供了诸如 HumanMessage(用户消息)、SystemMessage(系统提示词)、ToolMessage(工具执行结果消息)等标准消息流对象。
  • @langchain/openai:LangChain 的 OpenAI 模型集成库(由于 DeepSeek 兼容 OpenAI 的 API 格式,我们将通过此库来调用 DeepSeek 模型)。
  • zod:一个基于 TypeScript/JavaScript 的 Schema 声明与类型验证库,在 MCP 中专门用于严格定义工具的输入参数格式。
  • dotenv:环境变量加载工具,用于从本地 .env 文件中安全地加载 API 密钥等敏感配置。
  • chalk:终端文本样式美化工具,用于在控制台输出不同颜色的日志,帮助我们清晰捕获 Agent 的思考与调用链路。

MCP 核心架构演进:Host、Client 与 Server

要真正理解 MCP,首先必须理清 Host(宿主)Client(客户端)Server(服务端) 三者之间的协作关系。这不仅是一次技术的升级,幕后更是整个大模型应用架构的范式转变。

1. 传统 Tool 调用 vs MCP 协议调用

在传统的 AI 应用中,Host(如 Web 应用)、Client(大模型交互逻辑)和 Tool(工具函数)往往揉在同一个进程或同一个语言生态中。

而 MCP 协议的核心思想是解耦。它不是单纯的接口调用,其核心目的是为大模型动态扩展上下文(Context)。通过标准化的协议,模型能够随时知道自己拥有哪些工具(Tools)可以使用,以及拥有哪些数据(Resources)可以查阅。

2. 三者的核心职能划分

我们可以通过以下架构关系图来理解三者之间的通信与职责划分:

+-------------------------------------------------------------+
|                      Host (宿主环境)                        |
|   (例如:Cursor, 或者是我们自定义的 LangChain 主程序进程)       |
|                                                              |
|   +-----------------------------------------------------+   |
|   |                  MCP Client (客户端)                 |   |
|   +-----------------------------------------------------+   |
+-------------------------------------------------------------+
                               |
                               | (通过标准输入输出 Stdio 或 HTTP 通信)
                               v
+-------------------------------------------------------------+
|                     MCP Server (服务端)                     |
|   (独立运行的进程,可由 Node.js、Python、Go 等任意语言编写)       |
|                                                              |
|   +-------------------+             +-------------------+   |
|   |   Tools (工具)     |             | Resources (资源)   |   |
|   +-------------------+             +-------------------+   |
+-------------------------------------------------------------+
  • Host(宿主):整个 AI 应用的物理边界与生命周期管理者。它可以是像 Cursor、Claude Desktop 这样的现成 IDE 或桌面客户端,也可以是我们通过代码编写的独立 AI Agent 主程序。Host 负责初始化 Client,并为最终用户提供交互界面。
  • Client(客户端):通常内嵌在 Host 中。它的主要职责是建立与 MCP Server 的连接通道,向 Server 发送查询请求,获取 Server 注册的所有工具列表(Tools) and 静态资源(Resources),并将模型的决策转化为具体的调用指令。
  • Server(服务端):一个完全独立运行的进程。它可以部署在本地,也可以部署在远程服务器上。Server 内部封装了具体的业务逻辑、数据库连接或第三方 API。Server 通过 MCP 协议向 Client 声明:“我这里有一个可以查询用户信息的工具,它需要传入用户 ID,返回字符串”。

3. 通信机制:跨进程与跨语言

MCP 最强大的特性在于其跨进程(IPC)跨语言的调用能力。Server 与 Client 之间的通信层主要有两种实现方式:

  • Stdio(标准输入输出流):当 Server 部署在本地时,Client(主进程)会通过子进程技术(如 Node.js 的 child_process)启动 Server 脚本。两个进程之间不通过网络端口通信,而是直接通过 Stdio(标准输入,控制台输出) 管道交换 JSON-RPC 数据。这种方式天然具备极高的安全性和零网络延迟,且完全消成了单线程 JavaScript 的阻塞风险——因为工具的业务逻辑运行在独立的子进程中。
  • HTTP/SSE(远程通信):当 Server 部署在远程服务器时,Client 与 Server 之间通过标准 HTTP 协议及服务器发送事件(SSE)进行双向异步通信,从而实现分布式工具集群的联动。

有了这种标准协议,无论 Server 是用 Node.js、Java、Python 还是 Rust 编写,只要它遵循 MCP 协议的 JSON-RPC 规范向标准输出写入数据,任何语言编写的 Client 都能对其进行完美调用。


MCP 服务端(Server)的从零构建

在理解了 MCP 的核心架构后,我们正式进入代码实现阶段。首先,我们需要构建一个独立的 MCP Server。这个服务端的职责非常明确:对外声明它所拥有的资产(工具与资源),并通过标准输入输出流(Stdio)等待客户端的连接与指令。

我们来看服务端核心文件 my-mcp-server.mjs 的完整实现。

1. 服务端完整源码:my-mcp-server.mjs

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from 'zod';

// 模拟数据库(数据层)
const database = {
    users: {
        '001': { id: '001', name: 'moss', email: 'moss@qq.com', role: 'admin' },
        '002': { id: '002', name: 'jane', email: 'jane@qq.com', role: 'user' },
        '003': { id: '003', name: 'jim', email: 'jim@qq.com', role: 'user' },
    }
}

// 初始化 MCP 服务端实例
const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0',
});

// 1. 注册工具 (Tool)
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: `用户${userId}不存在。可用的ID:001,002,003` }
            ]
        }
    }
    return {
        content: [
            { type: 'text', text: `用户${userId}的信息是:姓名=${user.name},邮箱=${user.email},角色=${user.role}` }
        ]
    };
});

// 2. 注册静态资源 (Resource)
server.registerResource(
    '使用指南',
    'docs://guide',
    {
        description: 'MCP Server 使用指南',
        mimeType: 'text/plain'
    },
    async () => {
        return {
            contents: [
                {
                    uri: 'docs//guide',
                    mimeType: 'text/plain',
                    text: `
                    MCP Server 使用指南
                    功能:提供用户查询等工具。
                    使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
                    `
                }
            ]
        }
    }
);

// 3. 启动服务器并建立跨进程通信管道
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error('MCP server is running...');
}

main().catch(error => {
    console.error('Server error:', error);
    process.exit(1);
});

2. 服务端源码逐模块深度剖析

为了彻底理解这段代码的精妙之处,我们将其拆解为四个核心模块进行多维度分析:

  • 模块一:基础依赖与模拟数据层
    代码开头引入了核心类与传输层。随后定义了一个模拟数据库的闭包常量。这里采用底层内存对象进行 KV(Key-Value)映射,是为了确保工具在被跨进程调用时,能够以时间复杂度快速检索目标数据,避免引入外部数据库依赖带来的复杂性。

  • 模块二:MCP Server 实例初始化

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

初始化时传入的名称和版本并不是可有可无的注释,它们会随着 MCP 握手协议(Handshake)传递给客户端。大模型和客户端宿主正是通过这些元数据来识别当前连接的是哪一个生态服务,从而在多服务并存的环境中进行精准的路由与日志审计。

  • 模块三:核心工具定义——query_user
server.registerTool('query_user', { ... }, async ({ userId }) => { ... })

这是整个 MCP 生态中最常用的功能模块。大模型之所以能学会使用这个工具,全靠参数中提供的高质量上下文描述:

1. 工具描述

这是给大模型(LLM)看的“说明书”。大模型在阅读这段纯文本后,会理解该工具的核心能力是“查询用户信息”。

2. 输入参数规范

利用工具库进行强类型约束。代码显式声明了参数必须是字符串,且通过描述为大模型提供了参数提取的样本指引。当大模型决定调用此工具时,它会严格按照这个规范组装 JSON 数据。

3. 异步回调函数

接收解构后的用户ID。值得注意的是,无论成功与否,返回的数据结构必须严格遵循 MCP 规范,即包裹在内容数组中,并声明内容类型。这种高度标准化的返回格式,使得任何遵循 MCP 协议的客户端都无需定制开发,直接就能解析输出结果。

  • 模块四:静态资源注册——registerResource
    除了动态的工具外,MCP 还创新性地提出了**静态资源(Resource)**的概念。
server.registerResource('使用指南', 'docs://guide', { ... }, async () => { ... })

在传统开发中,如果想让大模型了解一段特定的业务背景文档或系统说明,我们通常必须使用 RAG(检索增强生成)技术,先进行向量切片、再建立索引、最后在运行时进行语义检索。
而 MCP 协议的静态资源提供了一种全新的高时效性机制:它允许服务端直接将本地的文档、配置、日志等以自定义协议的形式暴露出来。
客户端可以在连接建立之初,一次性读取这些静态资源,并将其作为消息直接注入到大模型的上下文窗口中。这对于体积较小、但全局极其重要的系统上下文来说,是一种比 RAG 更直接、更低延迟且更不容易产生幻觉的丰富上下文手段。

  • 模块五:Stdio 跨进程通道的建立
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server is running...');

代码实例化了一个标准输入输出流的传输层通道。当客户端启动该服务端时,服务端会接管当前的进程管道。
细心的开发者会发现,代码中打印启动日志时使用的是错误输出。这是非常关键的核心细节! 因为标准输出通道已经被 MCP 协议的 JSON-RPC 数据流完全占用。如果此时向标准输出误打印了一句普通的业务日志,会导致通信协议数据被污染,从而引发客户端解析失败。因此,所有的系统日志、调试信息必须通过标准错误流抛出,客户端会单独捕获并将其展示在调试控制台中。


MCP 客户端(Client)与 LangChain Agent 的全自动化交互链路

构建好服务端后,我们需要一个宿主环境来充当“大脑”与“手脚”。接下来,我们将引入 LangChain 框架。

在这里,LangChain 的核心本质并不是一个简单的接口调用工具,而是一个状态管理器与自动化调度中心。它负责连接 DeepSeek 大模型,读取 Server 端的资产,并在运行时根据大模型的推理决策,全自动地进行工具调用与上下文组装。

首先来看看客户端测试文件 langchain-mcp-test.mjs 的完整源码,随后对其进行底层的剖析与分块拆解。

1. 客户端完整源码:langchain-mcp-test.mjs

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';

// 初始化模型实例:配置兼容 OpenAI 格式的 DeepSeek 接口
const model = new ChatOpenAI({
  modelName: 'deepseek-v4-pro',
  apiKey: process.env.DEEPSEEK_API_KEY,
  temperature: 0, 
  configuration: {
    baseURL: 'https://api.deepseek.com/v1',
  },
});

// 配置并建立与 MCP Server 的本地连接(跨进程管道)
const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: 'node',
      args: ['/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs']
    }
  }
});

// 从 MCP 客户端连接中异步提取标准工具集
const tools = await mcpClient.getTools();

// 动态读取并解析服务端提供的静态资源 (Resource)
const res = await mcpClient.listResources();
let resourceContent = '';

// 利用 Object.entries 遍历多服务资源,将其扁平化拼装
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;
  }
}
console.log(chalk.cyan('--- 成功读取并整合 MCP 静态资源 ---'));
console.log(resourceContent, '\n---------------------------------\n');

// 将标准工具声明绑定至大模型实例,构建具备工具感知能力的模型
const modelWithTools = model.bindTools(tools);

// Agent 自动化执行核心逻辑(ReAct 思考-执行循环)
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 === 0) {
      console.log(chalk.green(`\n AI 最终回复: \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);
        
        // 构建 ToolMessage 追加入历史,必须精准回传 tool_call_id
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id
        }));
      }
    }
  }
  
  // 超出最大轮数限制时兜底返回
  return messages[messages.length - 1].content;
}

// 触发联调测试
await runAgentWithTools('MCP Server的使用指南是什么?');

// 优雅退出:彻底关闭子进程管道,释放系统资源
await mcpClient.close();

在这里插入图片描述


2. 必须掌握的 LangChain 核心基础知识点与 API

为了让零基础的读者彻底看懂这段代码,我们必须先将代码里看似简单的 LangChain API 逐一剥离出来,对其底层的设计哲学进行全面科普:

  • 消息对象模型(Messages)的底层设计
    在 LangChain 中,对话历史绝对不是一个简单的“字符串数组”,而是一个强类型对象数组。大模型接收的所有上下文都必须通过以下三种消息对象来表达:

    • 系统消息:系统级别的高级指示。用于在对话开始前为大模型确立身份、注入全局静态知识。大模型在推理时会给予其最高级别的权重。
    • 用户消息:人类用户的显式提问或指令。
    • 工具消息:专门用于承载工具运行结果的消息。它有一个极其关键的属性:工具调用ID。

      大模型本身是**无状态(Stateless)**的。当一个复杂的提问导致大模型单次吐出多个并行的工具调用请求时,本地主进程会并发去执行这些工具。当工具结果执行完毕后,主进程向大模型回传工具消息时,大模型必须依赖消息内带有的唯一ID,去精确对应它之前发出的哪一个请求。如果缺少这个 ID,大模型将无法闭环逻辑,导致上下文错乱。

  • 模型实例化参数的隐藏玄机

const model = new ChatOpenAI({ modelName: 'deepseek-v4-pro', temperature: 0, ... });

在做 Agent 工具链调度开发时,我们通常会将大模型的温度系数(创造力)强制设为 0。这意味着我们不需要大模型发挥任何文学想象力,而是要求其展现出极致的确定性。唯有这样,它才能在面对复杂的工具规范时,极为严谨、精准地提取用户输入的参数,避免因格式跑飞导致工具调用失败。

  • 工具绑定的运行本质
const modelWithTools = model.bindTools(tools);

这句代码或者是 API 并不是把工具的执行逻辑直接注入到了大模型体内。大模型永远不可能直接运行你的本地代码。绑定工具做的事情,是将客户端提取出来的结构化工具声明,在底层翻译成目标模型的标准格式。在后续执行调用时,LangChain 会自动将这些“工具说明书”打包进请求体发给模型。大模型在阅读说明书后,如果判定需要用工具,就会在响应中回传工具调用结构,呼叫客户端主进程替它去执行代码


3. 客户端源码分块深度剖析

在理清了上述底层 API 的机制后,我们现在把客户端代码拆解为三大核心业务板块,从数据变化、执行流程和设计思想上进行微观层面的细致讲解。

  • 板块一:多服务管理器与跨进程生命周期(连接层)
const mcpClient = new MultiServerMCPClient({
 mcpServers: {
   'my-mcp-server': {
     command: 'node',
     args: ['/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs']
   }
 }
});

当主程序执行到这里时,客户端连接器会立刻在操作系统底层通过子进程技术派生出一个独立的子进程,去执行配置中指定的脚本。
主进程与子进程之间建立起了一个全双工的标准输入输出(Stdio)管道。主进程(Client)通过向子进程(Server)的标准输入写入标准的格式数据来下发指令,子进程则通过标准输出回传结果。
由于 JavaScript 是单线程的,如果把复杂的工具计算逻辑和数据库查询强行塞在主线程里,一旦遇到密集计算就会阻塞整个 Agent 的思考。而通过 MCP 协议将其隔离在独立的子进程中,主线程只需通过异步无阻塞的方式等待管道回传即可,极大地提升了系统的并发稳定度。代码末尾的关闭操作负责向所有拉起的子进程发送退出信号,并销毁通信管道,避免后台的 Server 子进程常驻内存导致泄漏。

  • 板块二:基于键值对遍历的资源扁平化(上下文丰富层)
const res = await mcpClient.listResources();
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;
  }
}

资源列表方法返回的数据是一个复杂的 Key-Value 对象。为了提取深层数据,我们使用数组转化方法,它会将该对象转化为一个键值对组成的二维数组。通过对这个二维数组进行解构赋值,我们可以非常优雅、清晰地展开两层嵌套循环。
在循环体内部,客户端通过读取资源方法跨进程向指定的 Server 请求读取特定路径的数据。获取到返回的纯文本后,通过字符串累加到一个大变量中。这就是 MCP 协议在 RAG 之外 提供的另一种强悍的上下文补充手段。它不需要复杂的向量切片和检索耗时,直接将轻量、关键、且具备全局指导意义的文档(如“使用指南”)一次性读取出来,作为大模型的“常驻记忆”,在对话初始阶段直接封死模型的幻觉空间。
核心 API 详解:Object.entries() 的妙用
对于编程基础较弱的开发者,这段代码中最核心的 JavaScript 语法糖莫过于 Object.entries()。让我们把它拆开揉碎来理解。

  1. 为什么需要这个 API?
    在传统的 JavaScript 中,如果我们使用 for…in 循环去遍历一个对象,只能拿到对象的键(Key),如果想拿到值(Value),还需要通过 obj[key] 的方式去二次查找。当面对多路 MCP 服务返回的复杂嵌套对象时,这种传统的遍历方式会让代码变得臃肿且难以阅读。
    Object.entries() 就是为了解决这个痛点而生的。它的主要作用是:把一个传统的“键值对对象”,转换成一个“键值对组成的二维数组”
  2. 数据是如何变化的?
    我们可以通过一个具体的代码例子,直观地看到 Object.entries() 转换前后的数据结构对比:
// 转换前:普通的 JavaScript 对象 (Object)
const res = {
  "filesystem": ["file1.txt", "file2.txt"],
  "chrome-devtools": ["page1.html"]
};

// 使用 Object.entries(res) 转换后:二维数组 (Array)
[
  ["filesystem", ["file1.txt", "file2.txt"]],
  ["chrome-devtools", ["page1.html"]]
]
  1. 配合“解构赋值”实现优雅遍历
    变成二维数组后,我们就可以直接使用 JavaScript 原生的 for…of 循环进行遍历。注意看代码中的这一行:
for (const [serverName, resources] of Object.entries(res)) { ... }

这里配合使用了 数组解构赋值(Destructuring Assignment) 语法。在每一次循环迭代中:

  • 数组的第一个元素(键,即 MCP 服务的名称,如 “filesystem”)会自动赋值给变量 serverName

  • 数组的第二个元素(值,即该服务拥有的资源数组,如 ["file1.txt", "file2.txt"])会自动赋值给变量 resources

  • 板块三:Agent 自动化思考循环机制(逻辑调度层)
    函数 runAgentWithTools 内部的 for (let i = 0; i < maxIterations; i++) 是整个 Agent 系统的核心灵魂:


1. 第一步:模型被激活推理

const response = await modelWithTools.invoke(messages);
messages.push(response);

主进程将当前的上下文历史(包含系统指南和原始问题)发给大模型。大模型经过推理后,发现由于自身知识受限,无法直接给出对应信息,但它在请求体中看到了我们绑定的工具声明。于是模型做出了决策:我要调用工具。

2. 第二步:模型下发呼叫指令

模型返回的 response 是一个 AIMessage 对象。它的 content 此时为空,但是它的 tool_calls 属性中被精准填入了数据:

tool_calls: [
 { "name": "query_user", "args": { "userId": "002" }, "id": "call_xyz789" }
]

主进程执行 messages.push(response),将模型的这个“呼叫动作”存入历史队列。

3. 第三步:客户端本地反射执行工具

主进程通过判定,发现 tool_calls 存在内容,向后运行。开启循环,通过 tools.find() 在本地匹配到了我们在最开始从 Server 获取的 query_user 工具对象。

4. 第四步:数据跨进程流转与反馈闭环

const toolResult = await foundTool.invoke(toolCall.args);
messages.push(new ToolMessage({ content: toolResult, tool_call_id: toolCall.id }));

主进程调用 foundTool.invoke({ userId: "002" })。在底层,LangChain 的 MCP 适配器会将这个调用请求序列化为 JSON-RPC 格式,通过 Stdio 管道塞给 Server 子进程。Server 响应后,通过管道将字符串 用户002的信息是:姓名=jane… 抛回给主进程。主进程拿到结果,打包成 ToolMessage,并打上 tool_call_id: "call_xyz789" 的烙印,追加进messages数组。

5. 第五步:重回循环,大模型得出终解

代码执行到下一轮循环(i = 1)。再次执行 modelWithTools.invoke(messages)。此时,发给 DeepSeek 的 messages 上下文里,已经形成了一条完美的线索链:用户提问 -> 模型呼叫工具 -> 工具返回真实结果。DeepSeek 这一次睁眼看到了完整的用户数据,判定不再需要任何工具协助,于是直接在 response.content 中写下答案:“用户002的姓名是jane,邮箱是…”。主进程检测到本次响应的 tool_calls 为空,触发 if 终止条件,将最终回复打印在屏幕上,完美收尾。


总结

大模型上下文协议(MCP)的出现,彻底改变了 AI 工具生态的构建范式。通过将工具、数据源与复杂的 LLM 逻辑进行进程级别通信层级的解耦,它赋予了应用开发者前所未有的工程弹性。

在本文的落地实战中,我们看到了基于 Stdio 传输层所带来的高隔离性与稳定性。服务端只专注于数据加工和工具提供,而客户端配合 LangChain 则专注于状态维护与循环调度。无论是静态资源(Resources)在丰富上下文上的高时效表现,还是动态工具(Tools)在多轮 ReAct 循环中的精准执行,都证明了 MCP 协议是未来生产级 Agent 迈向标准化、分布式演进的必经之路。掌握这一新生态,将帮助开发者在下一代智能化应用浪潮中,构建出更加健壮、跨语言复用且更具工程深度的全栈 AI 系统。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐