我给 DSH 写了读 10x 数据的插件

让 Agent「先看规模再干活」------ 我给 DeepSeek Harness 写的 10x .h5 计数插件

一个只读 8 字节、毫秒级返回基因数/细胞数的 DSH 模型侧工具插件, 以及它背后那点「不读数据就能知道数据有多大」的小心思。

源码已开源Wang-Xinfu/h5-counts-plugin ------ 挂载版与含零依赖读取器的完整版都在仓库里


一、从一次尴尬的对话说起

做单细胞分析的人大概都经历过这样的对话:

复制代码
我:帮我看一下这个 10x 矩阵有多大?
模型:好的,正在读取文件......
模型:(咔哒咔哒,读了半天)
模型:这个文件是 38.8 MB,有 134,920 个特征、2,711 个细胞。

问题在于:为了回答「多大」,模型把整个矩阵读了一遍 ------ 解压了 2451 万个非零元素。而它真正需要的答案,其实只存在文件头部的 8 个字节里。

matrix/shape 这个数据集只有两个整数:[n_genes, n_cells]。Cell Ranger 在写文件的第一时间就把它们写好了,矩阵本体(data / indices / indptr)反而被分块压缩、散落在文件各处。读取规模信息却要付出解压全量的代价,就像为了看电梯里的楼层牌,把整栋楼爬了一遍。

于是我给 DeepSeek Harness 写了一个工具插件:dsh-tool-h5-counts ,注册模型侧工具 get_cell_gene_counts。模型从此可以用一次工具调用、几十毫秒拿到矩阵规模,再决定后续要不要读全量、怎么过滤、怎么聚类。


二、插件是什么

less 复制代码
dsh-tool-h5-counts(Cordis 插件)
└── name: tool-h5-counts
    └── inject: ['tools']
        └── apply(ctx)
            └── ctx.tools.register(defineTool({
                    name: 'get_cell_gene_counts',
                    input : file_path         // 10x .h5 矩阵文件路径
                    output: { genes, cells }  // 行数(基因) × 列数(细胞)
                }))
  • 输入 :一个参数 file_path(绝对路径或相对会话工作区的路径)。
  • 输出 :结构化 JSON { "genes": N, "cells": M },渲染为一行可读文本: get_cell_gene_counts: 134920 genes (rows) x 2711 cells (columns)
  • 行为 :只读 matrix/shape 头部,不加载、不解压任何矩阵数据 ------ 这是全部设计的核心。

模型侧的使用方式也很自然------它自己决定什么时候调用,就像用 ls 先看看目录里有什么:

css 复制代码
工具调用: get_cell_gene_counts(file_path="pbmc3k_filtered_feature_bc_matrix.h5")
返回    : {"genes": 134920, "cells": 2711}
模型    : 这个矩阵约 2.7k 细胞 × 13.5 万特征,规模不大,可以直接做标准 QC + 聚类流程......

三、为什么只读头部就够了

10x 的 .h5 是 HDF5 容器,标准结构如下:

