A20第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

嵌入与向量检索

嵌入与向量检索 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完本课你能解释嵌入(embedding,嵌入/向量化)把文本变成向量的原理,说清接入向量检索需要哪三个部件,并且对本站现状有一个诚实的认识:默认只有关键词检索,向量是「接口留好了、没接线」的扩展。

问题场景

资料库里有一段「组件重复渲染排查」。你问「怎么让页面不再闪」,关键词检索一个字都命中不了——因为片段里根本没有「闪」这个词。人知道「闪」和「重复渲染」是一回事,靠的是语义。向量检索做的事就是把这种语义相似度变成可计算的:把文本映射成一串数字(向量),语义相近的文本在向量空间里距离就近。

核心解释

先把诚实的边界说清楚:向量检索需要一个真实的嵌入服务(把文本发给模型供应商,换回一个上千维的向量),本站默认没有配置嵌入模型,所以资料库的真实检索是 FTS5 关键词 + LIKE 兜底(A21 展开)。但检索的接口契约是稳定的——输入一个查询字符串,输出片段列表——所以哪天接入向量,调用方代码不用变,变的只是 search_documents 内部实现。

接入向量检索需要三个部件,缺一不可:

  1. 嵌入服务:核实可用性是第一步。本站 lib/ai/config.ts 里配的是对话模型;嵌入要用另一个模型名(如 OpenAI 的 text-embedding-3-small),调用 /v1/embeddings 端点。模型名不存在或没权限,请求会直接报错——所以先拿一条文本试调核实,别等批量导入到一半才发现。
  2. 向量存储:片段表加一列存向量(或单独一张表),导入时对每个片段调一次嵌入并落库——这是离线一次性成本。
  3. 相似度计算:检索时对用户问题也做一次嵌入(在线每次成本),然后算问题向量和所有片段向量的余弦相似度(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)的精确匹配,关键词检索反而更可靠——这正是下一课要对比的。

动手任务

  1. 核实任务:查看你所用模型提供方的模型列表(或本站设置页),确认是否有嵌入模型可选;记下名字和调用端点。没有就是「本站未启用向量」的直接原因。
  2. 手算任务:给定 a=[1,0]、b=[0.8,0.6]、c=[-1,0],用本课的 cosine 公式算三对相似度,验证「语义近分数高、相反为负」。
  3. 设计任务:在纸上画出向量接入后的资料库写入路径(上传 → 解析 → 切分 → 嵌入 → 落库),标出哪一步新增了外部调用。

常见错误

  • 以为配好对话模型就自带向量检索:对话模型和嵌入模型是两种服务,端点、计费都不同,要单独配置与核实。
  • 导入时才调嵌入、失败不管:批量导入一半嵌入服务挂了,一部分片段有向量一部分没有,检索结果不可复现。失败要记录、可重试。
  • 把相似度阈值设成拍脑袋的全局常数:不同资料密度不同,阈值要靠测试集校准,而不是「0.8 感觉不错」。
  • 拿向量检索去匹配精确标识符:useState 和 useStage 在向量空间里可能很近——恰恰是关键词检索最擅长的精确区分,向量会放行。

检查与复盘

自查清单:

  • 能说出嵌入三部件(嵌入服务、向量存储、相似度计算)与各自的成本性质
  • 能解释为什么「统一检索接口」让向量接入不需要改调用方
  • 知道本站当前默认检索是什么,向量处于什么状态

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1多选中等(多选,全部选对才算正确)

接入向量检索需要哪些部件?下列说法正确的有?(多选)

Q2单选简单

关于本站资料库检索的现状,符合课程「诚实口径」的是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...