Skip to Content
Transformer 工程实战实战三——TypeScript 集成方案

JS 生态里的 Transformer 工具链

Python 占据了机器学习工具链的主导地位,但大量线上服务跑在 Node.js 上。工程师面临的现实选择不是”要不要用 Transformer”,而是”用什么方式集成”。

@huggingface/transformers

这是 Hugging Face 官方维护的 JavaScript 库(前身是 transformers.js),本质上是 ONNX Runtime Web 的高层封装。它的工作方式是:先将 PyTorch 模型转换为 ONNX 格式,再通过 ONNX Runtime 执行推理。这个转换步骤由 Hugging Face 预先完成,用户直接下载转换好的 .onnx 文件即可。

Python 训练 (PyTorch) ONNX 导出 (Hugging Face Hub 提供) ONNX Runtime Web / Node (JS 执行推理)

onnxruntime-node

@huggingface/transformers 在 Node.js 环境下内部依赖 onnxruntime-node。如果只需要推理自定义 ONNX 模型(不经过 Hugging Face Hub),可以直接使用 onnxruntime-node,控制粒度更细。

与 Python 生态的差距

维度Python (PyTorch)JS (@huggingface/transformers)
模型支持全量有限(需官方或社区提供 ONNX 版本)
推理性能GPU 加速,成熟优化CPU 为主,GPU 支持有限
量化/优化丰富(GPTQ、AWQ、bitsandbytes)基础量化(INT8/FP16)
训练/微调完整支持不支持
生态成熟度中等,快速发展

对于推理任务,@huggingface/transformers 覆盖了常见 NLP 场景(分类、NER、embedding、生成)。复杂的优化需求(如极低延迟的大模型推理)仍然需要 Python 侧的专用推理服务。


场景一:Node.js 后端推理

适用条件:

  • 对延迟要求不极致(100ms 级别可以接受)
  • 不想维护单独的 Python 推理服务
  • 模型规模在几百 MB 以内(如 DistilBERT、MiniLM)
  • 推理频率不高(峰值 QPS < 10)

安装依赖

npm install @huggingface/transformers npm install -D typescript @types/node ts-node

情感分析示例

// src/node_inference.ts import { pipeline } from '@huggingface/transformers'; // 首次运行会下载模型到本地缓存(~/.cache/huggingface/hub) // 模型大小约 67MB,之后复用缓存 const classifier = await pipeline( 'text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english' ); const result = await classifier('This movie is absolutely fantastic!'); console.log(result); // 输出: [{ label: 'POSITIVE', score: 0.9998 }]

pipeline 支持的任务类型:

任务类型字符串用途
text-classification文本分类、情感分析
token-classificationNER、词性标注
feature-extraction提取句子向量(embedding)
text-generation文本生成(小模型)
translation翻译
summarization摘要
zero-shot-classification零样本分类

性能特点

Node.js 推理使用 onnxruntime-node,默认跑在 CPU 上。对于 DistilBERT 规模的模型,单次推理耗时通常在 20-100ms(取决于序列长度和硬件)。首次推理还包含模型加载时间,热身后性能稳定。

如果推理频率高,可以将 pipeline 实例缓存在模块级别,避免重复加载:

// 模块级缓存,进程生命周期内只初始化一次 let classifier: Awaited<ReturnType<typeof pipeline>> | null = null; async function getClassifier() { if (!classifier) { classifier = await pipeline( 'text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english' ); } return classifier; }

完整可运行代码见 examples/src/node_inference.ts


场景二:浏览器端推理

适用条件:

  • 离线处理场景(PWA、桌面 Web 应用)
  • 隐私敏感数据不能发送到服务器
  • 只需要轻量任务(分类、嵌入)

限制:

  • 只能用小模型:浏览器的内存和计算资源有限,一般使用 Tiny/Small 级别的模型(< 50MB)
  • 首次加载慢:模型文件通过网络下载,需要做好缓存策略
  • 不支持 GPU 计算(WebGPU 支持还在推进中)

基本用法

@huggingface/transformers 同时支持 Node.js 和浏览器环境。浏览器端的引入方式使用 ES Module:

<script type="module"> import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3/dist/transformers.min.js'; const classifier = await pipeline( 'text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english' ); const result = await classifier(document.getElementById('input').value); console.log(result); </script>

配合 Web Worker 使用

推理运算会阻塞主线程。生产环境中必须将推理放入 Web Worker:

// inference.worker.ts import { pipeline } from '@huggingface/transformers'; let classifier: Awaited<ReturnType<typeof pipeline>> | null = null; self.onmessage = async (event: MessageEvent<{ text: string }>) => { if (!classifier) { classifier = await pipeline( 'text-classification', 'Xenova/distilbert-base-uncased-finetuned-sst-2-english' ); } const result = await classifier(event.data.text); self.postMessage(result); };
// main.ts const worker = new Worker(new URL('./inference.worker.ts', import.meta.url), { type: 'module', }); worker.postMessage({ text: 'I really enjoyed this experience!' }); worker.onmessage = (event) => { console.log('推理结果:', event.data); };

