5. Embedding 函数

Chroma 需要把文本转成向量才能存储。有两种方式:自己传向量,或者配置 Embedding 函数让 Chroma 自动转。如果使用第三方的,可能需要付费

5.1 方式一:自己传向量(灵活)

// 先用 Embedding 模型计算向量,再传给 Chroma
// 好处:可以用任意 Embedding 模型,完全可控
import { ChromaClient } from 'chromadb'

// 生成密码
const auth = Buffer.from("admin:123456").toString("base64");
const client = new ChromaClient({
    ssl: false,
    host: '192.168.31.249',
    port: 7777,
    headers: { Authorization: `Basic ${auth}` },
});

async function main() {
    // 创建连接的时候,这里就不用指定向量函数
    const collection = await client.getCollection({ name: 'my_docs' });
    // 1.生成向量的值
    const texts = ['文档1内容', '文档2内容']
    const embeddings = await getEmbeddings(texts)

    await collection.add({
        ids: ['doc_1', 'doc_2'],
        documents: texts,
        embeddings: embeddings,  // 2、自己传向量
        metadatas: [{ source: 'a.pdf' }, { source: 'b.pdf' }],
    })
}

// 生成向量的函数
async function getEmbeddings(texts: string[]): Promise<number[][]> {
    // 这里是以智谱的为例,可以自己实现,也可以调用第三方API
    const response = await fetch('https://open.bigmodel.cn/api/paas/v4/embeddings', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer 您的key`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            model: 'embedding-3',
            input: texts,
        }),
    })
    const data = await response.json()
    return data.data.map((item: any) => item.embedding)
}

main()

5.2 方式二:配置内置 Embedding 函数

前面已经讲过默认的本地 all-MiniLM-L6-v2,这里再简单介绍一下openai的

# 安装内置 embedding 支持
npm install @chroma-core/openai
import { ChromaClient } from 'chromadb'
import { OpenAIEmbeddingFunction } from "@chroma-core/openai";
// 生成密码
const auth = Buffer.from("admin:123456").toString("base64");
const client = new ChromaClient({
    ssl: false,
    host: '192.168.31.249',
    port: 7777,
    headers: { Authorization: `Basic ${auth}` },
});
// 使用OpenAi的向量函数
const embeddingFunction = new OpenAIEmbeddingFunction({
    modelName: "text-embedding-3-small",
    apiKey: '您的openai Key'
});

async function main() {
    const collection = await client.getCollection({ name: 'my_collection', embeddingFunction });
    await collection.add({
        ids: ["id3", "id4"],
        documents: ["这是使用的是openai的1", "这个也是使用的是openai的向量库2"],
    });
    // 查询一下
    console.log(await collection.get());
}
main()

5.3 两种方式对比

自己传向量 内置 Embedding 函数
灵活性 高,可用任意模型 低,只支持内置的几种
代码复杂度 稍高
推荐场景 生产环境、自定义模型 快速原型
与 LangChain.js 配合 LangChain.js 自己管理 Embedding 不常用

实际项目中推荐方式一,配合 LangChain.js 的 Embeddings 类使用,控制力更强。

5.4 使用 bge-small-zh-v1.5 模型处理中文

all-MiniLM-L6-v2 对中文不友好——它的训练数据以英文为主,中文 tokenization 和语义表示效果都比较差,因此我们需要选择一个对中文比较友好的 embedding 模型,比如:

特性 BAAI/bge-small-zh-v1.5 BAAI/bge-base-zh-v1.5 shibing624/text2vec-base-chinese BAAI/bge-m3 moka-ai/m3e-base
出品方 北京智源研究院 (BAAI) 北京智源研究院 (BAAI) Shibing624 北京智源研究院 (BAAI) MokaAI
核心定位 轻量、高效、平衡 性能更强、资源更高 轻量、经典、简单 全能选手:多语言、长文本、多功能 中文专家:高性价比
语言支持 中文为主 中文为主 中文 100+ 种语言 主打中文
最大文本长度 512 Token 512 Token 512 Token 8192 Token 512 Token
向量维度 512 768 (推测) 768 1024 768
参数量 2400万 - - - 1.1亿
推理速度 中等 中等 相对较慢
显存占用 (FP16) 中等 中等 较高 约 1.1GB
核心优势 极高性价比,资源消耗极低 在BGE系列中平衡效果与资源 经典轻量,上手快 多语言/长文本/混合检索能力顶尖 中文任务均衡,部署轻量
典型场景 资源有限,追求效率与效果平衡 对效果有更高要求,资源尚可 对延迟极其敏感、资源严重受限 多语言、长文档、追求极致效果 专注中文、快速部署、成本敏感

我们这里使用性价比比较高的 bge-small-zh-v1.5 作为演示。不过你在外面查询资料的时候,可能会遇到这几个包:

包名 说明 推荐说明
@xenova/transformers transformers.js 的旧包名(作者 Xenova) ❌ 已废弃,改名了
@huggingface/transformers transformers.js 的新包名(被 HF 官方收编后改名) ✅ 这就是现在该用的
chromadb/embeddings/transformers chromadb 旧版(v1) 自带的 transformers 封装路径 ❌ chromadb v2+ 已移除,v3 没这个路径
@chroma-core/default-embed chromadb 新版 的封装,内部就是调用 @huggingface/transformers

如果你装了 @chroma-core/default-embed 这个包,其实相当于已经简介装上了 @huggingface/transformers,可以直接使用。当然,推荐的还是直接使用 @chroma-core/default-embed ,因为他已经做好了封装,不需要我们再用 @huggingface/transformers包手动在封装一遍了。首次使用的时候,也会遇到下载问题,前面我们已经讲过处理方法,也就是使用 undici 库,然后代码最前面加上:

import { setGlobalDispatcher, ProxyAgent } from 'undici'
// 注意这里换成你自己的代理地址
setGlobalDispatcher(new ProxyAgent('http://127.0.0.1:7890'))

我们生成函数配置的也比较简单:

import { DefaultEmbeddingFunction } from '@chroma-core/default-embed';
const embedder = new DefaultEmbeddingFunction({
    modelName: 'Xenova/bge-small-zh-v1.5', // 改成中文模型
    dtype: 'fp16',
});
async function main() {
    // === 基本用法:把一句话转成向量 ===
    const text = '叶文洁在红岸基地向宇宙发送了信号'
    const output = await embedder.generate([text]);
    console.log(output);
}
main()

不过 bge 模型在训练对比学习(contrastive learning)的时候,用的是"问题-文档"配对的数据,比如训练数据长这样:

  • 问题:黑暗森林理论是谁提出的?(短、是疑问、意图明确)
  • 文档:罗辑提出了黑暗森林理论...(长、是陈述、内容丰富)

虽然它们讲的是同一件事,但语言形式差别很大。一个普通模型可能判断不出它们语义相关。

普通 embedding 模型(比如 all-MiniLM):训练时只学了"把意思相近的句子映射到相近的向量"。它假设你拿它做聚类、找相似句这类对称任务。query 和 document 都一视同仁。

检索模型(比如 BGE):专门为"搜索"训练的。它学的是非对称关系——"这个问题该匹配到哪段文档"。但模型本身是个黑盒,它怎么知道当前输入的是 query 还是 document?**靠前缀来区分。**前缀的作用:告诉模型"你现在处理的是哪种角色"

举个例子,BGE 训练时,数据类似这样的:

训练时的 query  "为这个句子生成表示以用于检索相关文章:红岸基地是干什么的?"
训练时的 document "红岸基地位于大兴安岭..."  (原文,不加任何东西)

这里的“为这个句子生成表示以用于检索相关文章:”这几个字就是前缀。

模型在训练中就学会了:看到带这个前缀的 → 这是查询,我要把它映射到"能匹配相关文档"的空间位置;看到没前缀的 → 这是文档,映射到"能被查询匹配"的位置。

总得来说:带前缀的是当成问题去处理,不带前缀的当成普通文档处理,入库的时候一般不用带前缀,但是检索的时候一般要带前缀。

这套规则只对检索专用模型(BGE、E5、GTE、bge-m3 等)适用。对通用对称模型(all-MiniLM、text2vec 等)加不加前缀都一样,甚至加了反而可能干扰

说到这里,这个模型的基本概念应该已经理解清楚了,接下来就是在代码中如何使用的问题了。

方式 1:连接时指定向量函数,查询前手动加前缀
// 8.3 建库:照搬 8.0,连接时指定
const embedder = new DefaultEmbeddingFunction({
    modelName: 'Xenova/bge-small-zh-v1.5', dtype: 'fp16'
})
const collection = await client.getOrCreateCollection({
    name: 'my_knowledge_base_bge',
    embeddingFunction: embedder,   // ← 连接时指定
})
await collection.add({ documents: [...] })  // 自动用 embedder.generate(),文档不加前缀 ✅

// 8.4 查询:手动加前缀,再用 queryTexts
const QUERY_PREFIX = '为这个句子生成表示以用于检索相关文章:'
const results = await collection.query({
    queryTexts: [QUERY_PREFIX + question],   // ← 手动加前缀,外面拼
    nResults: 3,
})
方式 2:封装一个带前缀逻辑的子类

我看EmbeddingFunction源码中,除了generate方法,还有个generateForQueries方法,不过DefaultEmbeddingFunction中并没有实现它,我们可以实现它:

const QUERY_PREFIX = '这是检索:'

// 继承官方封装,只补一个 query 专用方法(加前缀)
// generate() 直接复用父类:内部 pipeline + pooling:mean + normalize,文档不加前缀
class BgeEmbeddingFunction extends DefaultEmbeddingFunction {
    async generateForQueries(texts: string[]): Promise<number[][]> {
        return super.generate(texts.map(t => QUERY_PREFIX + t))
    }
}

然后我们就可以调用了,完整点的例子:

import { DefaultEmbeddingFunction } from '@chroma-core/default-embed';
import { ChromaClient } from 'chromadb'

const QUERY_PREFIX = '为这个句子生成表示以用于检索相关文章:'

// 继承官方封装,只补一个 query 专用方法(加前缀)
// generate() 直接复用父类:内部 pipeline + pooling:mean + normalize,文档不加前缀
class BgeEmbeddingFunction extends DefaultEmbeddingFunction {
    async generateForQueries(texts: string[]): Promise<number[][]> {
        return super.generate(texts.map(t => QUERY_PREFIX + t))
    }
}

// 使用子类创建函数
const embedder = new BgeEmbeddingFunction({
    modelName: 'Xenova/bge-small-zh-v1.5',
    dtype: 'fp16',
})

// 生成密码
const auth = Buffer.from("admin:123456").toString("base64");
const client = new ChromaClient({
    ssl: false,
    host: '192.168.31.249',
    port: 7777,
    headers: { Authorization: `Basic ${auth}` },
});

async function main() {
    // 直接调用时候指定向量函数
    const collection = await client.getCollection({ name: 'my_docs', embeddingFunction: embedder });
}

main()

注意:由于上面的封装是基于我拔源代码的时候发现的,不保证其它版本的也有这功能。此时我用的chromadb 客户端的版本是3.4.3@chroma-core/default-embed 的版本是:0.1.9