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
💬 评论 0
还没有评论,快来抢沙发吧~