模型缓存

浏览器端模型文件通过 Cache API 缓存,存储在 Origin Private File System(OPFS)中。下载一次后,后续直接从本地加载,无需重复下载。


场景三:对接 Embedding API

当不需要在本地跑模型时,直接调用 API 是最轻量的方案。OpenAI 的 text-embedding-3-smalltext-embedding-3-large 是目前综合性价比较高的选择。

安装

npm install openai

获取文本向量

import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // baseURL 可以替换为兼容 OpenAI 格式的本地服务: // baseURL: 'http://localhost:11434/v1' // Ollama // baseURL: 'http://localhost:8000/v1' // vLLM }); const response = await client.embeddings.create({ model: 'text-embedding-3-small', input: ['文本一', '文本二', '文本三'], encoding_format: 'float', }); // response.data 是按 index 排序的向量列表 const embeddings = response.data.map((item) => item.embedding); // embeddings[0] 是一个 1536 维的 number[]

text-embedding-3-small 的关键参数:

参数
向量维度1536(可通过 dimensions 参数压缩到 256-1536)
最大输入长度8191 tokens
批量请求上限2048 条/次
费用$0.02 / 1M tokens(截至 2024 年)

替代方案

openai SDK 支持通过 baseURL 参数指向任何兼容 OpenAI API 格式的服务:

  • Ollamahttp://localhost:11434/v1):本地运行开源模型,适合开发环境和数据不出境场景
  • vLLMhttp://localhost:8000/v1):高性能推理服务,适合生产环境自部署
  • Groq、Together AI:第三方托管服务,通常比 OpenAI 便宜

只需修改 baseURLmodel,其余代码不变。

完整示例见 examples/src/embedding_api.ts


三种方案的选型对比

维度Node.js 本地推理浏览器端推理Embedding API
延迟中(20-200ms)高(首次加载慢,推理 50-500ms)低(网络 RTT + API 处理,50-200ms)
部署复杂度低(无额外服务)低(纯前端)极低(无需部署模型)
模型规模限制中(< 500MB 合理)严格(< 50MB 为宜)无限制(由 API 提供商决定)
可用性依赖无外部依赖无外部依赖依赖 API 服务
数据隐私数据不出机器数据不离开浏览器数据发送给 API 提供商
成本计算成本(服务器 CPU)用户设备算力API 调用费用
适用场景低频推理、不想维护 Python 服务离线、隐私敏感场景大多数生产场景

决策流程:

数据是否涉及隐私 / 需要离线使用? ├── 是 → 本地推理(Node.js 或浏览器端) │ └── 是否在浏览器中? │ ├── 是 → 浏览器端推理(限 < 50MB 模型) │ └── 否 → Node.js 本地推理 └── 否 → Embedding API(首选,最简单) └── 需要控制成本或数据主权? └── 是 → 自部署 Ollama/vLLM + OpenAI 兼容 API

实战:给现有 Web 项目加语义搜索能力

这一节将上面的工具拼在一起,实现一个完整的语义搜索功能。场景是为一个知识库加上”相关文档推荐”。

架构

用户查询 embed(query) ← OpenAI Embedding API cosineSimilarity() ← 纯 JS 计算,内存操作 排序,返回 topK ← 无外部数据库 相关文档列表

这个实现不依赖向量数据库,文档列表存在内存里。文档数量在数千条以内,这种方式完全够用,部署复杂度极低。

核心接口

// 构建内存索引 const index = await buildIndex(documents: string[]) // 执行语义搜索 const results = await search(index, query: string, topK = 3) // 返回: { document: string; score: number; rank: number }[]

buildIndex 实现要点

  • 分批调用 API,每批 100 条,避免超出请求体积限制
  • 返回 IndexEntry[],每条记录存储原文和对应的向量

search 实现要点

  • 对查询文本调用一次 Embedding API
  • 遍历所有 IndexEntry,计算余弦相似度
  • 按相似度降序排序,取前 topK 条

接入现有项目

以 Express.js 接口为例:

import { buildIndex, search } from './semantic_search.js'; // 应用启动时构建索引(一次性操作) const docs = await loadDocumentsFromDB(); // 从数据库加载文档 const index = await buildIndex(docs); // 搜索接口 app.get('/search', async (req, res) => { const { q, k = 5 } = req.query; if (typeof q !== 'string') { return res.status(400).json({ error: 'missing query' }); } const results = await search(index, q, Number(k)); res.json(results); });

扩展方向

文档数量超过 1 万条后,线性扫描的性能会下降。此时可以切换到向量数据库:

  • pgvector:PostgreSQL 插件,无需引入新基础设施,适合已有 Postgres 的项目
  • Qdrant:独立向量数据库,支持过滤、分片,适合向量检索是核心功能的场景
  • Chroma:轻量嵌入式向量数据库,适合单机或开发环境

切换时,只需替换 buildIndexsearch 的实现,上层接口保持不变。

完整的 semantic_search.ts 实现见 examples/src/semantic_search.ts

Last updated on