bash 复制代码
/filtered_feature_bc_matrix.h5
├── /matrix/
│   ├── shape      # [n_genes, n_cells]   ← 只读这一个数据集(2 个整数,8 字节)
│   ├── data       # 非零表达值(chunked + deflate 压缩,几千万个)
│   ├── indices    # 行索引(同 data 一样庞大)
│   └── indptr     # 列指针
└── /matrix/features/*  # 基因名、feature_type 等元数据

shape连续存储 (contiguous layout)的小数据集,位于文件前部;data/indices/indptr分块压缩(chunked + deflate/shuffle)的大数据集。所以:

做法 读取量 实测耗时(38.8 MB PBMC 3k)
只读 matrix/shape(本插件) 8 字节 35 ms
读取完整矩阵(data+indices+indptr 2451 万个非零元素 520 ms

基准数据为 10x Genomics 公开数据集 3k PBMCs from a Healthy Donor(Granulocytes removed through cell sorting)Multiome(ATAC + 基因表达)版 (Cell Ranger ARC 2.0.0 处理,官网下载):文件 38.8 MB,矩阵 134,920 特征 × 2,711 细胞(36,601 个基因 + 98,319 个 ATAC peak),24,511,186 个非零元素。以上为进程内计时,Python 3.14 + h5py 3.16,不含解释器启动开销。另外用构造的 v0/v2/chunked/shuffle 各布局测试矩阵实测,头部读取在 0.77--2.12 ms

注意这个差距是结构性的:全量读取耗时随非零元素数线性增长,几百万细胞的数据集要读几十 GB 的解压量;而头部读取是 O(1) 的,文件再大也还是毫秒级。这正是「让 Agent 先看规模」能成立的原因------代价几乎为零,收益是避免一次昂贵的全量误读。


四、两代实现:从「依赖 h5py」到「零依赖也能读」

这个工具我写过两个版本,正好体现了「先能跑,再跑得稳」的演进。

4.1 简单版:python -c + h5py

最初挂到 web profile 的版本(lib/index.js)只有几十行:用 execFile 调本机 python -c,让 h5py 打开文件读 matrix/shape,JSON 输出。h5py 是事实标准,能处理所有布局,天然正确。

js 复制代码
const PY_SCRIPT = [
  'import json, sys',
  'import h5py',
  "with h5py.File(sys.argv[1], 'r') as f:",
  "    shape = f['matrix/shape'][()]",
  "print(json.dumps({'genes': int(shape[0]), 'cells': int(shape[1])}))",
].join('\n');

局限也很明显:依赖目标机器装了 python + h5py。换一台没有 h5py 的机器,工具就哑了。

4.2 完整版:内嵌零依赖 HDF5 读取器

于是有了第二个版本(complete/get_cell_gene_counts.plugin.js,721 行):内嵌一段独立的 Python 脚本,h5py 可用时优先用 h5py,否则退回纯标准库读取器,自己解析 HDF5 二进制格式:

  • Superblock v0/v1 :老式布局,解析地址大小、基地址、根组符号表项;v2/v3 :现代布局,用 OHDR 魔数探测两种偏移排布(有无保留字节),兼容 HDF5 1.10+ 与 1.14。
  • v1 对象头 :对象头续接消息(0x0010)、符号表消息(0x0011)→ B-tree(TREE)+ 符号节点(SNOD)+ 本地堆(HEAP)解析组层级;v2 对象头:按 flags 跳可选字段、每 4 条消息一个 gap、Link 消息(0x0006)取成员,遇到 fractal-heap 索引(0x0002)则明确报错并提示装 h5py。
  • 数据集消息 :dataspace(v1/v2 两代编码)取维度;layout 支持 compact / contiguous / chunked 三类,兼容 HDF5 1.14 的 layout v3 偏移;filter pipeline 反向执行 deflate(zlib)+ shuffle 过滤器(按 chunk 的 filter mask 跳过未施加的过滤器)。
  • Chunked B-tree :经典布局与 HDF5 1.14 布局(32 字节条目)按节点逐条校验识别;对 shape 只需读第一个 chunk 的前 64 字节,然后立即停止。

工程细节上:

  • 脚本经 stdin 流入 python - ,文件路径走 H5_PATH 环境变量,彻底绕开 shell 转义和引号问题;
  • 宿主侧接入 fs.resolve / stat / processPath(解析相对路径、校验文件存在)、沙箱策略(越界文件明确报 [sandbox: ...])、会话 cwd、exec.signal 中止、60s 超时、stdout 上限;
  • 统一的错误契约:失败输出 {"error": "..."} 并非零退出,宿主把 stderr 细节透传给模型。

读取器对每个字段都做合法性校验(魔数、地址范围、rank 上限、chunk 大小),不会因为一个坏文件把整个 Agent 进程带崩------这其实是给 LLM 用的工具最重要的素质:它要能优雅地说「我不行」


五、怎么装进你的 DSH

以挂到 web profile 为例(profile 目录默认在 $env:DSH_HOME\profiles\web,流程与官方 dsh plugin 等价):

powershell 复制代码
# 1) 克隆源码放进 profile 的 plugins 目录
git clone https://github.com/Wang-Xinfu/h5-counts-plugin.git $env:DSH_HOME\profiles\web\plugins\dsh-tool-h5-counts

# 2) 在 profile 的 package.json 声明依赖
#    "dependencies": { "dsh-tool-h5-counts": "file:./plugins/dsh-tool-h5-counts" }

# 3) 在 profile 目录安装依赖(转发 pnpm)
dsh plugin --profile web install

# 4) 在 profile 的 cordis.patch.yml 插入插件行
#    - insert:
#        - id: tool-h5-counts
#          name: dsh-tool-h5-counts

# 5) 重启 dsh web,新会话即可使用

运行依赖:本机 python;挂载版还需 h5pypip install h5py),完整版 h5py 可选------没有时自动退回纯标准库读取器。若提示 tool "get_cell_gene_counts" is already registered in this scope(和 Harness 内置工具重名),把 defineToolname 改成 h5_matrix_counts 即可。


六、测试:把「读不了的文件」也测了

开发时构造了一套测试矩阵(test_matrix_v0.h5 / test_matrix_v2.h5 / test_matrix_chunked.h5 / test_matrix_chunked_shuffle.h5,以及仿真的 real_*.h5),分别覆盖:

  • superblock v0 与 v2(老/新文件格式);
  • 连续存储与分块存储;
  • 纯 deflate 压缩与 deflate + shuffle 组合。

有意思的是,其中一部分文件连 h5py 自己都会报 bad heap free list 打不开------这正是零依赖读取器存在的意义:它能读的布局,比 h5py 在部分异常文件上能读的还多,而且每次失败都会给出可操作的提示(「run 'pip install h5py' and retry」)。


七、写在最后

这个插件很小,但它是「给 Agent 设计工具」的一个不错样本:

  1. 把 O(n) 的问题变成 O(1) ------ 模型问规模,就别让它读全量;
  2. 能降级、能自救 ------ 有 h5py 用 h5py,没有就用纯标准库硬解析;
  3. 错误即信息 ------ 每个失败路径都告诉模型「差在哪、怎么修」;
  4. 接入要浅 ------ 纯 Cordis 插件、零运行时依赖,卸载即恢复。

下次你的 Agent 再遇到 .h5,它会在「要不要读这个 38 MB 的文件」之前,先花 35 毫秒问一句:这里面到底有多少细胞。


插件源码已开源:Wang-Xinfu/h5-counts-plugin ------ 挂载版 lib/index.js,含零依赖读取器的完整版 complete/get_cell_gene_counts.plugin.js。基准数据为 10x Genomics 公开数据集 3k PBMCs from a Healthy Donor(Granulocytes removed through cell sorting) 的 Multiome 版(官方下载)。

相关推荐
执子念的飞鱼1 小时前
70MB Excel 浏览器预览失败:定位 XLSX 重复解压与内存峰值
javascript·性能优化
12.=0.1 小时前
【REVIEW_C】【持续更新】
服务器·前端·javascript
Hilaku2 小时前
为什么同一段代码在 Safari 上永远有 Bug?
前端·javascript·程序员
张元清2 小时前
React scrollIntoView + useRef:滚动到指定元素 (2026)
javascript·react.js
BreezeJiang2 小时前
别再背工厂模式了:NestJS 第一行代码就是它的工业级落地
前端·javascript
渣波3 小时前
深度解析工厂模式:从蜜雪冰城到 NestFactory,彻底搞懂“创建与使用分离”
前端·javascript
雪芽蓝域zzs3 小时前
新建前端pnpm(vue js ) 仿若依项目(一)
前端·javascript·vue.js
kaixin_learn_qt_ing4 小时前
Electron程序---初体验
javascript·electron
星夜夏空994 小时前
网络编程(5)—— Reactor实现(v1)
服务器·javascript·网络