跳到主要内容
inspiration22分钟阅读

从代码分发到能力分发:一种 Network-Native Python Library 抽象

胡思蓝
从代码分发到能力分发:一种 Network-Native Python Library 抽象 cover
正文

摘要

统 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 源码、模型和运行环境的情况下写出:

javascript
from easyremote.silan.lotus import sem_filter

result = sem_filter(
    documents,
    predicate="保留讨论 memory safety 的段落",
)

这不是把 RPC 伪装成函数,而是把“可安装的 Python 接口”与“可迁移的远端执行”分离为两个明确层次。


1. 问题背景

Python package manager 分发的是代码。用户执行:

javascript
pip install package

之后,package 的实现、依赖和资源进入本地环境,由本地 Python interpreter 执行。

这个模型适合:

  • 纯 Python 算法;
  • 体积有限的依赖;
  • 可复制的数据;
  • 普遍可用的 CPU/GPU 环境;
  • 不依赖特定设备状态的函数。

但越来越多现实 workload 并不满足这些条件。

一个 AI function 可能依赖数十 GB 模型权重和特定 CUDA 环境;一个企业分析函数必须靠近私有数据库;一个相机函数只能运行在拥有相机权限的设备;一个机器人动作必须在本地安全控制器内执行;一个长期运行的浏览器 Agent 还携带登录状态、cookie、session 和本地文件。

在这些场景中,真正有价值的资产不是一段可以复制的源码,而是:

某个主体,在某个受治理的执行环境中,持续提供的一项可调用能力。

问题因此发生变化。我们不再只问“如何发布 Python code”,而要问:

能否让一个不能复制、不能迁移、甚至不能查看实现的能力,仍然像普通 Python library 一样被安装、导入、组合和版本管理?


2. Concrete Use Case:GPU 上的 Semantic Operator Library

假设一个研究团队拥有一台 GPU 工作站。工作站上常驻一个经过优化的语义模型,并提供三个函数:

javascript
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 源码;
  • 服务地址变更。

理想体验是:

javascript
pip install easyremote

easyremote add @silan/lotus@^1.4 \
  --realm easynet.run

之后直接使用:

javascript
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 的共同结构是:

javascript
实现和资源不能迁移
        +
调用接口需要被复用
        +
执行必须具备身份、权限、终态和审计

3. Existing Bad Cases

3.1 Clone Repository

最直接的方法是让消费者 clone provider 项目:

javascript
git clone ...
pip install -r requirements.txt
python run.py

这个方案的问题不是安装麻烦,而是它错误地假设实现可以迁移。

对于 GPU workload,消费者可能没有 GPU;对于企业 workload,数据库和凭据不能复制;对于设备 workload,相机、机器人和登录 session 根本不存在于消费者机器;对于商业算法,源码和模型也不应被分发。

Clone 同时把接口依赖变成了实现依赖。调用者只是想使用 sem_filter(),却被迫接管模型、环境、资源和部署生命周期。

3.2 HTTP API

另一种方法是把每个函数改造成 HTTP endpoint:

javascript
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。

3.3 Generic RPC

gRPC、Ray、Pyro 一类系统可以降低远程调用成本,但通常仍缺少开放世界包管理所需要的几个维度:

  • 谁发布了这个函数?
  • 它属于哪个信任域?
  • 同名函数应该选择哪个 owner?
  • 调用者安装的是哪个接口版本?
  • 路由变化后契约是否仍然一致?
  • 团队如何复现完全相同的依赖集合?
  • 远端执行是否产生可验证终态?

RPC 解决“怎么调用”,但不自动解决“调用什么、相信谁、安装哪个版本”。

3.4 在 Import 阶段访问网络

一个看似简洁的方案是实现动态 import hook:

javascript
from network.someone.lotus import sem_filter

当 Python import module 时,系统实时搜索网络并构造函数。

这个方案会让 import 变得不可预测:

  • 网络不可用会导致 import 失败;
  • 同一份代码在不同时间获得不同 API;
  • IDE 和类型检查器不知道 module 内容;
  • 新出现的同名函数可能改变解析结果;
  • provider 可以通过网络元数据影响本地 import;
  • 一个正常的 Python import 可能隐含远程信任决策。

因此,网络可以动态变化,但 Python import surface 必须来自一个本地、版本化、可复现的快照。

3.5 只按裸函数名发现

如果调用者只写:

javascript
client.call("sem_filter", ...)

当网络中存在多个 sem_filter 时,系统只能随机选择、按先后顺序选择,或者依赖隐藏策略。

