工具开发|Tagger:极简 CSV/TSV 标注器的设计与实现

图 1 Tagger的彩虹橘子🌈好看吗?!

阅读提示:在标注csv数据的时候感觉wps等等工具为什么没有快捷标记的方式,而且这些标记又没法给LLM读取,之后我咋处理,哎~所以自己按照想法搓了一个工具,至于为什么图标是个橙子,还是一个五彩斑斓的橙子,因为我喜欢吃,而且我突然想到了珍妮文斯特的“橘子不是唯一的水果”,就想要一个彩虹一样的🌈,打开Adobe搓了下,效果不错,嗯。祝食用愉快~🍊

导言:为什么要做 Tagger

事情的起因很简单:笔者经常拿到一个 CSV 或 TSV 文件,需要快速标注哪些数据重要、哪些需要复查、哪些可以直接丢掉。

市面上的 CSV 工具已经很多了——Excel、Numbers、各种在线表格。但它们都是为编辑和计算设计的。我想要的只是“快速给这一行打个标签”,而且要快,还要可以被LLM读取。

笔者需要的是:

打开文件 → 选中数据 → 按个键 → 标签打上 → 导出给 AI

就这么简单,但没有一个工具是为这件事而生的。于是决定自己做一个。

“ 橘子不是唯一的水果,但 Tagger 可以是五彩斑斓的 😊 ”

一:现有工具调研

动手之前,我先看了 GitHub 上几个已有项目。

1.1 Ceesvee

这是一个功能非常完整的 CSV 编辑器,用 Tauri + Rust + React 构建。它已经把 CSV 编辑、筛选、快捷键、行书签、标签、备注、sidecar annotation 做得非常完整,还有数据清洗、验证、PII 检测等高级功能。功能路线大致是:

CSV → 查看 → 编辑 → 筛选 → 清洗 → 验证 → Tag → Note → Export

越来越像一个完整的数据工作台。

1.2 Rowly

一个轻量的 macOS CSV 编辑器,强调快速打开和编辑,用 Electron 构建。

1.3 Columnar

一个纯粹的 CSV 查看器,明确说自己 no editing,专注于巨大 CSV/TSV 的浏览,用 Tauri 构建。

1.4 结论:故意做小

这些工具都很优秀,但没有一个是专门为“人工标注”设计的。Ceesvee 虽然有标签功能,但定位是数据工作台。

所以笔者决定定位为:

“ 一个为“快速人工标注表格数据”而生的极简 CSV/TSV Editor。 ”

核心流程就是:

打开 → 看 → 选 → 快捷键 → 打标签 → 保存 → 给 AI

二:设计

2.1 标签系统

每个标签包含四个字段:

{
"name": "important",
"definition": "High-confidence candidate that should be retained for downstream analysis.",
"color": "#4A90E2",
"shortcut": "cmd+1"
}

其中 definition 不是备注,而是标签的语义定义。这一点很重要:当标签数据导出给 LLM 时,模型需要知道 important 到底是什么意思。

2.2 四种标注范围

这是 Tagger 和普通 CSV 工具拉开差距的地方:

范围 触发方式 示例
Cell Tag 选中一个单元格 给 BRCA1 打上 important
Row Tag 选中一整行 整行标记为 review
Column Tag 选中一列 标记 phenotype 列的语义
Dataset Tag 不选任何内容 标记整个数据集

用户不需要告诉软件“我要给什么打标签”,软件根据当前选择自动判断:

2.3 快捷键优先

快捷键是整个软件最重要的交互:

T        → 进入标签模式
1 → 添加第 1 个标签
2 → 添加第 2 个标签
Shift+1 → 移除第 1 个标签
Esc → 退出标签模式

用户可以连续操作:选行 → 按 1 → 选下一行 → 按 2 → 选下一行 → 按 3……这就是真正的“快速标注”。

2.4 数据文件与标签文件分离

这是一个关键设计决策。标签不写进 CSV 本身,而是保存在独立的 JSON 文件中:

data.tsv
data.tsv.tags.json

这样做的好处:

  1. CSV 保持干净,用任何工具打开都不会看到奇怪的标注;
  2. 标签文件可以单独导入导出;
  3. 大模型可以直接读取 JSON 理解标注含义。

2.5 AI 导出格式

专门设计了一个给 LLM 的导出格式:

{
"dataset": {
"file": "data.tsv",
"description": "Human phenotype-gene associations"
},
"tag_definitions": {
"important": {
"definition": "High-confidence candidate."
}
},
"annotations": [
{
"row_id": "001",
"tags": ["important"]
}
]
}

这一步的本质:把人的隐性判断,转换成机器可理解的显式知识。

三:技术选型

3.1 第一方案:Tauri + Rust + React

参考 Ceesvee 和 Columnar 的技术栈,最初选择:

理由:

  1. Tauri 已经验证可以处理百万行 CSV;
  2. 跨平台:macOS、Windows、Linux;
  3. 比 Electron 轻量得多。

3.2 架构设计

┌─────────────────────────────────┐
│ React / TypeScript (UI层) │
│ - 表格虚拟化 │
│ - 标签面板 │
│ - 快捷键处理 │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ Tauri Commands (通信层) │
│ - 文件读写 │
│ - CSV/TSV解析 │
│ - 标签持久化 │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ Rust Core (业务逻辑层) │
│ - CSV/TSV解析器 │
│ - 行身份管理 │
│ - 标签数据结构 │
└───────────────┬─────────────────┘
│
本地文件系统

