传统 Python 库建立在一个默认前提上:实现代码、依赖、模型和数据可以被复制到调用者机器,然后在本地进程中执行。这个前提正在被越来越多的 workload 打破。大模型可能常驻某台 GPU;企业数据不能离开内网;相机、机器人和浏览器只存在于具体设备;算法实现可能受许可证或商业机密约束;服务还可能在 NAT 后动态上线和迁移。
现有方法通常在两个极端之间选择:要么 clone 实现并承担完整运行环境,要么把函数改造成 HTTP/RPC 服务并失去 Python library 的使用体验。前者复制了不该复制的实现,后者暴露了不该暴露的网络复杂度。
我想提出一个 Network-Native Python Library 的抽象:用户安装的不是函数实现,而是一组经过发布者签名、版本化的函数契约;Python 侧获得普通的 module、function、type hints 和 docstring,而真正的执行仍然发生在函数、模型、数据或设备所在的位置。
EasyRemote 提供 Python authoring、package 和 function facade;EasyNet 提供 Realm、身份、目录、路由、签名 Invocation 与 Receipt。最终用户可以在没有 provider 源码、模型和运行环境的情况下写出:
from easyremote.silan.lotus import sem_filter
result = sem_filter(
documents,
predicate="保留讨论 memory safety 的段落",
)这不是把 RPC 伪装成函数,而是把“可安装的 Python 接口”与“可迁移的远端执行”分离为两个明确层次。
Python package manager 分发的是代码。用户执行:
pip install package之后,package 的实现、依赖和资源进入本地环境,由本地 Python interpreter 执行。
这个模型适合:
- 纯 Python 算法;
- 体积有限的依赖;
- 可复制的数据;
- 普遍可用的 CPU/GPU 环境;
- 不依赖特定设备状态的函数。
但越来越多现实 workload 并不满足这些条件。
一个 AI function 可能依赖数十 GB 模型权重和特定 CUDA 环境;一个企业分析函数必须靠近私有数据库;一个相机函数只能运行在拥有相机权限的设备;一个机器人动作必须在本地安全控制器内执行;一个长期运行的浏览器 Agent 还携带登录状态、cookie、session 和本地文件。
在这些场景中,真正有价值的资产不是一段可以复制的源码,而是:
某个主体,在某个受治理的执行环境中,持续提供的一项可调用能力。
问题因此发生变化。我们不再只问“如何发布 Python code”,而要问:
能否让一个不能复制、不能迁移、甚至不能查看实现的能力,仍然像普通 Python library 一样被安装、导入、组合和版本管理?
假设一个研究团队拥有一台 GPU 工作站。工作站上常驻一个经过优化的语义模型,并提供三个函数:
def sem_filter(
documents: list[Document],
predicate: str,
) -> list[Document]:
...
def sem_map(
documents: list[Document],
instruction: str,
) -> list[Document]:
...
def sem_join(
left: list[Document],
right: list[Document],
condition: str,
) -> list[tuple[Document, Document]]:
...团队其他成员只拥有普通笔记本。他们希望在 notebook、pipeline 或 Agent 中使用这些 operator,但不希望处理:
- CUDA 与驱动版本;
- 模型下载和冷启动;
- GPU 调度;
- 网络端口;
- NAT 和反向代理;
- API token;
- HTTP DTO;
- provider 源码;
- 服务地址变更。
理想体验是:
pip install easyremote
easyremote add @silan/lotus@^1.4 \
--realm easynet.run之后直接使用:
from easyremote.silan.lotus import sem_filter, sem_map
filtered = sem_filter(
documents,
predicate="讨论 memory safety",
)
for item in sem_map.stream(
filtered,
instruction="逐条提取核心结论",
):
print(item)调用者没有 clone provider 仓库,也没有获得模型权重。import 读取的是本地生成的 Python facade;只有调用函数时才发起网络 Invocation。
类似需求还存在于其他 workload:
- 医院只暴露
cohort_summary(),而不允许原始患者记录离开院内设备; - 相机设备只暴露有限帧流,而不允许调用者获得相机句柄或设备 shell;
- 企业只暴露
invoice_normalize(),而不公开数据库凭据; - 机器人只暴露受边界约束的
move(),而不允许任意控制指令; - 浏览器节点暴露
search_orders(),而登录 session 始终留在拥有者机器。
这些 workload 的共同结构是:
实现和资源不能迁移
+
调用接口需要被复用
+
执行必须具备身份、权限、终态和审计最直接的方法是让消费者 clone provider 项目:
git clone ...
pip install -r requirements.txt
python run.py这个方案的问题不是安装麻烦,而是它错误地假设实现可以迁移。
对于 GPU workload,消费者可能没有 GPU;对于企业 workload,数据库和凭据不能复制;对于设备 workload,相机、机器人和登录 session 根本不存在于消费者机器;对于商业算法,源码和模型也不应被分发。
Clone 同时把接口依赖变成了实现依赖。调用者只是想使用 sem_filter(),却被迫接管模型、环境、资源和部署生命周期。
另一种方法是把每个函数改造成 HTTP endpoint:
requests.post(
"https://gpu-node.example/api/filter",
headers={"Authorization": "..."},
json={
"documents": documents,
"predicate": "...",
},
)HTTP 解决了远程通信,但没有形成 Python library abstraction。每个调用者仍然要理解:
- endpoint;
- authentication;
- timeout;
- request/response schema;
- error code;
- retry;
- streaming protocol;
- API version;
- service discovery。
函数数量增加后,系统会积累大量手写 client、DTO 和 endpoint convention。开发者感知到的不再是 sem_filter(),而是某个服务的 transport contract。
gRPC、Ray、Pyro 一类系统可以降低远程调用成本,但通常仍缺少开放世界包管理所需要的几个维度:
- 谁发布了这个函数?
- 它属于哪个信任域?
- 同名函数应该选择哪个 owner?
- 调用者安装的是哪个接口版本?
- 路由变化后契约是否仍然一致?
- 团队如何复现完全相同的依赖集合?
- 远端执行是否产生可验证终态?
RPC 解决“怎么调用”,但不自动解决“调用什么、相信谁、安装哪个版本”。
一个看似简洁的方案是实现动态 import hook:
from network.someone.lotus import sem_filter当 Python import module 时,系统实时搜索网络并构造函数。
这个方案会让 import 变得不可预测:
- 网络不可用会导致 import 失败;
- 同一份代码在不同时间获得不同 API;
- IDE 和类型检查器不知道 module 内容;
- 新出现的同名函数可能改变解析结果;
- provider 可以通过网络元数据影响本地 import;
- 一个正常的 Python import 可能隐含远程信任决策。
因此,网络可以动态变化,但 Python import surface 必须来自一个本地、版本化、可复现的快照。
如果调用者只写:
client.call("sem_filter", ...)当网络中存在多个 sem_filter 时,系统只能随机选择、按先后顺序选择,或者依赖隐藏策略。
这在 demo 中看起来方便,在开放网络中却是危险的。来自不同发布者、不同 Realm、不同版本的同名函数,可能具有完全不同的权限、成本和语义。
发现可以模糊,安装和执行必须精确。
Network-Native Python Library 不是给 RPC 包一层 decorator 就能成立。它至少需要满足以下要求。
消费者必须获得普通 Python module 和 function:
from easyremote.silan.lotus import sem_filter并支持:
inspect.signature();- IDE completion;
- mypy/pyright;
- 默认参数;
- docstring;
- sync、async 和 stream;
- 本地参数绑定与基础校验。
import 不能访问网络。网络包必须先被解析和物化为本地 .py/.pyi facade。
网络故障可以让函数调用失败,但不能让已经安装的 module 突然消失。
安装网络包不能下载或执行 provider 源码。它只能安装:
- 签名;
- schema;
- documentation;
- descriptor reference;
- type stubs;
- deterministic proxy code。
系统不应支持发布者提供的任意 install script。
一个 package 的身份必须包含:
Realm + Publisher + Package + Version例如:
easynet.run :: @silan/lotus @ 1.4.2Realm 表示身份、信任和路由域;Publisher 表示谁对 package 负责;Package 是可安装接口集合;Version 描述公开 library surface。
npm 使用 @scope/package 解决不同用户和组织之间的命名冲突,并允许 scope 与 registry 建立映射。npm Scope 提供了有价值的参照,但 Network-Native Package 分发的是接口,不是实现。
项目必须拥有 lockfile,固定:
- Realm identity;
- Publisher URA;
- package version;
- manifest hash;
- exported Ability URA;
- descriptor reference;
- schema hash;
- call mode。
团队成员在不同机器执行 restore 后,应获得相同 Python symbols 和相同函数契约。
函数契约和执行地址必须分离。
descriptor_ref 固定调用者所依赖的接口;Directory 动态解析当前 execution route。Provider 可以更换机器、重启进程或从 GPU-A 迁移到 GPU-B,而不改变消费者 import。
反过来,如果函数签名发生不兼容变化,系统不能因为“仍然找得到一个同名函数”就继续调用。
搜索可以跨 Realm,但安装不能偷偷跨 Realm。
调用者必须明确声明:
easyremote add @silan/lotus@^1.4 \
--realm easynet.run相同的 @silan/lotus 在另一个 Realm 中是不同 package。Publisher handle 最终必须解析成 canonical principal identity,而不是长期依赖可变字符串。
一次远程调用不能退化为:
function name + args它至少需要确定:
- caller;
- callee;
- descriptor;
- subject;
- nonce;
- causal context;
- arguments。
调用必须有唯一终态:成功、失败或取消。Stream 必须有有界缓冲和单一 terminal closure。重试和 replay 必须能够被识别,执行结果必须能够通过 Receipt 审计。
Network-native function 可以拥有本地函数语法,但不能隐瞒网络事实。
它必须暴露可理解的异常:
- unavailable;
- deadline exceeded;
- permission denied;
- schema mismatch;
- resource exhausted;
- cancelled。
网络延迟不会因为使用了 Python 语法就消失。大型对象也不能被假装成普通内存引用。
小型 JSON 数据可以直接传递;大型 DataFrame、视频和数据集应使用 Resource reference 或 stream:
dataset = resource(
"easynet:///r/health.example/resource/hospital/datasets/cohort"
)
summary = cohort_summary(dataset, query="...")能力应靠近数据和设备执行,而不是默认把所有内容复制到调用者机器。
我提出把系统划分为两个互相独立但可组合的平面。
Capability Plane 管理真正的执行事实:
- 谁是 caller;
- 谁拥有能力;
- Ability 的 canonical identity;
- Descriptor;
- 当前执行位置;
- admission;
- routing;
- invocation lifecycle;
- Receipt。
EasyNet 负责这一层。Realm、Directory、Runtime 和 Hub 共同回答:
这个经过签名的调用,在当前网络状态下,是否允许执行,以及应该在哪里执行?
Package Plane 管理开发者依赖:
- package name;
- publisher scope;
- semantic version;
- exports;
- dependencies;
- local alias;
- lockfile;
- Python facade generation。
EasyRemote 负责这一层。它回答:
一个 Python 项目依赖哪些远端能力,以及这些能力应该以什么稳定的 Python API 出现?
Package Plane 不执行函数,也不自行决定网络路由。它只把经过治理的 AbilityDescriptor 编译成 Python library surface。
一个 Network Package 本质上是发布者签名的 interface manifest:
{
"realm": "easynet.run",
"publisher": "@silan",
"publisher_ura": "easynet:///r/easynet.run/user/silan",
"name": "lotus",
"version": "1.4.2",
"exports": {
"sem_filter": {
"ability_ura": "easynet:///r/easynet.run/ability/...",
"descriptor_ref": "...",
"schema_hash": "sha256:...",
"call_mode": "rpc"
},
"sem_map": {
"ability_ura": "easynet:///r/easynet.run/ability/...",
"descriptor_ref": "...",
"schema_hash": "sha256:...",
"call_mode": "stream"
}
}
}Manifest 不是实现 bundle。它只是声明:
这个发布者
在这个 Realm
发布了这个版本的 library
其中这些 Python symbols
对应这些受治理的 Ability contractsManifest 可以作为发布者拥有的网络 Resource 存在,不需要引入新的协议 identity kind。
Provider 编写普通 Python 函数:
from easyremote import ComputeNode
node = ComputeNode(namespace="lotus")
@node.register
def sem_filter(
documents: list[dict],
predicate: str,
) -> list[dict]:
...
@node.register
def sem_map(
documents: list[dict],
instruction: str,
):
yield from ...
node.serve()EasyRemote 从函数签名生成 schema,并将实现注册到本地 Runtime。实现、模型、GPU 和数据仍然留在 provider 机器。
随后发布 interface package:
easyremote publish @silan/lotus@1.4.2 \
--realm easynet.run发布操作读取已经存在的 AbilityDescriptor,生成并签名 Library Manifest;它不上传源码。
Consumer 执行:
easyremote add @silan/lotus@^1.4 \
--realm easynet.runPackage resolver:
- 解析 Realm;
- 将
@silan解析成 canonical publisher identity; - 获取满足版本范围的 manifest;
- 验证签名和 manifest integrity;
- 获取每个 AbilityDescriptor;
- 验证 schema 与 descriptor reference;
- 写入 lockfile;
- 生成本地 Python module;
- 原子切换到新 snapshot。
如果任意步骤失败,旧版本继续可用,不留下半安装状态。
安装完成后:
from easyremote.silan.lotus import sem_filterimport 只加载本地生成的 facade,不访问网络。
如果用户希望采用短名称:
easyremote add @silan/lotus@^1.4 \
--realm easynet.run \
--as lotus则可以:
from easyremote.lotus import sem_filter这里的 lotus 是项目本地 alias,不是全球 package identity。
调用:
result = sem_filter(documents, predicate="...")执行路径为:
Python call
↓
local typed RemoteFunction
↓
descriptor-bound Invocation
↓
local EasyNet Runtime
↓
Directory route resolution
↓
remote execution Runtime
↓
provider-resident Python implementation
↓
result / stream / terminal Receipt调用者看见普通 Python function;系统内部仍然保留完整身份、权限、路由和生命周期。
Network Package 中至少存在三个不同版本概念。
层次 | 含义 |
|---|---|
Package version | 整个 Python library 的公开 surface |
Descriptor version | 单个 Ability 的调用契约 |
Implementation binding | 当前实际执行实现与位置 |
它们不能混为一谈。
新增一个兼容函数通常只需要 package minor version;删除函数或改变参数需要 major version;修复远端算法但保持输入输出契约不变,可以只更新 implementation binding;从 GPU-A 迁移到 GPU-B 不需要消费者升级 package。
这意味着 provider 可以持续演进部署,而 consumer 依赖的是稳定接口。
RPC 的中心抽象是:
调用远端方法Network-Native Python Library 的中心抽象是:
安装一个由确定主体负责、
在确定 Realm 中解析、
具有确定版本和函数契约、
但执行位置可以动态变化的 Python dependency它把传统 library system 的:
- import;
- typing;
- version;
- dependency;
- lockfile;
- namespace;
与 capability network 的:
- identity;
- trust;
- routing;
- admission;
- execution;
- Receipt;
组合成一个闭环。
用户感知到的是“像 Python library 一样使用”,但这个结果不是语法糖本身,而是 package resolution、contract binding、runtime routing 和 execution accountability 同时成立。
这个系统不应该只用 hello world 验证。至少需要三类 workload。
验证:
- consumer 没有模型和 provider repo;
- IDE 能正确补全函数签名;
- provider 更换 GPU host 后业务代码不变;
- descriptor 不兼容变更能够在调用前失败;
- stream 保持增量传输。
验证:
- 原始数据不离开 owner environment;
- consumer 只获得受限聚合结果;
- subject 和 caller 权限被严格验证;
- Receipt 可以说明谁对什么数据执行了什么能力。
验证:
- consumer 不获得设备 shell 或原始驱动;
- binary stream 有界;
- cancellation 产生单一终态;
- 设备断线、恢复和路由更新不会改变 package interface。
对照基线应包括:
- clone 并本地部署;
- REST/OpenAPI client;
- generic RPC;
- 动态 service discovery。
评价指标不只包括 latency,还包括:
- consumer 需要管理多少实现依赖;
- 是否复制模型、数据或凭据;
- interface mismatch 在何时被发现;
- 同名 package 是否可能错误解析;
- route 迁移是否要求修改业务代码;
- failure 是否产生确定终态;
- 同一 lockfile 是否生成相同 API;
- stream 和 queue 是否保持有界。
这个抽象不能消除网络本身。
远端函数仍然可能因为网络、权限、资源和 provider 生命周期失败;大型 Python object 不能无成本跨进程传递;依赖本地 object identity、闭包、文件句柄和线程状态的函数也不能直接成为网络函数。
JSON Schema 到 Python type 的映射存在表达能力边界。复杂自定义类型需要明确的 portable schema,而不能依赖下载 provider class definition。
多 provider 选择也不能靠随机同名匹配解决。若一个 package 希望由多个实现共同提供,负载选择必须成为经过治理的 route policy,而不是 Python facade 的隐藏行为。
最后,package availability 与 execution availability 是不同状态。已经安装的 package 可以被 import,但如果没有任何可执行 provider,调用必须明确返回 unavailable,而不能悄悄降级到另一个不兼容函数。
传统 Python library 的价值,是让开发者不必理解一段实现就能复用它。但传统 package manager 通过复制实现来实现复用。
Network-Native Python Library 保留同样的开发体验,却改变了复用的物理方式:
传统 Python Package:
分发实现 → 本地执行
Network-Native Python Package:
分发接口 → 远端受治理执行所以我现在对 EasyRemote 的定位是:它不是一个更简单的 RPC decorator,也不是把互联网伪装成本地函数的魔法。
它是一个 Python capability package system:开发者安装的是稳定、类型化、版本化的接口;函数实现、模型、数据和设备继续留在拥有者那里;Realm 决定信任和解析边界;Runtime 决定执行位置;Invocation 与 Receipt 决定一次调用如何被授权、完成和审计。
真正值得讨论的不是 from easyremote... import ... 这一行是否足够优雅,而是这一行背后能否同时成立四件事:
不复制实现,仍然可以复用; 不固定地址,仍然可以寻址; 不暴露网络细节,仍然保持失败真实; 不牺牲 Python 体验,仍然拥有身份、权限、版本和审计。
如果这四件事能够在具体 workload 中被可复现地证明,那么“网络本身成为 Python library 的执行底座”才不只是语法想象,而是一个成立的系统抽象。