这在 demo 中看起来方便,在开放网络中却是危险的。来自不同发布者、不同 Realm、不同版本的同名函数,可能具有完全不同的权限、成本和语义。

发现可以模糊,安装和执行必须精确。


4. High Requirements

Network-Native Python Library 不是给 RPC 包一层 decorator 就能成立。它至少需要满足以下要求。

R1. Python-Native Interface

消费者必须获得普通 Python module 和 function:

javascript
from easyremote.silan.lotus import sem_filter

并支持:

  • inspect.signature()
  • IDE completion;
  • mypy/pyright;
  • 默认参数;
  • docstring;
  • sync、async 和 stream;
  • 本地参数绑定与基础校验。

R2. Import Must Be Local

import 不能访问网络。网络包必须先被解析和物化为本地 .py/.pyi facade。

网络故障可以让函数调用失败,但不能让已经安装的 module 突然消失。

R3. No Implementation Distribution

安装网络包不能下载或执行 provider 源码。它只能安装:

  • 签名;
  • schema;
  • documentation;
  • descriptor reference;
  • type stubs;
  • deterministic proxy code。

系统不应支持发布者提供的任意 install script。

R4. Global Naming Without Global Monopolies

一个 package 的身份必须包含:

javascript
Realm + Publisher + Package + Version

例如:

javascript
easynet.run :: @silan/lotus @ 1.4.2

Realm 表示身份、信任和路由域;Publisher 表示谁对 package 负责;Package 是可安装接口集合;Version 描述公开 library surface。

npm 使用 @scope/package 解决不同用户和组织之间的命名冲突,并允许 scope 与 registry 建立映射。npm Scope 提供了有价值的参照,但 Network-Native Package 分发的是接口,不是实现。

R5. Reproducible Resolution

项目必须拥有 lockfile,固定:

  • Realm identity;
  • Publisher URA;
  • package version;
  • manifest hash;
  • exported Ability URA;
  • descriptor reference;
  • schema hash;
  • call mode。

团队成员在不同机器执行 restore 后,应获得相同 Python symbols 和相同函数契约。

R6. Stable Contract, Dynamic Placement

函数契约和执行地址必须分离。

descriptor_ref 固定调用者所依赖的接口;Directory 动态解析当前 execution route。Provider 可以更换机器、重启进程或从 GPU-A 迁移到 GPU-B,而不改变消费者 import。

反过来,如果函数签名发生不兼容变化,系统不能因为“仍然找得到一个同名函数”就继续调用。

R7. Explicit Trust and Realm Crossing

搜索可以跨 Realm,但安装不能偷偷跨 Realm。

调用者必须明确声明:

javascript
easyremote add @silan/lotus@^1.4 \
  --realm easynet.run

相同的 @silan/lotus 在另一个 Realm 中是不同 package。Publisher handle 最终必须解析成 canonical principal identity,而不是长期依赖可变字符串。

R8. Complete and Auditable Invocation

一次远程调用不能退化为:

javascript
function name + args

它至少需要确定:

  • caller;
  • callee;
  • descriptor;
  • subject;
  • nonce;
  • causal context;
  • arguments。

调用必须有唯一终态:成功、失败或取消。Stream 必须有有界缓冲和单一 terminal closure。重试和 replay 必须能够被识别,执行结果必须能够通过 Receipt 审计。

R9. Honest Remote Semantics

Network-native function 可以拥有本地函数语法,但不能隐瞒网络事实。

它必须暴露可理解的异常:

  • unavailable;
  • deadline exceeded;
  • permission denied;
  • schema mismatch;
  • resource exhausted;
  • cancelled。

网络延迟不会因为使用了 Python 语法就消失。大型对象也不能被假装成普通内存引用。

R10. Data-Aware Invocation

小型 JSON 数据可以直接传递;大型 DataFrame、视频和数据集应使用 Resource reference 或 stream:

javascript
dataset = resource(
    "easynet:///r/health.example/resource/hospital/datasets/cohort"
)

summary = cohort_summary(dataset, query="...")

能力应靠近数据和设备执行,而不是默认把所有内容复制到调用者机器。


5. Our Solution

我提出把系统划分为两个互相独立但可组合的平面。

5.1 Capability Plane

Capability Plane 管理真正的执行事实:

  • 谁是 caller;
  • 谁拥有能力;
  • Ability 的 canonical identity;
  • Descriptor;
  • 当前执行位置;
  • admission;
  • routing;
  • invocation lifecycle;
  • Receipt。

EasyNet 负责这一层。Realm、Directory、Runtime 和 Hub 共同回答:

