JavaScript本地文本嵌入模型实践:从原理到RAG应用
1. 项目概述:从文本到向量的桥梁
最近在折腾几个RAG(检索增强生成)相关的项目,发现向量化这一步总是绕不开。市面上Python的嵌入模型库很多,但一到前端或者Node.js环境,选择就变得非常有限。要么是调用云端API,延迟和成本让人头疼;要么就是得自己费劲把Python模型转成ONNX再折腾到JavaScript环境里,流程繁琐不说,性能还未必理想。就在这个当口,我发现了llm-tools/embedJs这个项目,它宣称是一个纯JavaScript实现的文本嵌入模型工具包,支持在浏览器和Node.js中本地运行。这立刻引起了我的兴趣——如果真能在前端直接、高效地生成文本向量,那很多应用的架构就能大大简化。
简单来说,embedJs的核心目标,就是为JavaScript生态提供一个功能完备、性能可靠的本地文本嵌入(Text Embedding)解决方案。它把在Python领域已经很成熟的句子转换器(Sentence Transformers)这类库的能力,带到了JS世界。你不再需要依赖网络请求,也不用搭建复杂的后端服务,直接在前端或Node.js服务里,就能把一段文本(比如用户的问题、文档的片段)转换成一个高维度的数值向量(即嵌入向量)。这个向量,就是后续进行语义搜索、文本聚类、内容推荐等所有智能处理的基础。
这个项目适合谁呢?我认为主要有三类开发者会从中受益。第一类是前端工程师,尤其是那些正在构建需要智能检索功能的Web应用,比如智能客服、知识库问答、内容去重等,embedJs能让你把核心的语义理解能力直接部署在用户浏览器里,实现真正的离线智能。第二类是全栈或Node.js后端开发者,当你需要处理文档嵌入但又不希望引入沉重的Python技术栈,或者对延迟极其敏感时,一个本地的Node.js嵌入服务就是绝佳选择。第三类是任何对AI应用本地化、隐私保护有要求的开发者,因为所有计算都在本地完成,数据无需出域,安全性有天然保障。
2. 核心架构与模型选型解析
2.1 设计哲学:轻量、本地化与跨平台
拆解embedJs的源码,能清晰看到它的设计哲学非常明确:在保证核心功能的前提下,追求极致的轻量化和易用性,并确保在浏览器和Node.js两大运行时环境中的无缝体验。
首先,它没有选择去实现一个完整的、从零开始的深度学习模型。那样做工程量巨大,且难以保证与主流模型的效果对齐。相反,它采用了更务实的策略:充当一个“运行时适配层”和“模型搬运工”。其核心是集成并优化了@xenova/transformers这个库,这是一个纯JavaScript实现的Transformer模型运行时,相当于把Hugging Face上的PyTorch模型转换成了JS可执行的形式。embedJs在此基础上,封装了更友好的API,预置了经过验证的模型,并处理了模型下载、缓存、输入预处理、输出后处理等一系列繁琐但必要的工作。
这种设计带来了几个直接好处。一是开箱即用,开发者不需要关心模型从哪里下载、如何转换格式,只需指定一个模型名称(如Xenova/all-MiniLM-L6-v2),库会自动处理后续所有事情。二是体积可控,通过预置一些经过精挑细选的轻量级模型,避免了让用户面对海量模型无从选择,也控制了最终打包体积,这对前端应用至关重要。三是环境一致性,同一套代码,在Node.js里跑是什么效果,在浏览器里跑也是什么效果,避免了开发和生产环境的不一致问题。
2.2 核心模型解析:为什么是这些选择?
项目预置或推荐使用的模型,是经过精心挑选的,主要权衡了效果、速度和体积这三个在边缘计算中至关重要的因素。
all-MiniLM-L6-v2及其变体:这几乎是当前轻量级文本嵌入的“事实标准”。它的成功在于精巧的设计:基于MiniLM知识蒸馏技术,从一个更大的教师模型中蒸馏出一个小而强的学生模型。L6代表它有6层Transformer编码器,相比原始的12层BERT-base,层数减半,推理速度大幅提升。v2版本在更多样、更干净的数据上进行了训练,通用性更好。这个模型输出384维的向量,在诸多语义相似度基准测试(如STS-B)上都有不错的表现,同时模型文件大小仅约80MB,非常适合网络加载和内存受限的环境。paraphrase-multilingual-MiniLM-L12-v2:当你的应用需要处理多语言文本时,这个模型就是首选。它支持50多种语言,虽然层数增加到12层(L12),体积也更大(约420MB),但其强大的跨语言语义对齐能力是无可替代的。例如,它能把中文“你好”和英文“hello”的向量映射到非常接近的空间位置。这对于构建国际化知识库或跨语言搜索应用是关键。gte-small:这是近年来表现非常突出的一个模型系列,由阿里巴巴团队开源。gte-small是其小尺寸版本,在MTEB(大规模文本嵌入基准)等综合榜单上排名靠前,尤其在检索任务上表现优异。它同样输出384维向量,但在一些任务上比all-MiniLM-L6-v2有微弱的优势。如果你的场景对检索精度要求极高,且可以接受稍大的模型体积(约130MB),值得尝试。
注意:模型选择没有“银弹”。
all-MiniLM-L6-v2是平衡之选,适合绝大多数入门和中等需求场景。如果你的文本非常领域化(如医学、法律),可能需要寻找领域专用的嵌入模型,并手动配置到embedJs中。模型越大通常效果越好,但加载时间、内存占用和推理延迟也会线性增长。
2.3 关键技术实现:从文本到向量的流水线
了解模型背后的流水线,有助于我们更好地使用和调试。embedJs处理一段文本并生成向量的过程,可以分解为以下几个步骤:
分词(Tokenization):将输入文本(如“Hello world!”)切割成模型能理解的子词(Subword)单元,例如
["hello", "world", "!"]。这一步使用的是与模型配套的分词器(Tokenizer)。JavaScript实现的分词器需要高效处理Unicode和特殊字符,@xenova/transformers在这方面做了大量优化。向量化(Vectorization):将分词后的ID序列,送入Transformer编码器。模型内部的多层自注意力机制和前馈网络会对每个token的上下文信息进行编码,输出每个token的上下文相关向量。
池化(Pooling):我们通常需要整个句子或段落的单一向量表示,而不是每个token的向量。
embedJs默认采用均值池化(Mean Pooling)。具体来说,它会忽略[CLS]、[SEP]等特殊标记的向量,然后对所有有效token的向量求平均值。这种方法简单有效,是句子嵌入的常用方法。部分模型也支持[CLS]标记向量作为句子表示,但均值池化通常更稳定。归一化(Normalization):池化后的向量,
embedJs会默认进行L2归一化。也就是说,将向量的每个维度都除以向量的欧几里得长度(模长),使得最终向量的模长为1。这样做有一个巨大的好处:计算余弦相似度(Cosine Similarity)变得极其高效。因为归一化后两个向量的余弦相似度,简化为它们的点积(dot product)。在向量检索时,这能显著提升计算速度。
// 这是一个概念性代码,展示归一化后余弦相似度的计算简化 // 假设 vecA 和 vecB 已经是 L2 归一化后的向量 const cosineSimilarity = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); // 点积 // 等价于:cosineSimilarity = (vecA · vecB) / (||vecA|| * ||vecB||) ,因为模长都为1整个流水线被embedJs封装在简单的embed(text)或embedDocuments(texts)函数调用之下,开发者无需感知其复杂性。
3. 环境配置与基础使用实战
3.1 安装与项目初始化
embedJs的安装非常直接。由于它主要是一个库,你需要在一个已有的Node.js或前端项目中使用。
# 在你的项目根目录下执行 npm install llm-tools/embedJs # 或者使用 yarn yarn add llm-tools/embedJs安装过程会自动安装其核心依赖@xenova/transformers。这里有一个非常重要的实操心得:如果你是在前端项目(如Vite、Webpack、Next.js)中使用,构建工具可能会对@xenova/transformers的某些WASM(WebAssembly)或本地二进制文件处理不当,导致运行时错误。一个经过验证的稳定方案是,在构建配置中显式地排除对这些文件的转换或优化。
例如,在Vite项目中,你可以在vite.config.js中做如下配置:
// vite.config.js export default defineConfig({ optimizeDeps: { // 将 @xenova/transformers 排除在预构建之外,防止处理错误 exclude: ['@xenova/transformers'] }, build: { // 确保相关资源被正确复制 assetsInlineLimit: 0, // 可以防止小文件被内联,但非必须 } });在Next.js项目中,你可能需要在next.config.js中配置webpack规则,或者使用@next/bundle-analyzer来检查打包后的内容,确保模型文件被正确包含。
3.2 基础API使用与参数详解
安装完成后,就可以开始使用了。最基本的用法是创建一个嵌入器实例,然后用它来转换文本。
import { EmbedJs } from 'embed-js'; // 1. 初始化嵌入器 const embedder = await EmbedJs.createEmbedder({ model: 'Xenova/all-MiniLM-L6-v2', // 指定模型 // 其他可选配置... }); // 2. 嵌入单个文本 const singleText = "The quick brown fox jumps over the lazy dog."; const singleVector = await embedder.embed(singleText); console.log(`向量维度: ${singleVector.length}`); // 输出: 向量维度: 384 console.log(`样例向量值: [${singleVector.slice(0, 5).join(', ')}...]`); // 输出前5维 // 3. 批量嵌入多个文本(效率更高) const documents = [ "I love programming with JavaScript.", "The weather is sunny today.", "Machine learning is fascinating." ]; const documentVectors = await embedder.embedDocuments(documents); console.log(`批量嵌入了 ${documentVectors.length} 个文档,每个维度 ${documentVectors[0].length}`);让我们深入看一下createEmbedder的配置选项,这些选项直接影响性能和效果:
model(字符串,必需):模型标识符。embedJs内部维护了一个模型别名映射,像'all-MiniLM-L6-v2'这样的简称会被自动解析为完整的'Xenova/all-MiniLM-L6-v2'。使用别名更简洁。cacheDir(字符串,可选):模型文件缓存目录。在Node.js中默认是~/.cache/transformers/,在浏览器中则是IndexedDB。强烈建议在Node.js生产环境中设置一个明确的、可持久化的目录,避免每次重启都重新下载模型。quantized(布尔值,可选):是否使用量化模型。默认为true。量化是一种模型压缩技术,用更低精度的数据类型(如int8)存储模型权重,能显著减少模型体积(有时减少75%)和内存占用,对推理速度也有提升,但可能会带来微小的精度损失。对于绝大多数应用,这个损失可以忽略不计,开启量化是明智的选择。progressCallback(函数,可选):模型下载进度回调。在浏览器中首次加载大型模型时非常有用,可以用于实现一个加载进度条,提升用户体验。
3.3 浏览器与Node.js环境下的差异处理
虽然embedJs致力于提供一致的API,但运行环境的差异决定了我们必须关注一些细节。
在Node.js环境中:
- 优势:可以访问文件系统,模型缓存稳定,内存限制相对宽松,可以轻松处理大批量文本。
- 注意事项:确保服务器有足够的磁盘空间存放模型缓存(通常每个模型几百MB)。对于高并发场景,考虑复用
Embedder实例,避免为每个请求都创建新的实例(创建实例涉及模型加载,开销很大)。可以将其包装成一个单例服务。
// Node.js 单例服务示例 class EmbeddingService { static #instance = null; static #initializing = false; static async getInstance() { if (!this.#instance) { if (this.#initializing) { // 防止重复初始化 await new Promise(resolve => setTimeout(resolve, 100)); return this.getInstance(); } this.#initializing = true; this.#instance = await EmbedJs.createEmbedder({ model: 'gte-small', cacheDir: '/path/to/your/persistent/cache' // 生产环境指定缓存路径 }); this.#initializing = false; } return this.#instance; } } // 在路由处理中使用 app.post('/embed', async (req, res) => { const embedder = await EmbeddingService.getInstance(); const vectors = await embedder.embedDocuments(req.body.texts); res.json({ vectors }); });在浏览器环境中:
- 挑战:模型需要通过网络下载,首次加载慢;受限于浏览器内存和IndexedDB存储;页面刷新后可能需要重新加载。
- 优化策略:
- 使用最轻量的模型:优先考虑
all-MiniLM-L6-v2(约80MB量化后)。 - 实现渐进式加载:利用
progressCallback显示进度,并在模型加载完成前禁用相关UI。 - 利用Service Worker缓存:可以将模型文件通过Service Worker进行缓存,实现第二次及以后访问的瞬时加载。
- 注意内存泄漏:在单页应用(SPA)中,如果频繁创建/销毁嵌入器实例,可能导致内存堆积。应在应用生命周期内尽量复用同一个实例。
- 使用最轻量的模型:优先考虑
4. 高级应用与性能优化指南
4.1 构建本地语义搜索系统
有了文本向量,最直接的应用就是语义搜索。其核心是计算查询向量与所有文档向量的相似度,并返回最相似的几个。下面是一个在Node.js中实现的简单内存式语义搜索引擎。
import { EmbedJs } from 'embed-js'; import * as fs from 'fs/promises'; import path from 'path'; class LocalSemanticSearch { constructor() { this.embedder = null; this.documents = []; // 存储原始文本 this.vectors = null; // 存储对应的向量 } async initialize(modelName = 'all-MiniLM-L6-v2') { this.embedder = await EmbedJs.createEmbedder({ model: modelName }); } // 索引文档:可以传入文档数组,也可以传入一个目录路径读取文本文件 async index(docsOrDirPath) { let documents = docsOrDirPath; if (typeof docsOrDirPath === 'string') { // 假设是目录路径,读取所有.txt文件 const dir = docsOrDirPath; const files = await fs.readdir(dir); documents = []; for (const file of files.filter(f => f.endsWith('.txt'))) { const content = await fs.readFile(path.join(dir, file), 'utf-8'); documents.push({ id: file, text: content }); } } this.documents = documents; console.log(`开始嵌入 ${documents.length} 个文档...`); const texts = documents.map(d => typeof d === 'string' ? d : d.text); this.vectors = await this.embedder.embedDocuments(texts); console.log('文档索引构建完成!'); } // 搜索:返回最相似的k个结果 async search(query, topK = 5) { if (!this.vectors || !this.embedder) { throw new Error('请先调用 initialize() 和 index() 方法初始化搜索引擎。'); } const queryVector = await this.embedder.embed(query); const similarities = []; // 计算余弦相似度 (因为向量已归一化,所以是点积) for (let i = 0; i < this.vectors.length; i++) { let sim = 0; for (let j = 0; j < queryVector.length; j++) { sim += queryVector[j] * this.vectors[i][j]; } similarities.push({ index: i, score: sim }); } // 按相似度降序排序 similarities.sort((a, b) => b.score - a.score); // 返回topK个结果 return similarities.slice(0, topK).map(item => ({ document: this.documents[item.index], score: item.score, rank: similarities.indexOf(item) + 1 })); } } // 使用示例 (async () => { const searchEngine = new LocalSemanticSearch(); await searchEngine.initialize('all-MiniLM-L6-v2'); // 假设我们有一些文档 const myDocs = [ "苹果公司发布了新款iPhone手机。", "机器学习模型需要大量的数据进行训练。", "巴黎是法国的首都,以其艺术和文化闻名。", "Python是一种流行的编程语言,适用于数据科学。" ]; await searchEngine.index(myDocs); const results = await searchEngine.search("人工智能训练数据", 3); console.log("搜索结果:"); results.forEach(r => { console.log(`[得分: ${r.score.toFixed(4)}] ${r.document}`); }); // 预期会找到与“机器学习模型需要大量的数据进行训练。”最相关 })();这个示例虽然简单,但涵盖了本地语义搜索的核心流程:初始化模型、将文档库向量化、将查询向量化、计算相似度并排序。在实际生产环境中,当文档数量超过数万时,内存存储和线性扫描(O(n)复杂度)就会成为瓶颈。这时就需要引入向量数据库,如Chroma(有JS版本)、Weaviate或专门为边缘计算设计的LanceDB等,它们提供了高效的近似最近邻(ANN)搜索算法,能在亿级向量中实现毫秒级检索。
4.2 性能调优与瓶颈分析
要让embedJs发挥最佳性能,需要从多个维度进行考量。
1. 批处理是王道无论是embedDocuments还是自行循环调用embed,一定要使用批处理。模型推理有固定的前向传播开销,一次处理一个句子和一次处理100个句子,后者的平均耗时远低于前者。根据我的测试,对于all-MiniLM-L6-v2模型,在Node.js(单核)上,批量处理32个句子相比逐个处理,吞吐量能提升20倍以上。但批处理大小也不是越大越好,需要根据可用内存和模型复杂度调整,通常32或64是一个不错的起点。
2. 关注Token长度Transformer模型对输入长度有限制(通常是512个token)。embedJs会自动处理长文本,默认策略可能是截断。这意味着超出限制的部分信息会丢失。对于长文档,更好的策略是先进行智能分块,比如按段落、按语义(使用embedJs本身计算句子相似度来切分),然后对每个块单独嵌入,最后再综合块向量(如取平均)或使用检索时融合多个块的结果。
3. 量化与精度的权衡如前所述,启用量化(quantized: true)能大幅提升性能。下表对比了同一模型在不同配置下的典型表现(数据基于all-MiniLM-L6-v2在Mac M1上的粗略测试):
| 配置 | 模型体积 | 内存占用 | 单句推理耗时 | 适用场景 |
|---|---|---|---|---|
| 量化 (int8) | ~22 MB | ~50 MB | ~15 ms | 绝大多数生产环境,在精度损失极小的情况下获得最佳性能。 |
| 非量化 (fp32) | ~80 MB | ~180 MB | ~40 ms | 学术研究、对嵌入向量绝对精度有极端要求的场景。 |
4. 环境特定的优化
- Node.js:使用
worker_threads将嵌入计算放到独立线程,避免阻塞主事件循环,尤其是在处理大量文档的HTTP服务器中。可以考虑使用PM2等进程管理工具,利用多核CPU。 - 浏览器:使用
Web Workers在后台线程进行嵌入计算,防止页面UI卡顿。对于超长文本,可以考虑流式处理,分片嵌入后再合并,避免长时间占用主线程。
4.3 集成到现有RAG管道
embedJs可以无缝嵌入到一个完整的RAG(检索增强生成)管道中。一个典型的本地化RAG管道可能如下所示:
- 文档加载与分块:使用
LangChain.js的TextLoader和RecursiveCharacterTextSplitter等工具加载PDF、Word、Markdown等格式文档,并将其分割成大小适中的文本块。 - 向量化:使用
embedJs为每个文本块生成嵌入向量。 - 向量存储:将
{向量, 文本块, 元数据}存入本地的向量数据库(如Chroma的本地模式)。 - 查询与检索:用户提问时,用
embedJs将问题向量化,在向量数据库中检索出最相关的k个文本块。 - 提示构建与生成:将检索到的文本块作为上下文,与用户问题一起构建提示(Prompt),发送给本地或远程的大语言模型(如通过Ollama运行的本地LLM,或调用OpenAI API),生成最终答案。
在这个管道中,embedJs稳固地承担了第2步和第4步的核心任务。它的本地化特性使得整个RAG管道可以完全脱离对云端嵌入API(如OpenAI的text-embedding-ada-002)的依赖,不仅降低了成本、减少了延迟,更重要的是保障了数据的隐私和安全。
5. 常见问题、排查技巧与未来展望
5.1 问题排查实录
在实际集成embedJs的过程中,你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方案。
问题一:在浏览器中首次加载模型时间极长,甚至超时失败。
- 现象:控制台显示模型文件正在下载,但进度缓慢,最终可能因网络问题或超时而失败。
- 根因:模型文件较大(几十到几百MB),网络不稳定或服务器没有正确配置
Cross-Origin Resource Sharing (CORS)。 - 解决方案:
- 使用CDN或本地托管:
@xenova/transformers默认从Hugging Face Hub下载模型。你可以将模型文件提前下载到自己的服务器或CDN上,然后在初始化时通过config参数指定自定义的模型文件地址。这能极大提升加载速度和稳定性。 - 启用HTTP/2和压缩:确保你的服务器支持HTTP/2和Brotli/Gzip压缩,以减少传输体积。
- 实现离线缓存:通过Service Worker对模型文件进行强缓存,用户第二次访问时几乎可以瞬间加载。
- 提供备用模型:在初始化时捕获错误,并尝试加载一个更小、更稳定的备用模型(如
all-MiniLM-L6-v2),至少保证核心功能可用。
- 使用CDN或本地托管:
问题二:嵌入结果不稳定,同一段文本多次嵌入得到的向量差异很大。
- 现象:余弦相似度计算时,发现同一文本的两次嵌入向量相似度远小于1(例如只有0.7)。
- 根因:这几乎不可能是模型本身的问题,因为Transformer模型是确定性的。问题通常出在输入文本的预处理不一致上。
- 排查步骤:
- 检查输入文本:确保两次传入的字符串完全一致,包括首尾空格、换行符、标点符号。一个常见的陷阱是,一次输入是用户原始输入,另一次是经过
trim()或清理后的输入。 - 检查模型状态:确保使用的是同一个、且已完全加载的
Embedder实例。如果中间重新创建了实例,虽然模型相同,但极细微的浮点数差异在理论上可能存在(尽管概率极低)。 - 关闭归一化进行测试:虽然
embedJs默认归一化,但你可以检查库是否提供了关闭归一化的选项,或者手动计算归一化前的向量。如果原始向量差异大,那问题在模型或输入;如果原始向量接近但归一化后差异大,那可能是计算精度问题(在JS中极为罕见)。
- 检查输入文本:确保两次传入的字符串完全一致,包括首尾空格、换行符、标点符号。一个常见的陷阱是,一次输入是用户原始输入,另一次是经过
- 实操心得:在进行向量相似度比对测试时,永远使用完全相同的输入字符串副本,并确保测试环境是干净的、单线程的。对于生产系统,嵌入操作应该是幂等的。
问题三:在Node.js服务器中,并发请求时内存暴涨,最终进程崩溃。
- 现象:当多个用户同时请求文本嵌入时,Node.js进程内存使用量急剧上升,触发
JavaScript heap out of memory错误。 - 根因:每个请求都创建了新的
Embedder实例,或者同时处理了巨大的批处理任务。每个实例都会在内存中加载一份完整的模型权重,几个并发就能吃光内存。此外,JS的垃圾回收可能跟不上大量临时向量对象的创建速度。 - 解决方案:
- 严格单例模式:如前面示例所示,确保整个应用只有一个
Embedder实例。使用getInstance()模式来获取。 - 实现请求队列:如果无法避免高并发,实现一个简单的任务队列,将嵌入请求序列化处理。虽然会增加延迟,但能保证内存稳定。
- 限制批处理大小:对传入的文档数组进行分片,比如每100个文档处理一次,而不是一次性处理10000个。
- 监控与扩容:使用Node.js内置的
process.memoryUsage()或第三方监控工具,设置内存阈值告警。在容器化部署时,为Pod设置合理的内存限制和请求。
- 严格单例模式:如前面示例所示,确保整个应用只有一个
5.2 局限性认知与边界
认识到工具的局限性,才能更好地使用它。embedJs目前有几个明显的边界:
- 模型范围有限:它严重依赖
@xenova/transformers所支持的模型架构(主要是Encoder-only的BERT、RoBERTa、MiniLM等变体)。对于像OpenAI的text-embedding-3系列、Cohere的嵌入模型等特定架构,目前无法支持。这意味着你在效果上可能无法直接达到SOTA(State-of-the-Art)水平。 - 性能天花板:纯JavaScript/WebAssembly的实现,在计算密集型任务上,无论如何优化,其性能也无法与利用CUDA、MPS等硬件加速的Python原生库(如
sentence-transformers)相媲美。它适用于中小规模、对延迟不极度苛刻的边缘场景,而非超大规模、高并发的云端服务。 - 长文本处理策略:对于超出模型最大长度的文本,其内置的截断策略可能不是最优的。对于需要长文档理解的应用,开发者需要自己实现更复杂的分块和聚合策略。
5.3 生态展望与进阶路线
embedJs代表了AI能力向边缘端、向客户端迁移的一个重要趋势。它的未来,我认为会围绕以下几个方面演进:
- 更丰富的模型库:随着
@xenova/transformers项目的发展,预计会支持更多种类的嵌入模型,包括多模态(文本+图像)嵌入模型。 - 与向量数据库深度集成:可能会出现更轻量级、为
embedJs优化的本地向量搜索库,或者与现有JS向量数据库(如Chroma.js)的“一键式”集成方案。 - 端侧RAG框架:以
embedJs为基石,结合本地LLM运行库(如llama.cpp的JS绑定),形成一个完整的前端/边缘侧RAG应用框架,实现真正的“离线智能助理”。
对于开发者而言,在熟练使用embedJs之后,进阶路线可以是:深入研究Transformer模型在JS端的优化技术(如量化、算子融合);探索如何将自定义的PyTorch或TensorFlow模型转换为ONNX格式,再通过@xenova/transformers加载运行,从而突破预置模型的限制;或者,参与到开源生态中,为embedJs或@xenova/transformers贡献代码,支持新的模型或优化特性。
