嵌入与向量检索
嵌入与向量检索 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完本课你能解释嵌入(embedding,嵌入/向量化)把文本变成向量的原理,说清接入向量检索需要哪三个部件,并且对本站现状有一个诚实的认识:默认只有关键词检索,向量是「接口留好了、没接线」的扩展。
问题场景
资料库里有一段「组件重复渲染排查」。你问「怎么让页面不再闪」,关键词检索一个字都命中不了——因为片段里根本没有「闪」这个词。人知道「闪」和「重复渲染」是一回事,靠的是语义。向量检索做的事就是把这种语义相似度变成可计算的:把文本映射成一串数字(向量),语义相近的文本在向量空间里距离就近。
核心解释
先把诚实的边界说清楚:向量检索需要一个真实的嵌入服务(把文本发给模型供应商,换回一个上千维的向量),本站默认没有配置嵌入模型,所以资料库的真实检索是 FTS5 关键词 + LIKE 兜底(A21 展开)。但检索的接口契约是稳定的——输入一个查询字符串,输出片段列表——所以哪天接入向量,调用方代码不用变,变的只是 search_documents 内部实现。
接入向量检索需要三个部件,缺一不可:
- 嵌入服务:核实可用性是第一步。本站
lib/ai/config.ts里配的是对话模型;嵌入要用另一个模型名(如 OpenAI 的 text-embedding-3-small),调用/v1/embeddings端点。模型名不存在或没权限,请求会直接报错——所以先拿一条文本试调核实,别等批量导入到一半才发现。 - 向量存储:片段表加一列存向量(或单独一张表),导入时对每个片段调一次嵌入并落库——这是离线一次性成本。
- 相似度计算:检索时对用户问题也做一次嵌入(在线每次成本),然后算问题向量和所有片段向量的余弦相似度(cosine similarity,夹角越小越相近),取 top-k。
// 嵌入与相似度的最小实现示意(接入时才需要,本站默认不调用)
async function embed(text: string, apiKey: string): Promise<number[]> {
const res = await fetch("https://api.openai.com/v1/embeddings", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ model: "text-embedding-3-small", input: text }),
});
if (!res.ok) throw new Error(`嵌入请求失败:${res.status}`);
const data = await res.json();
return data.data[0].embedding; // 长度约 1536 的浮点数组
}
function cosine(a: number[], b: number[]): number {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb)); // -1 ~ 1,越大越相近
}
如何验证区分能力:这是向量检索的验收标准——相近提问(同义改写)应该命中同一段资料;不相关的资料(比如问「登录锁定」时,向量检索不应该把「退款流程」排在前面)分数应该明显更低。测试用 2-3 组问题对照跑一遍,看分数差距是否拉开。
示例拆解
假如接入向量,search_documents 的内部会多一条路线,对外契约不变:
// 检索入口契约不变:query in → fragments out
async function searchDocuments(query: string) {
// 路线 A(现状):FTS5 MATCH + LIKE 兜底 —— 零外部依赖,关键词精确
const kwHits = keywordSearch(query);
// 路线 B(可选接入):query 嵌入 → 与片段向量算余弦 → top-k —— 语义泛化
// const qv = await embed(query, apiKey);
// const vecHits = chunks.map(c => ({ ...c, score: cosine(qv, c.embedding) }))
// .sort((a, b) => b.score - a.score).slice(0, 5);
return kwHits; // 未来可两路合并(A21 的混合检索)
}
关键点:
- 嵌入的是片段和问题,不是整份文档——A19 的切分在这里兑现价值:片段是语义检索单元,切得太碎或太糊都直接影响向量质量。
- 离线/在线成本要分开记:导入时每个片段嵌一次(一次性),每次提问嵌一次 query(每次调用都花钱),A23 会把这类成本纳入比较。
- 向量不是万能:专有名词、编号(
useState、A15)的精确匹配,关键词检索反而更可靠——这正是下一课要对比的。
动手任务
- 核实任务:查看你所用模型提供方的模型列表(或本站设置页),确认是否有嵌入模型可选;记下名字和调用端点。没有就是「本站未启用向量」的直接原因。
- 手算任务:给定
a=[1,0]、b=[0.8,0.6]、c=[-1,0],用本课的 cosine 公式算三对相似度,验证「语义近分数高、相反为负」。 - 设计任务:在纸上画出向量接入后的资料库写入路径(上传 → 解析 → 切分 → 嵌入 → 落库),标出哪一步新增了外部调用。
常见错误
- 以为配好对话模型就自带向量检索:对话模型和嵌入模型是两种服务,端点、计费都不同,要单独配置与核实。
- 导入时才调嵌入、失败不管:批量导入一半嵌入服务挂了,一部分片段有向量一部分没有,检索结果不可复现。失败要记录、可重试。
- 把相似度阈值设成拍脑袋的全局常数:不同资料密度不同,阈值要靠测试集校准,而不是「0.8 感觉不错」。
- 拿向量检索去匹配精确标识符:
useState和useStage在向量空间里可能很近——恰恰是关键词检索最擅长的精确区分,向量会放行。
检查与复盘
自查清单:
- 能说出嵌入三部件(嵌入服务、向量存储、相似度计算)与各自的成本性质
- 能解释为什么「统一检索接口」让向量接入不需要改调用方
- 知道本站当前默认检索是什么,向量处于什么状态
参考资料
- OpenAI 文档:Embeddings —— 嵌入模型的使用场景与调用方式
- OpenAI 文档:API Reference - Embeddings —— 请求参数与返回结构
- 下一课 A21 在本站真实实现上对比关键词检索与向量检索的命中差异。
读完这一课了?
标记完成后,工作台会安排接下来的学习。