3.3 行身份管理

不能用行号作为行的标识——用户排序后行号会变。参考 Ceesvee 的方案:

  1. 优先使用用户指定的 ID 列;
  2. 没有 ID 列时,使用 SHA256(row content) 作为行 ID。

3.4 两版并行

最终项目同时包含两个版本:

tagger/
├── tagger.html ← 浏览器独立版(零安装)
├── src/ ← React 前端(桌面版 + 开发版)
├── src-tauri/ ← Rust 后端(桌面版)
└── package.json ← Node.js 项目配置

浏览器版和桌面版共用同一套设计理念和 UI 风格,差别主要在运行环境和适用的文件大小。

四:实现细节

4.1 CSV 解析

手写了一个支持引号和换行的 CSV 解析器:

function parseCSV(text, delimiter) {
const lines = [];
let current = '';
let inQuotes = false;
let row = [];

for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inQuotes) {
if (ch === '"' && text[i+1] === '"') { current += '"'; i++; }
else if (ch === '"') { inQuotes = false; }
else { current += ch; }
} else {
if (ch === '"') { inQuotes = true; }
else if (ch === delimiter) { row.push(current); current = ''; }
else if (ch === '\n' || ch === '\r') {
// 处理换行...
}
else { current += ch; }
}
}
// ...
}

关键点:

4.2 标签持久化

浏览器版使用 localStorage,以文件名为 key:

function saveKey() { return 'tagger_' + filePath; }

function saveState() {
localStorage.setItem(saveKey(), JSON.stringify({ tags, annotations }));
}

桌面版(Tauri)使用独立的 JSON 文件,由 Rust 端读写。

4.3 标注逻辑

核心逻辑很简单——根据当前选择判断标注范围:

function applyTag(tagName) {
if (selectedCell) {
// 单元格标注
annotateCell(selectedCell.row, selectedCell.col, tagName);
} else if (selectedRows.size > 0) {
// 行标注
for (const ri of selectedRows) {
annotateRow(ri, tagName);
}
} else {
// 数据集标注
annotateDataset(tagName);
}
}

4.4 快捷键处理

用全局 keydown 事件监听,区分模式和上下文:

document.addEventListener('keydown', (e) => {
// T → 切换标签模式
if (e.key === 't' && !e.metaKey && !e.ctrlKey) {
toggleMode();
return;
}

// 标签模式下的数字键
if (mode === 'tag') {
const num = parseInt(e.key);
if (num >= 1 && num <= 9) {
const tag = tagList[num - 1];
if (e.shiftKey) removeTag(tag.name);
else applyTag(tag.name);
}
}
});

4.5 UI 设计

UI 遵循“克制”原则:

┌─────────────────────────────────────────────────────────┐
│ data.tsv 🔍 🏷 Export │
├─────────────────────────────────────────────────────────┤
│ # Gene Disease Score Tags │
├─────────────────────────────────────────────────────────┤
│ 1 BRCA1 Breast 0.92 🔵 important │
│ 2 TP53 Cancer 0.87 🟡 review │
│ 3 EGFR Lung 0.76 │
│ 4 PTEN Cancer 0.72 🔴 reject │
├─────────────────────────────────────────────────────────┤
│ 1,248 rows 3 selected ⌘1 Important ⌘2 Review │
└─────────────────────────────────────────────────────────┘

右侧标签栏默认收起,点击展开。默认界面不要出现一大堆按钮。

五:反思与收获

5.1 定位

市场上已经有太多 CSV 编辑器。Tagger 的优势恰恰是“不做其他,只做标签”。这个定位让它和所有现有产品形成明确差异。

5.2 技术选型

最初选择 Tauri + Rust 是因为参考了 Ceesvee 的成功案例;当环境装不了 Rust 时,果断切换到纯浏览器方案。

5.3 设计先行

在写第一行代码之前,产品设计文档已经非常详细:

这些设计在实现过程中几乎没有大的修改

5.4 做小

砍掉功能比添加功能更需要判断力:

6.5 隐性判断变显式知识

“把人的隐性判断变成机器可理解的显式知识”,可能是 Tagger 最有价值的设计理念。

用户给一行数据打上 important 标签,这个判断是隐性的;当标签有明确定义、导出格式结构化时,这个判断就变成了显式的、可传递的知识。这比“AI 自动打标签”更有意义——人的判断是有语境的,AI 需要先理解这个语境。

七:下一步

Tagger 目前是原型阶段,我思考了一下未来的方向(如果有时间继续做的话.jpg):

  1. AI 辅助标注:人工标一部分,AI 预测剩余部分;
  2. 协作功能:多人标注同一文件;
  3. 高级导出:更多格式、自定义模板;
  4. 性能优化:Web Worker 解析、虚拟化渲染;
  5. 更多文件格式:Excel、JSON、Parquet。

笔者的话

橘子不是唯一的水果,但 Tagger 可以是五彩斑斓的 😊 这个项目从想法到能用的原型花的时间很短

项目地址:github.com/your-username/tagger 反馈邮箱:1747819157@qq.com