MCP协议的Streamable HTTP:革新数据传输的未来

引言

在数字化时代,数据传输的效率和稳定性是推动技术进步的关键。MCP(Model Context Protocol)作为AI生态系统中的重要一环,通过引入Streamable HTTP传输机制,为数据交互带来了革命性的变化。本文将深入解读MCP协议的Streamable HTTP,从会话协商到正式通信传输数据的全过程,探讨其技术架构、协议内容、实现方式以及对AI应用的影响。

技术架构

MCP协议采用客户端-服务器架构,其核心组件包括:

  1. MCP主机(MCP Host):包含MCP客户端的应用程序,如Claude Desktop、Cursor IDE等AI工具。
  2. MCP客户端(MCP Client):在主机内部与服务器保持1:1连接的协议实现。
  3. MCP服务器(MCP Server):轻量级程序,通过标准化协议暴露特定功能,可以是本地Node.js/Python程序或远程服务。

MCP服务器提供三类标准能力:

  • 资源(Resources):如文件读取、API数据获取。
  • 工具(Tools):第三方服务或功能函数,如Git操作、浏览器控制。
  • 提示词(Prompts):预定义的任务模板,增强模型特定场景表现。

Streamable HTTP的协议内容

Streamable HTTP作为MCP协议的一项重大更新,旨在解决传统HTTP+SSE方案的局限性,同时保留其优势。其核心内容包括以下几个方面:

  1. 统一消息入口:所有客户端到服务器的消息都通过/message端点发送,不再需要专门的SSE端点。
  2. 动态升级SSE流:服务器可以根据需要将客户端发往/message的请求升级为SSE连接,用于推送通知或请求。
  3. 会话管理机制:客户端通过请求头中的Mcp-Session-Id与服务器建立会话,服务器可选择是否维护会话状态。
  4. 支持无状态服务器:服务器可以选择完全无状态运行,不再需要维持长期连接。

实现方式

从会话协商到正式通信传输数据

1. 会话协商

会话协商是Streamable HTTP通信的初始阶段,客户端与服务器通过以下步骤建立会话:

  1. 客户端发送初始化请求:客户端通过HTTP POST向MCP服务器的/message端点发送一个InitializeRequest消息,携带协议版本和客户端能力信息。
  2. 服务器响应初始化:服务器收到请求后,返回一个InitializeResult消息,包含服务器支持的协议版本、服务器能力以及会话ID(Mcp-Session-Id)。
  3. 客户端发送已初始化通知:客户端收到服务器的响应后,发送一个Initialized通知,告知服务器初始化已完成。

示例:客户端发送初始化请求

POST /message HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: text/event-stream, application/json{"jsonrpc": "2.0", "method": "initialize", "params": {"clientInfo": {"name": "MCP Client", "version": "1.0"}, "capabilities": {}}}

示例:服务器响应初始化

HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: 12345{"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "MCP Server", "version": "1.0"}, "capabilities": {}}}

示例:客户端发送已初始化通知

POST /message HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: text/event-stream, application/json
Mcp-Session-Id: 12345{"jsonrpc": "2.0", "method": "initialized", "params": {}}
2. 正式通信传输数据

会话建立后,客户端和服务器可以通过以下步骤进行正式通信:

  1. 客户端发送消息:客户端通过HTTP POST向MCP服务器的/message端点发送JSON-RPC消息,携带会话标识Mcp-Session-Id
  2. 服务器处理请求并响应:服务器根据请求内容处理消息,并通过SSE流或JSON对象返回响应。
  3. 动态升级为SSE流:如果需要实时推送消息,服务器可以将连接升级为SSE流。
  4. 断线重连与数据恢复:如果网络波动导致连接中断,客户端可以携带Last-Event-ID重新连接,服务器根据该ID重放未发送的消息。

示例:客户端发送消息

POST /message HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: text/event-stream, application/json
Mcp-Session-Id: 12345{"jsonrpc": "2.0", "id": 1, "method": "get_file", "params": {"path": "/example.txt"}}

示例:服务器响应并升级为SSE流

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Mcp-Session-Id: 12345data: {"jsonrpc": "2.0", "id": 1, "result": "File content here"}

示例:客户端断线重连

GET /message HTTP/1.1
Host: mcp.example.com
Accept: text/event-stream
Last-Event-ID: 12345
Mcp-Session-Id: 12345

示例:服务器重放未发送的消息

data: {"jsonrpc": "2.0", "id": 2, "result": "Continued content here"}

服务端代码实现

from datetime import datetime
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.routing import Route
from starlette.responses import JSONResponse, StreamingResponse
import json
import uuid
from starlette.middleware.cors import CORSMiddleware
import asyncio
from typing import Dict, Any
import aiofiles
import random# 存储会话ID和对应的任务队列
sessions: Dict[str, Dict[str, Any]] = {}# 添加CORS支持
app = Starlette()
app.add_middleware(CORSMiddleware,allow_origins=["*"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],expose_headers=["Mcp-Session-Id"],
)@app.route('/message', methods=["POST", "GET"])
async def handle_message(request: Request):"""处理POST和GET请求。"""session_id = request.headers.get("Mcp-Session-Id") or request.query_params.get("Mcp-Session-Id")if request.method == "POST":try:data = await request.json()if data.get("method") == "initialize":# 初始化会话session_id = str(uuid.uuid4())sessions[session_id] = {"initialized": True,"task_queue": asyncio.Queue()}response = JSONResponse(content={"jsonrpc": "2.0","id": data.get("id"),"result": {"serverInfo": {"name": "MCP Server", "version": "1.0"},"capabilities": {},},})response.headers["Mcp-Session-Id"] = session_idreturn responseelif session_id and sessions.get(session_id, {}).get("initialized"):# 处理已初始化的请求if data.get("method") == "get_file":try:# 异步读取文件内容content = await async_read_file(data.get("params", {}).get("path", ""))return JSONResponse(content={"jsonrpc": "2.0","id": data.get("id"),"result": content,})except Exception as e:return JSONResponse(content={"jsonrpc": "2.0","id": data.get("id"),"error": f"Error reading file: {str(e)}",})else:return JSONResponse(content={"error": "Unknown method"})else:return JSONResponse(content={"error": "Session not initialized"}, status_code=400)except Exception as e:return JSONResponse(content={"error": f"Internal server error: {str(e)}"}, status_code=500)elif request.method == "GET":# 处理SSE流请求if not session_id or session_id not in sessions:return JSONResponse(content={"error": "Session not found"}, status_code=404)async def event_generator(session_id):while True:try:message = await asyncio.wait_for(sessions[session_id]["task_queue"].get(), timeout=10)  # 超时时间10秒yield f"data: {json.dumps(message)}\n\n"except asyncio.TimeoutError as e:yield f"data: {e}\n\n"  # 发送空数据作为心跳包,防止超时断开return StreamingResponse(event_generator(session_id), media_type="text/event-stream")async def async_read_file(path: str) -> str:"""异步读取文件内容。"""try:async with aiofiles.open(path, "r") as file:content = await file.read()return contentexcept Exception as e:raise Exception(f"Error reading file: {str(e)}")async def background_task(session_id: str, task: Dict[str, Any]):"""后台任务处理。"""# 模拟耗时操作await asyncio.sleep(1)# 将结果放入任务队列sessions[session_id]["task_queue"].put_nowait(task)@app.on_event("startup")
async def startup_event():async def push_test_messages():while True:sp = random.randint(1, 3)await asyncio.sleep(sp)  # 每5秒推送一个消息for session_id in sessions.keys():if sessions[session_id]["initialized"]:sessions[session_id]["task_queue"].put_nowait({"message": f"Hello from server!", "sleep": sp,"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S")})asyncio.create_task(push_test_messages())  # 创建后台任务if __name__ == "__main__":import uvicornuvicorn.run(app, host="0.0.0.0", port=8000)

客户端代码实现

import httpx
import json
import asyncio
import aiofilesclass MCPClient:def __init__(self, server_url: str):self.server_url = server_urlself.session_id = Noneself.headers = {"Content-Type": "application/json","Accept": "text/event-stream, application/json"}async def initialize(self):"""初始化会话。"""async with httpx.AsyncClient() as client:try:response = await client.post(f"{self.server_url}/message",headers=self.headers,json={"jsonrpc": "2.0","method": "initialize","params": {"clientInfo": {"name": "MCP Client", "version": "1.0"},"capabilities": {},},},)response.raise_for_status()self.session_id = response.headers.get("Mcp-Session-Id")print(f"Session ID: {self.session_id}")return self.session_idexcept Exception as e:print(f"Failed to initialize session: {e}")return Noneasync def send_message(self, method: str, params: dict = None):"""发送消息。"""if not self.session_id:await self.initialize()async with httpx.AsyncClient() as client:try:response = await client.post(f"{self.server_url}/message",headers={"Mcp-Session-Id": self.session_id, **self.headers},json={"jsonrpc": "2.0","id": 1,"method": method,"params": params or {},},)response.raise_for_status()return response.json()except Exception as e:print(f"Failed to send message: {e}")return Noneasync def listen_sse(self):if not self.session_id:await self.initialize()async with httpx.AsyncClient(timeout=None) as client:  # 取消超时限制try:async with client.stream("GET",f"{self.server_url}/message",headers={"Mcp-Session-Id": self.session_id, **self.headers},) as response:async for line in response.aiter_lines():if line.strip():  # 避免空行print(f"SSE Message: {line}")except Exception as e:print(f"Failed to listen SSE: {e}")await self.reconnect()async def reconnect(self):"""断线重连。"""print("Attempting to reconnect...")await asyncio.sleep(5)  # 等待5秒后重试await self.initialize()await self.listen_sse()async def main():client = MCPClient("http://localhost:8000")await client.initialize()response = await client.send_message("get_file", {"path": "/Users/houjie/PycharmProjects/python-sdk/examples/mcp-server/example.txt"})print(f"Response: {response}")await client.listen_sse()if __name__ == "__main__":asyncio.run(main())

前端页面代码实现

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>MCP Streamable HTTP Demo</title><style>body {font-family: Arial, sans-serif;margin: 0;padding: 20px;background-color: #f5f5f5;}.container {max-width: 800px;margin: 0 auto;background-color: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}h1 {text-align: center;color: #333;}.message-area {margin-top: 20px;}.message {padding: 10px;margin-bottom: 10px;border-radius: 4px;background-color: #e9f7fe;border-left: 4px solid #0099cc;}.sse-message {padding: 10px;margin-bottom: 10px;border-radius: 4px;background-color: #f0f9ff;border-left: 4px solid #0077cc;}button {background-color: #0099cc;color: white;border: none;padding: 10px 15px;border-radius: 4px;cursor: pointer;font-size: 14px;}button:hover {background-color: #0077cc;}input[type="text"] {width: 100%;padding: 10px;margin-bottom: 10px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;}</style>
</head>
<body><div class="container"><h1>MCP Streamable HTTP Demo</h1><div><input type="text" id="serverUrl" placeholder="Enter server URL" value="http://localhost:8000"><button id="initBtn">Initialize Session</button></div><div id="sessionId"></div><div><input type="text" id="filePath" placeholder="Enter file path"><button id="sendBtn">Send Message</button></div><div class="message-area" id="messages"></div></div><script>let client = null;let sessionInitialized = false;document.getElementById('initBtn').addEventListener('click', async () => {const serverUrl = document.getElementById('serverUrl').value;client = new MCPClient(serverUrl);await client.initialize();sessionInitialized = true;document.getElementById('sessionId').textContent = `Session ID: ${client.session_id}`;});document.getElementById('sendBtn').addEventListener('click', async () => {if (!sessionInitialized) {alert('Please initialize the session first.');return;}const filePath = document.getElementById('filePath').value;const response = await client.send_message('get_file', { path: filePath });addMessage(`Response: ${JSON.stringify(response)}`);});class MCPClient {constructor(serverUrl) {this.serverUrl = serverUrl;this.session_id = null;this.headers = {'Content-Type': 'application/json','Accept': 'text/event-stream, application/json'};}async initialize() {try {const response = await fetch(`${this.serverUrl}/message`, {method: 'POST',headers: this.headers,body: JSON.stringify({jsonrpc: '2.0',method: 'initialize',params: {clientInfo: { name: 'MCP Client', version: '1.0' },capabilities: {}}})});if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}this.session_id = response.headers.get('Mcp-Session-Id');addMessage(`Session ID: ${this.session_id}`);this.listen_sse();} catch (error) {addMessage(`Failed to initialize session: ${error}`);}}async send_message(method, params) {if (!this.session_id) {await this.initialize();}try {const response = await fetch(`${this.serverUrl}/message`, {method: 'POST',headers: { 'Mcp-Session-Id': this.session_id, ...this.headers },body: JSON.stringify({jsonrpc: '2.0',id: 1,method: method,params: params || {}})});if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return await response.json();} catch (error) {addMessage(`Failed to send message: ${error}`);return null;}}listen_sse() {if (!this.session_id) {return;}const eventSource = new EventSource(`${this.serverUrl}/message?Mcp-Session-Id=${this.session_id}`, {headers: { 'Mcp-Session-Id': this.session_id }});eventSource.onmessage = (event) => {addSSEMessage(event.data);};eventSource.onerror = (error) => {addMessage(`Failed to listen SSE: ${error}`);this.reconnect();};}async reconnect() {addMessage('Attempting to reconnect...');await new Promise(resolve => setTimeout(resolve, 5000));await this.initialize();this.listen_sse();}}function addMessage(message) {const messagesDiv = document.getElementById('messages');const messageDiv = document.createElement('div');messageDiv.className = 'message';messageDiv.textContent = message;messagesDiv.appendChild(messageDiv);messagesDiv.scrollTop = messagesDiv.scrollHeight;}function addSSEMessage(message) {const messagesDiv = document.getElementById('messages');const messageDiv = document.createElement('div');messageDiv.className = 'sse-message';messageDiv.textContent = `SSE Message: ${message}`;messagesDiv.appendChild(messageDiv);messagesDiv.scrollTop = messagesDiv.scrollHeight;}</script>
</body>
</html>

运行步骤

  1. 安装依赖
    确保安装了所需的库:

    pip install starlette uvicorn httpx aiofiles
    
  2. 启动服务器
    将服务端代码保存为 server.py,然后运行以下命令启动服务器:

    uvicorn server:app --reload
    
  3. 运行客户端
    将客户端代码保存为 client.py,然后运行以下命令启动客户端:

    python client.py
    
  4. 打开前端页面
    将前端页面代码保存为 index.html,然后在浏览器中打开该文件。

示例运行效果

客户端输出
Session ID: 587bb6ad-08f5-4102-8b27-4c276e9d7815
Response: {'jsonrpc': '2.0', 'id': 1, 'result': 'File content here'}
Listening for SSE messages...
SSE Message: data: {"message": "Hello from server!", "sleep": 1, "datetime": "2024-01-01 12:00:00"}
SSE Message: data: {"message": "Hello from server!", "sleep": 2, "datetime": "2024-01-01 12:00:02"}
...
服务器输出
INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:51487 - "POST /message HTTP/1.1" 200 OK
前端页面效果

前端页面将显示会话ID、发送的消息以及接收到的SSE流消息。

调试

  1. 检查服务器日志:查看服务器日志,确认是否生成了 Mcp-Session-Id 并返回给客户端。
  2. 检查网络请求:使用浏览器开发者工具(F12),查看网络请求的响应头,确认是否包含 Mcp-Session-Id
  3. 检查跨域问题:确保服务器正确配置了 CORS,允许前端页面的域名和端口。

希望这些信息能够帮助你成功实现基于MCP协议的Streamable HTTP服务端、客户端和前端页面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/76546.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL - 索引原理与优化:深入解析B+Tree与高效查询策略

文章目录 引言一、BTree索引核心原理1.1 索引数据结构演化1.2 BTree的存储结构通过主键查询&#xff08;主键索引&#xff09;商品数据的过程通过非主键&#xff08;辅助索引&#xff09;查询商品数据的过程 MySQL InnoDB 的索引原理 二、执行计划深度解析三、索引失效的六大陷…

《K230 从熟悉到...》识别机器码(AprilTag)

《K230 从熟悉到...》识别机器码&#xff08;aprirltag&#xff09; tag id 《庐山派 K230 从熟悉到...》 识别机器码&#xff08;AprilTag&#xff09; AprilTag是一种基于二维码的视觉标记系统&#xff0c;最早是由麻省理工学院&#xff08;MIT&#xff09;在2008年开发的。A…

Linux驱动复习

应用层调用内核层函数称为系统调用 1.硬件设备管理 1&#xff0c;字符设备驱动&#xff08;一个一个字节&#xff09;——芯片内部外设 &#xff1a;WDT,Timer&#xff0c;adc,iic&#xff0c;SPI,R,UART,LCD,CAMERA,USB,Keyboard&#xff0c;Mouse 2&#xff0c;块设备驱动&a…

【FAQ】HarmonyOS SDK 闭源开放能力 —Account Kit(3)

1.问题描述&#xff1a; PC场景&#xff0c;青少年模式系统API不支持吗&#xff1f; 解决方案&#xff1a; PC场景&#xff0c;青少年模式系统API不支持&#xff0c;另外文档上的几个API也不支持。 2.问题描述&#xff1a; 华为一键登录 Beta7本地运行到手机可以拿到匿名手…

【gdutthesis模板】论文标题太长导致换页问题解决

标题太长导致换页问题解决方案如下&#xff1a; 调小下方数值即可

SAP学习笔记 - 豆知识18 - (TODO)Msg 番号 ME154 構成品目无法決定

1&#xff0c;现象 構成品目の決定は不可能です Msg 番号 ME154 構成品目无法決定 2&#xff0c;到Deepseek里找一下解决方案 SAP ME21N中错误「组件物料的确定不可行&#xff08;ME154&#xff09;」的解决步骤 此错误在创建分包采购订单时出现&#xff0c;通常由于系统无…

10.多线程

预备知识 预备知识一 预备知识二 预备知识三 如何理解进程和线程的关系&#xff0c;举一个生活中的例子 家庭&#xff1a;进程家庭成员&#xff1a;线程 每个家庭成员都会为这个家庭做贡献&#xff0c;只不过大家都在做不同的事情&#xff08;比如&#xff1a;我们在上学&…

Python入门(8):文件

1. 文件基本概念 文件&#xff1a;存储在计算机上的数据集合&#xff0c;Python 通过文件对象来操作文件。 文件类型&#xff1a; 文本文件&#xff1a;由字符组成&#xff0c;如 .txt, .py 二进制文件&#xff1a;由字节组成&#xff0c;如 .jpg, .mp3 2. 文件打开与关闭…

市场交易策略优化与波动管理

市场交易策略优化与波动管理 在市场交易中&#xff0c;策略的优化和波动的管理至关重要。市场价格的变化受多种因素影响&#xff0c;交易者需要根据市场环境动态调整策略&#xff0c;以提高交易的稳定性&#xff0c;并有效规避市场风险。 一、市场交易策略的优化方法 趋势交易策…

HTTP数据传输的几个关键字Header

本文着重针对http在传输数据时的几种封装方式进行描述。 1. Content-Type(描述body内容类型以及字符编码) HTTP的Content-Type用于定义数据传输的媒体类型&#xff08;MIME类型&#xff09;&#xff0c;主要分为以下几类&#xff1a; (一)、‌基础文本类型‌ text/plain‌ …

面向教育领域的实时更新RAG系统:核心模块设计与技术选型实践指南

目录 面向教育领域的实时更新RAG系统&#xff1a;核心模块设计与技术选型实践指南 一、业务需求分析 二、系统架构设计&#xff08;核心模块&#xff09; 三、核心模块详解与技术选型建议 &#xff08;一&#xff09;实时更新向量知识库 &#xff08;二&#xff09;教材与…

k8s patch方法更新deployment和replace方法更新deployment的区别是什么

在Kubernetes中&#xff0c;patch 和 replace 方法用于更新资源&#xff08;如 Deployment&#xff09;&#xff0c;但它们的实现方式和适用场景有显著差异。以下是两者的核心区别&#xff1a; 1. 更新范围 replace 方法 完全替换整个资源配置。需要用户提供完整的资源定义&…

解决安卓手机WebView无法直接预览PDF的问题(使用PDF.js方案)

在移动端开发中&#xff0c;通过 webview 组件直接加载PDF文件时&#xff0c;不同平台的表现差异较大&#xff1a; iOS & 部分安卓浏览器&#xff1a;可正常内嵌预览&#xff08;依赖系统内置PDF渲染能力&#xff09; 大多数安卓设备&#xff1a;由于缺乏原生PDF插件&…

基于javaweb的SSM+Maven机房管理系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

7-6 混合类型数据格式化输入

本题要求编写程序&#xff0c;顺序读入浮点数1、整数、字符、浮点数2&#xff0c;再按照字符、整数、浮点数1、浮点数2的顺序输出。 输入格式&#xff1a; 输入在一行中顺序给出浮点数1、整数、字符、浮点数2&#xff0c;其间以1个空格分隔。 输出格式&#xff1a; 在一行中…

【GPIO8个函数解释】

函数解释 void GPIO_DeInit(GPIO_TypeDef* GPIOx); 作用&#xff1a;将指定GPIO端口的所有寄存器恢复为默认值。这会清除之前对该端口的所有配置&#xff0c;使其回到初始状态。使用方法&#xff1a;传入要复位的GPIO端口指针&#xff0c;例如GPIOA、GPIOB等。 void GPIO_AF…

将图表和表格导出为PDF的功能

<template><div><divref"pdfContent"style"position: relative; width: 800px; margin: 0 auto"><!-- ECharts 图表 --><div id"chart" style"width: 100%; height: 400px" /><!-- Element UI 表格 …

C++中的链表操作

在C中&#xff0c;链表是一种常见的数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点包含数据部分和指向下一个节点的指针。C标准库&#xff08;STL&#xff09;中提供了std::list和std::forward_list两种链表实现&#xff0c;分别对应双向链表和单向链表。此外&am…

蛋白设计 ProteinMPNN

传统方法的局限性是什么&#xff1f; 传统蛋白质设计方法的局限性&#xff1a; 基于物理的传统方法&#xff0c;例如罗塞塔&#xff0c;面临计算难度&#xff0c;因为需要计算所有可能结构的能量&#xff0c;包括不需要的寡聚态和聚合态。 设计目标与显式优化之间缺乏一致性通…

有哪些开源的视频生成模型

1. 阿里巴巴通义万相2.1&#xff08;WanX 2.1&#xff09; 技术架构&#xff1a;基于Diffusion Transformer&#xff08;DiT&#xff09;架构&#xff0c;结合自研的高效变分自编码器&#xff08;VAE&#xff09;和Flow Matching训练方案&#xff0c;支持时空上下文建模。参数…