这个经过签名的调用,在当前网络状态下,是否允许执行,以及应该在哪里执行?

5.2 Package Plane

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。


6. Library Manifest

一个 Network Package 本质上是发布者签名的 interface manifest:

javascript
{
  "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。它只是声明:

javascript
这个发布者
在这个 Realm
发布了这个版本的 library
其中这些 Python symbols
对应这些受治理的 Ability contracts

Manifest 可以作为发布者拥有的网络 Resource 存在,不需要引入新的协议 identity kind。


7. End-to-End Workflow

7.1 Provider

Provider 编写普通 Python 函数:

javascript
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:

javascript
easyremote publish @silan/lotus@1.4.2 \
  --realm easynet.run

发布操作读取已经存在的 AbilityDescriptor,生成并签名 Library Manifest;它不上传源码。

7.2 Consumer Installation

Consumer 执行:

javascript
easyremote add @silan/lotus@^1.4 \
  --realm easynet.run

Package resolver:

  1. 解析 Realm;
  2. 将 @silan 解析成 canonical publisher identity;
  3. 获取满足版本范围的 manifest;
  4. 验证签名和 manifest integrity;
  5. 获取每个 AbilityDescriptor;
  6. 验证 schema 与 descriptor reference;
  7. 写入 lockfile;
  8. 生成本地 Python module;
  9. 原子切换到新 snapshot。

如果任意步骤失败,旧版本继续可用,不留下半安装状态。

7.3 Consumer Import

安装完成后:

javascript
from easyremote.silan.lotus import sem_filter

import 只加载本地生成的 facade,不访问网络。

如果用户希望采用短名称:

javascript
easyremote add @silan/lotus@^1.4 \
  --realm easynet.run \
  --as lotus

则可以:

javascript
from easyremote.lotus import sem_filter

这里的 lotus 是项目本地 alias,不是全球 package identity。

7.4 Invocation

调用:

javascript
result = sem_filter(documents, predicate="...")

执行路径为:

javascript
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;系统内部仍然保留完整身份、权限、路由和生命周期。


8. Version Model

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 依赖的是稳定接口。


9. Why This Is Not Just RPC

RPC 的中心抽象是:

javascript
调用远端方法

Network-Native Python Library 的中心抽象是:

javascript
安装一个由确定主体负责、
在确定 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 同时成立。


10. Evaluation Plan

这个系统不应该只用 hello world 验证。至少需要三类 workload。

Workload A:GPU Semantic Operators

验证:

  • consumer 没有模型和 provider repo;
  • IDE 能正确补全函数签名;
  • provider 更换 GPU host 后业务代码不变;
  • descriptor 不兼容变更能够在调用前失败;
  • stream 保持增量传输。

Workload B:Private Data Analytics

验证:

  • 原始数据不离开 owner environment;
  • consumer 只获得受限聚合结果;
  • subject 和 caller 权限被严格验证;
  • Receipt 可以说明谁对什么数据执行了什么能力。

Workload C:Edge Camera or Robot

验证:

  • 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 是否保持有界。

11. Limitations

这个抽象不能消除网络本身。

远端函数仍然可能因为网络、权限、资源和 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,而不能悄悄降级到另一个不兼容函数。


12. Conclusion

传统 Python library 的价值,是让开发者不必理解一段实现就能复用它。但传统 package manager 通过复制实现来实现复用。

Network-Native Python Library 保留同样的开发体验,却改变了复用的物理方式:

javascript
传统 Python Package:
分发实现 → 本地执行

Network-Native Python Package:
分发接口 → 远端受治理执行

所以我现在对 EasyRemote 的定位是:它不是一个更简单的 RPC decorator,也不是把互联网伪装成本地函数的魔法。

它是一个 Python capability package system:开发者安装的是稳定、类型化、版本化的接口;函数实现、模型、数据和设备继续留在拥有者那里;Realm 决定信任和解析边界;Runtime 决定执行位置;Invocation 与 Receipt 决定一次调用如何被授权、完成和审计。

真正值得讨论的不是 from easyremote... import ... 这一行是否足够优雅,而是这一行背后能否同时成立四件事:

不复制实现,仍然可以复用; 不固定地址,仍然可以寻址; 不暴露网络细节,仍然保持失败真实; 不牺牲 Python 体验,仍然拥有身份、权限、版本和审计。

如果这四件事能够在具体 workload 中被可复现地证明,那么“网络本身成为 Python library 的执行底座”才不只是语法想象,而是一个成立的系统抽象。

0 次点赞
胡思蓝2 次阅读分享:

还没有评论