RAG ①:Embeddings 与向量数据库入门(Node.js 优先)
摘要:RAG(Retrieval-Augmented Generation)的第一步不是“让模型更强”,而是把你的文档变成一种可检索的索引。Embeddings 负责把文本映射到向量;向量数据库负责存储与相似度搜索。
1. Embeddings:把文本变成向量
Embedding 模型会把一段文本编码成一个高维向量(例如 1536/3072 维)。语义越接近,向量越接近。
你可以把它理解成:
- 传统搜索:靠关键词匹配
- 向量搜索:靠“语义距离”
典型用途:
- 文档/FAQ 检索
- 相似问答
- 聚类、去重
2. 相似度指标(你需要知道的最少知识)
常见三种:
- Cosine(余弦相似度):最常用
- IP(Inner Product):有时等价于 cosine(取决于向量是否归一化)
- L2(欧氏距离)
工程上你主要关注:
- 你选的向量库支持哪种 metric
- embedding 模型输出是否已归一化(normalized)
3. 向量数据库选型:Milvus / Chroma / pgvector
roadmap 里提到三类:
- Milvus:生产级,能力全(适合大规模、团队协作)
- Chroma:轻量,本地开发友好(适合 demo/小项目)
- pgvector:把向量能力放进 PostgreSQL(适合已有 PG 体系的团队)
本文先不绑定具体向量库,先把“embedding + upsert + query”的抽象概念讲清楚。
4. Node.js:计算 Embedding(OpenAI 兼容)
下面示例以 OpenAI 风格 SDK 为例(其它兼容平台结构类似)。
你只需要替换
baseURL(如果不是 OpenAI)和apiKey。
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
// baseURL: "https://xxx/v1" // 如果你用的是兼容 OpenAI 的第三方平台
});
export async function embed(text) {
const resp = await client.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return resp.data[0].embedding; // number[]
}
// demo
const v = await embed("RAG 是什么?");
console.log(v.length);
工程建议:
- 对输入做去噪(去掉多余空白、HTML 标签等)
- 对同一段文本做缓存(embedding 成本可观)
5. 一套最小的向量检索接口(建议你先抽象出来)
无论你用 Milvus/Chroma/pgvector,最终你都需要这三个能力:
upsert(chunks[]):写入/更新向量query(vector, topK):按向量相似度查 topKdelete(filter):删除(可选)
你可以先定义一个最小接口:
// 伪代码/接口定义
export interface VectorStore {
upsert(items: Array<{ id: string; vector: number[]; text: string; metadata?: any }>): Promise<void>;
query(vector: number[], topK: number): Promise<Array<{ id: string; score: number; text: string; metadata?: any }>>;
}
有了它,你的 RAG 逻辑就不会被某个向量库绑定死。
6. 最小 RAG(只差一步):把检索结果拼回 Prompt
在你有了 query() 之后,RAG 的第一版就能跑:
- 用户问题 -> embedding
- 去向量库 query topK
- 把 topK 文本片段作为“证据”拼到 prompt
- 让 LLM 基于证据回答,并要求引用证据
拼 prompt 的示例:
你是一个文档问答助手。请只根据 EVIDENCE 回答,不要编造。
EVIDENCE:
- (doc1#chunk3) ...
- (doc2#chunk7) ...
QUESTION:
{userQuestion}
7. 下一篇写什么
下一篇进入 RAG 的“ETL”部分:
- 文档加载(Markdown/HTML/PDF)
- 切分(Recursive / MarkdownHeader)
- 为什么不能随便按长度切
并且会继续保持 Node.js 优先(示例会用 JS/TS)。