逐日AI
第 1 周 · D2约 4 小时

embedding 与向量检索:相似度、维度与模型选型,把文本存进 pgvector

从坐标系这个类比出发理解向量到底编码了什么,弄清余弦相似度与内积的区别和归一化的前提,再对着排行榜谈模型选型与维度取舍,最后把语料写进 pgvector 并查出最近邻。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能解释 embedding 把什么变成了坐标,并说明为什么余弦相似度在归一化之后等价于内积
  2. 能列出选 embedding 模型时必须看的四项:多语言能力、维度与存储成本、最大输入长度、是否需要区分查询与文档前缀
  3. 能用 pgvector 建表、写入向量、建索引并查出最近邻,且看得懂查询计划里有没有走索引

昨天的 BM25 只认字面:你把"文件上传上限"换成"附件体积",它一个词都对不上。今天给系统装上第二只眼睛,把"意思相近"变成"坐标相近"。读完并做完实验之后,回到页面顶部把这三条目标逐一勾掉。

小白版讲解

一张地图上的坐标

先看一张地图。北京在地图上是一对数字,上海是另一对数字,两个城市离得近不近,你不用看名字,量一下两点的距离就知道。地图厉害的地方在于:它把"地理位置"这件说不清的事,变成了两个可以做减法的数字。你甚至可以问出"离杭州最近的三个城市是哪些"——这在只有城市名单的时候是个难题,在有坐标之后只是一次排序。

embedding 做的是同一件事,只不过它给的不是二维坐标,而是几百上千维的坐标,而且量的不是地理距离,是意思上的远近。"回收站保留多久"和"删掉的文件还能找回来吗"这两句话,字面上只共用一个"回"字,但在这张"意思地图"上它们几乎叠在一起;"回收站保留多久"和"服务器在哪个机房"则隔得很远。把一段文本变成这样一组坐标的过程叫 embedding(嵌入),产出的那一组数字叫向量。

维度为什么要那么多?因为"意思"不是一条线上的位置。一句话同时带着话题、语气、时态、专业领域、是不是疑问句等等许多种属性,二维坐标塞不下。1536 维的意思是:模型用 1536 个数字来描述这句话的各个侧面。这些维度没有名字,第 37 维代表什么,谁也说不上来——它是训练出来的,不是设计出来的。这一点很重要:向量不可解释,你没法像昨天那样把一个分数拆回"哪个词贡献了多少"。这是它相对 BM25 的代价,也是为什么第一天先写 BM25。

还有一件反直觉的事要现在说清楚:向量编码的是"像不像",不是"对不对"。两句意思相反的话——"支持导出为 PDF"和"不支持导出为 PDF"——在向量空间里离得非常近,因为它们谈的是同一件事。指望向量帮你区分肯定与否定,一定会翻车。它负责的是"把话题相关的材料捞出来",判断对错是后面生成那一步的事。

三种距离,和归一化这个前提

有了坐标就要量距离。常用的有三种,pgvector 各给了一个运算符。

欧氏距离<->)是最直观的:两点之间的直线长度。它同时受方向和长度影响——两个向量方向完全一致,但一个长一个短,欧氏距离依然不为零。

内积<#>)是逐维相乘再求和。它同时含着"方向有多一致"和"两个向量各自有多长"两层信息。长度在这里是个麻烦:文本越长,模型输出的向量模长往往越大,用内积排序会系统性地偏向长文档——这跟昨天 BM25 里 b 参数要处理的问题一模一样,只是换了个地方冒出来。

余弦相似度(对应距离运算符 <=>)只看方向:把两个向量都当成从原点出发的箭头,量它们的夹角。夹角越小越相似。它天然不受长度影响,所以是文本检索的默认选择。

三者的关系可以用一句话串起来:把所有向量都做 L2 归一化(把长度压成 1)之后,余弦相似度就等于内积,欧氏距离也变成余弦距离的一个单调函数。原因很简单:余弦相似度的定义是内积除以两个模长的乘积,模长都是 1 时,除数就是 1。这不是数学游戏,是一条能省掉一整类 bug 的工程约定——只要你在 embedding 的出口统一归一化一次,后面用哪个运算符排出来的名次都一样,再也不用担心"这个库默认用的是内积还是余弦"。

similarity.js
export function l2Normalize(vec) {
  const norm = Math.sqrt(vec.reduce((sum, x) => sum + x * x, 0))
  return norm === 0 ? vec : vec.map((x) => x / norm) // 零向量原样返回,别除以 0
}
 
export function dot(a, b) {
  let sum = 0
  for (let i = 0; i < a.length; i += 1) sum += a[i] * b[i]
  return sum
}
 
// 归一化之后,余弦相似度就是内积;余弦距离 = 1 - 相似度,越小越近
export function cosineDistance(a, b) {
  return 1 - dot(a, b)
}

维度不是越高越好

排行榜上的模型动辄 1536 维、3072 维,很容易让人觉得维度越高越准。先算一笔账。

30 篇文档、1536 维、每维 4 字节,一共不到 200 KB,随便存。换成一个百万块的知识库:100 万乘以 1536 乘以 4 字节,大约 6 GB。这 6 GB 不只是磁盘,近似最近邻索引(比如 HNSW)为了快,需要把图结构和向量放进内存,所以它基本等于一台机器的内存预算。维度直接决定了这笔钱。

维度还影响别的两件事。一是计算量:每比较一次就是一轮几千次乘加,维度翻倍,检索延迟大致也翻倍。二是质量,但这一项的收益是递减的:从 256 维加到 512 维通常能看到明显提升,从 1536 加到 3072 往往只换来很小的改善,却付出翻倍的存储和延迟。

好消息是现在的主流模型支持套娃式表示(Matryoshka representation):训练时就把最重要的信息压在靠前的维度上,于是你可以直接把 1536 维的向量截短到 512 维再重新归一化,仍然可用。这就是 text-embedding-3-small 那个 dimensions 参数在做的事——不是另外训练了一个小模型,而是同一个向量截了个头。注意两点:截短必然有损失,损失多少要在你自己的数据上跑评估才知道(这是第 8 天的活);索引里所有向量的维度必须一致,中途改维度等于全库重建。

选模型的四个问题

面对排行榜,逐个问下面四个问题就够了,不必纠结榜单第几名。

第一问:语言。 你的语料是中文、中英混合还是多语种?一个英文榜单第一的模型在中文检索上可能很一般。看榜单要看多语言检索那一栏,不要看总分。

第二问:维度与存储。 就是上一节那笔账。顺带确认这个模型支不支持降维,不支持的话你就被锁死在它的原生维度上了。

第三问:最大输入长度。 每个模型都有 token 上限,超了要么报错要么被静默截断——静默截断是更可怕的那种,因为你会以为整篇都编码了,实际上后半篇根本没进向量。这一条会直接约束第 4 天切块能切多大。

第四问:查询和文档要不要用不同前缀。 这一条最容易漏。有一族开源模型(e5 系列是典型)是拿成对数据训练的,要求给查询加 query: 、给文档加 passage: 这样的前缀。不加前缀它照样返回向量、照样能算距离,只是效果明显变差,而且没有任何报错。用之前一定要去读模型卡片,确认它属不属于这一族。

接口版还是本地版:让它们长一个样

具体到怎么算向量,有两条路。

调接口(本课主线用 OpenAI 的 text-embedding-3-small):不用管模型下载、不用管显卡、批量请求一次能算几十条,工程量几乎为零。代价是每一段文本都要付一次钱,而且首次建库那一次是最贵的——十万块文档就是十万次计费;还有一个常被忽略的问题:内容要出你的网关,合规上过不过得去要先问清楚。

本地跑开源模型(比如 Xenova/multilingual-e5-small,384 维):不联网、不出网关、按次没有边际成本。代价是要自己管模型加载和推理速度,质量通常也比头部接口差一档。

这两条路不该在业务代码里各写一遍。正确做法是定一个接口,让后端可换:

TextText
embed(texts: string[], opts?: { dimensions?: number }): Promise<number[][]>

这个签名会用满十四天,之后每天的 lab 都照抄它。它的三个后端分别是:离线的确定性哈希向量(MOCK=1 时用)、本地开源模型、以及默认的 OpenAI 接口。业务代码只认这个函数,换后端时一行都不用改。

embed.js
// 唯一的后端选择点:业务代码只拿到一个 embed 函数,不知道背后是谁
export function createEmbedBackend() {
  if (process.env.MOCK === '1') {
    return { name: 'mock-hash', dimensions: 384, embed: mockEmbed }
  }
  if (process.env.EMBED_BACKEND === 'local') {
    // e5 一族要求查询与文档带不同前缀,前缀由调用方拼好再传进来
    return { name: 'local:e5-small', dimensions: 384, embed: localEmbed }
  }
  const dimensions = Number(process.env.EMBED_DIM ?? 1536)
  return { name: 'openai:text-embedding-3-small', dimensions, embed: openaiEmbed }
}

离线那个后端值得多说一句。它不是"假装调用成功",而是一套确定性的哈希向量:把文本切成相邻的二元组,每个二元组做一次 FNV-1a 哈希,用哈希值决定往哪一维加、加正还是加负,最后归一化。同一段文本永远得到同一个向量,跨天也一致。它保留的是字面重合度,不是语义——所以离线模式下向量检索的行为很像关键词检索。它验证的是代码路径,不是检索效果,这句话会写在每天实验的 README 里。

pgvector 上手:建表、写入、查最近邻

向量存哪儿?专用向量库有一堆,但如果你的数据本来就在 PostgreSQL 里,pgvector 扩展往往是性价比最高的选择:向量和业务数据在同一个库、同一个事务里,权限过滤直接写 where,不用维护两套存储之间的一致性。第 12 天的记忆存储也用它,30 天课的第 12 天讲的是同一个扩展的另一种用法。

建表就是加一个向量类型的列,维度写死在类型里:

SQLSQL
CREATE EXTENSION IF NOT EXISTS vector;
 
CREATE TABLE chunk_embeddings (
  chunk_id       text PRIMARY KEY REFERENCES chunks(chunk_id) ON DELETE CASCADE,
  embedding      vector(1536) NOT NULL,
  embedding_half halfvec(1536) NOT NULL
);

halfvec 是半精度类型,每维 2 字节而不是 4 字节,占用直接减半。检索质量的损失通常很小——今天的实验会把两列的前三名并排打出来给你看差多少。

查最近邻就是按距离排序取前几名,<=> 是余弦距离:

SQLSQL
SELECT c.chunk_id, c.doc_id, e.embedding <=> $1 AS distance
  FROM chunk_embeddings e
  JOIN chunks c ON c.chunk_id = e.chunk_id
 ORDER BY e.embedding <=> $1, c.chunk_id
 LIMIT 3;

注意 ORDER BY 后面多了个 c.chunk_id:两条向量距离相同时得有个决胜键,否则名次会随扫描顺序变,明天再跑就对不上。

不建索引时这条查询是全表扫描,几万行以内也够快。建索引用 HNSW,今天只要知道一件事——索引的运算符类必须和查询里的运算符配套

SQLSQL
CREATE INDEX ON chunk_embeddings USING hnsw (embedding vector_cosine_ops);

建成 vector_l2_ops 却用 <=> 去查,索引就是白建的,规划器只会全表扫,而且不报任何错。怎么确认?看查询计划:

TextText
默认        ->  Seq Scan on chunk_embeddings
关掉全表扫描 ->  Index Scan using chunk_embeddings_hnsw on chunk_embeddings

只有 30 行数据时,规划器选全表扫描是对的——建了索引不等于一定走索引,行数太少时走索引反而更慢。想确认索引到底能不能用,把 enable_seqscan 关掉再看一遍,能出现 Index Scan 就说明运算符类配对了。索引参数怎么调、召回率会掉多少,是第 5 天的内容,今天只到"建了个索引,能查"。

search.js
import { toSql } from 'pgvector'
 
// 写入:toSql 把数组变成 pgvector 认识的文本,再由 ::vector 转成向量类型
export async function upsert(sql, row) {
  await sql.unsafe(
    `INSERT INTO chunk_embeddings (chunk_id, embedding, embedding_half)
     VALUES ($1, $2::vector, $2::vector::halfvec)
     ON CONFLICT (chunk_id) DO UPDATE SET embedding = EXCLUDED.embedding`,
    [row.chunkId, toSql(row.embedding)],
  )
}
 
export async function nearest(sql, queryVec, topK) {
  return sql.unsafe(
    `SELECT chunk_id, embedding <=> $1::vector AS distance
       FROM chunk_embeddings
      ORDER BY embedding <=> $1::vector, chunk_id
      LIMIT ${topK}`,
    [toSql(queryVec)],
  )
}

今天到这里,你手上有了两套互不相同的检索:一套只认字面,一套只认意思。实验里会看到它们各自失手的地方——问"限流超了返回 429 吗",BM25 稳稳命中那篇写着 429 的文档,向量却把话题相近但没提 429 的产品手册排到了前面。两套都不完美,而它们的错法不一样,这正是第 9 天要把两路合起来的理由。

源码导读

动手实验

🧪 D2 实验:把一批文本向量化后存入 pgvector 并按余弦距离查最近邻的可复跑脚本

代码位置:labs/rag-14days/day-02-pgvector-nearest-neighbor

验收标准:

  1. 离线跑通后能打印出 4 个问题各自的 BM25 前三名与向量前三名,两栏并排,向量那栏的距离互不相同。
  2. v01(单个文件最大能上传多大)两路的前两名都是 doc-024doc-006——离线哈希向量保留字面重合度,所以跟 BM25 高度一致。
  3. v03(限流超了返回 429 吗)BM25 第一名是 doc-010 且分数远高于第二名,向量这一路却把 doc-006 排到了第一——精确匹配是向量的弱项。
  4. 全精度与半精度那一段:半精度占用是全精度的一半左右,前三名顺序完全一致,距离偏差在小数点后第五位以后。
  5. 起了 docker compose 之后再跑一遍,能看到查询计划的两组输出:默认是全表扫描,关掉 enable_seqscan 之后是走 HNSW 索引的扫描。

实验目录里的 corpus/ 是第 1 天那份语料的完整副本,问题也跟昨天的基线问题有重合,方便你直接对比两种检索的名次。starter/ 里挖了四个练习点:哈希向量、L2 归一化、余弦距离、半精度量化。原样跑起来你会看到所有文档的距离都是 0.0000、前三名永远是 doc-001doc-003——那是因为距离函数还没实现,所有文档"一样近"。没有 API key、没有 Docker 也能全程离线做完。

  1. 先只跑 MOCK=1 pnpm start,看清楚 starter 的初始现象:BM25 那一栏是正常的,向量那一栏所有距离都是 0。
  2. 补完哈希向量与 L2 归一化,再跑一次,看到向量那一栏的距离开始互不相同,前三名跟 BM25 大体对得上。
  3. 补完余弦距离与半精度量化,确认半精度的占用是全精度的一半、名次不变、距离只在小数点后第五位以后有偏差。
  4. 在 lab 根目录 docker compose up -d 起一个 pgvector,配上 DATABASE_URL 重跑,验证换成真数据库之后名次与内存版一致。
  5. 看最后那段查询计划:默认走全表扫描,SET enable_seqscan = off 之后走 HNSW 索引,想清楚为什么 30 行的表选全表扫描是对的。

面试题

今天 4 道题在下方题库区,侧重向量相似度的数学前提、维度与召回质量的关系、embedding 模型选型的取舍。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。

检查清单与明日预告

  • 能解释 embedding 把什么变成了坐标,并说明为什么余弦相似度在归一化之后等价于内积
  • 能列出选 embedding 模型时必须看的四项:多语言能力、维度与存储成本、最大输入长度、是否需要区分查询与文档前缀
  • 能用 pgvector 建表、写入向量、建索引并查出最近邻,且看得懂查询计划里有没有走索引
  • 能说清为什么向量分不清"支持"和"不支持",以及这件事该由哪一步来兜
  • 实验的 5 条验收标准全部通过,真数据库那一遍也跑过
  • 4 道面试题不看要点也能答出至少 3 道

明天(D3)我们退回到更上游的一步:文档是怎么进来的。今天我们直接拿一份干净的 Markdown 语料算向量,现实里进来的是 PDF、网页和扫描件——多栏排版会让文字顺序错乱、表格会塌成一行、页眉页脚会混进正文。解析质量是检索质量的天花板:一段在解析时就错乱的文字,向量算得再准也没用。先把今天这条链路跑通、知道下游要什么,明天才知道解析该保留哪些东西。

面试题库

  • 余弦相似度和内积什么时候等价?如果向量没有归一化,用内积排序会出什么问题?When are cosine similarity and inner product equivalent? What goes wrong if you rank by inner product on vectors that are not normalised?
    国内高频海外高频基础#embeddings#similarity#normalisation

    分析过程 · 先想清楚再作答

    1. 这题是送分题,但区分度藏在后半句。只答「归一化之后两者等价」的人很多,面试官真正想听的是「没归一化会怎么坏」,因为那是线上真的会发生的事。
    2. 先把定义摆出来:余弦相似度等于内积除以两个向量模长的乘积。模长都是 1 时除数就是 1,所以余弦相似度就是内积——这一句话就是等价的全部理由,不需要额外的假设。
    3. 再说没归一化的后果:内积里混着「方向有多一致」和「向量有多长」两层信息。文本越长,模型输出的向量模长往往越大,于是排序会系统性地偏向长文档——这跟 BM25 里 b 参数要压的是同一个毛病,只是换了个地方冒出来。
    4. 点出这类 bug 的性质:它不报错。程序照常跑、结果照常出,只是名次悄悄偏了,你要跑一轮离线评估才可能发现。所以工程上的做法是在 embedding 的出口统一归一化一次,而不是靠每个调用点自觉。
    5. 补一句欧氏距离:向量都归一化之后,欧氏距离的平方等于 2 减去 2 倍内积,也就是余弦距离的单调函数,三种距离排出来的名次完全一致。这一句能说明你理解的是关系而不是三条并列的规则。
    6. 可预期的追问:那 pgvector 里该用哪个运算符?答案是既然已经归一化,`<=>`(余弦距离)和 `<#>`(负内积)名次一样,选 `<=>` 的理由是可读性和「就算哪天有人漏了归一化也不至于错」。

    How to reason about it · think before answering

    1. This starts as a giveaway, but the second half is where candidates separate. Many can say 'they are equivalent after normalisation'; few can describe what breaks without it.
    2. State the definition: cosine similarity is the inner product divided by the product of the two magnitudes. When both magnitudes are 1, the divisor is 1 and cosine reduces to the inner product. That is the whole argument.
    3. Then the failure mode: an un-normalised inner product mixes 'how aligned' with 'how long'. Longer texts tend to produce larger-magnitude vectors, so ranking drifts systematically toward long documents, the same bias BM25's b parameter exists to counter.
    4. Stress that this bug is silent. Nothing throws, results still look plausible, and only an offline evaluation reveals the drift. Hence the engineering rule: normalise once at the embedding boundary, never at each call site.
    5. Add Euclidean distance for completeness: on normalised vectors, squared L2 equals 2 minus twice the inner product, a monotone function of cosine distance, so all three metrics produce the same ranking.
    6. Expected follow-up: which pgvector operator should you use? Since the vectors are normalised, `<=>` and `<#>` rank identically; prefer `<=>` for readability and because it stays correct if someone later forgets to normalise.

    答题要点

    • 余弦相似度 = 内积 / 两个模长之积,模长为 1 时除数为 1,两者等价。
    • 没归一化时内积混入模长信息,长文档的向量模长普遍更大,排序会系统性偏向长文档。
    • 这类错误不报错,只能靠离线评估发现,所以要在 embed 出口统一归一化。
    • 归一化之后欧氏距离与余弦距离互为单调函数,三种运算符名次一致。
    • pgvector 里对应 `<->`(L2)、`<#>`(负内积)、`<=>`(余弦距离)三个运算符。

    Key points

    • Cosine equals inner product divided by both magnitudes; with unit magnitudes the divisor is 1, so they coincide.
    • Without normalisation the inner product carries magnitude, and longer documents usually have larger magnitudes, biasing the ranking.
    • The failure is silent, so normalise once at the embedding boundary and verify with offline evaluation.
    • On normalised vectors L2 and cosine are monotonically related, so all operators rank the same.
    • In pgvector the operators are `<->` for L2, `<#>` for negative inner product and `<=>` for cosine distance.
  • 把 embedding 维度从 1536 降到 512,你会损失什么?什么场景下这个损失可以接受?What do you lose when you cut embedding dimensions from 1536 to 512, and when is that loss acceptable?
    国内高频海外高频进阶#embeddings#dimensions#cost

    分析过程 · 先想清楚再作答

    1. 这题考的是你会不会算账。只说「维度越低越省、精度越低」的答案没有区分度,面试官在等一个具体的成本模型和一个决策顺序。
    2. 先把三笔账列出来:存储与内存(向量数量乘维度乘每维字节数,近似最近邻索引要把它放进内存,所以基本等于机器预算)、检索延迟(每次比较就是一轮乘加,维度大致线性影响耗时)、检索质量(收益递减,低维段每加一档提升明显,高维段加倍只换来很小的改善)。
    3. 再说清降维为什么可行:主流模型用套娃式表示训练,重要信息压在靠前的维度上,所以直接截短再归一化仍然可用,这不是另训了一个小模型。截短必然有损失,损失多少只能在自己的数据上跑评估才知道。
    4. 给出决策顺序:先按存储与内存预算倒推一个维度上限,再从上限往下试两三档,看指标掉多少,掉得能接受就用低的。反过来「先选最高维再想办法省钱」基本都会返工。
    5. 点出可接受的典型场景:库很大而单条价值不高(比如日志、工单)、召回之后还有重排兜底(重排能把粗排的损失补回来一部分)、或者对延迟极敏感的在线场景。反过来法务、医疗这类一条都不能漏的场景就要谨慎。
    6. 可预期的追问:能不能不同文档用不同维度?不能——同一个索引里所有向量必须同维,改维度等于全库重建,这跟换模型是同一类迁移成本。

    How to reason about it · think before answering

    1. This is a cost-modelling question. 'Lower dimensions are cheaper but less accurate' earns nothing; the interviewer wants a cost model and a decision order.
    2. Lay out three costs: storage and memory (vector count times dimensions times bytes per dimension, which an ANN index must hold in RAM), query latency (roughly linear in dimensions), and retrieval quality, whose returns diminish sharply at the high end.
    3. Explain why truncation works at all: models trained with Matryoshka representations pack the most important information into the leading dimensions, so truncating and re-normalising keeps the vector usable. It is still lossy, and how lossy is an empirical question on your own data.
    4. Give the decision order: derive a dimension ceiling from your memory budget, then step down two or three notches and measure the metric drop. Choosing the largest model first and optimising cost later usually means redoing the work.
    5. Name the acceptable cases: large corpora of low individual value, pipelines where a reranker recovers some of the loss, and latency-critical online paths. Be conservative where a single miss is expensive, such as legal or clinical retrieval.
    6. Expected follow-up: can different documents use different dimensions? No. Every vector in an index must share one dimension, so changing it means rebuilding the whole index, the same migration cost as changing models.

    答题要点

    • 三笔账:存储与索引内存、检索延迟、检索质量,前两笔随维度近似线性,第三笔收益递减。
    • 套娃式表示让截短再归一化仍然可用,但一定有损失,损失多少要在自己的数据上评估。
    • 决策顺序是先按内存预算定上限,再往下试档位看指标掉多少。
    • 库大、单条价值低、后面还有重排兜底、对延迟敏感的场景,降维划算。
    • 同一索引里维度必须一致,改维度等于全库重建。

    Key points

    • Three costs: storage and index memory, query latency, and retrieval quality; the first two scale with dimensions, the third has diminishing returns.
    • Matryoshka representations make truncation viable, but it is lossy and the loss must be measured on your own data.
    • Decide by deriving a ceiling from the memory budget, then stepping down and measuring.
    • Truncation pays off for large corpora, low-value items, latency-sensitive paths, and pipelines with a reranker.
    • All vectors in one index share a dimension, so changing it forces a full rebuild.
  • 为什么有些 embedding 模型要求查询和文档加不同的前缀?不加会怎样,你怎么在上线前发现这个问题?Why do some embedding models require different prefixes for queries and documents? What happens if you skip them, and how would you catch it before shipping?
    国内高频海外高频进阶#embeddings#model-selection#evaluation

    分析过程 · 先想清楚再作答

    1. 这题的题眼是「静默失效」。会背「e5 要加 query 和 passage 前缀」只能拿基础分,能说清它为什么不报错、以及怎么在上线前抓住它,才是做过的人。
    2. 先讲原因:这一族模型是拿成对数据训练的,一侧是短问句、一侧是长段落,两者的分布本来就不一样。前缀是训练时给模型的角色标记,告诉它这一段该按查询编码还是按文档编码。推理时不给,模型就落在了训练分布之外。
    3. 再讲后果的性质:不加前缀模型照样输出向量、照样能算距离、名次照样有先后,只是整体质量下滑。**没有任何报错**——这跟忘了归一化是同一类问题:错误不会自己浮出来。
    4. 怎么发现:唯一可靠的办法是一小份标注问题集,用同一批文档跑两遍(加前缀与不加前缀),比命中率。这就是第 8 天要做的评估闸门,它的价值恰恰在于抓这类静默错误。上线前跑一遍,比读十遍文档管用。
    5. 补一个更容易踩的变体:**建库时加了前缀、查询时忘了加**,或者两边加成同一个前缀。这种情况下所有向量都在同一个坐标系里,看起来更「正常」,但查询与文档的对齐关系是错的,掉分同样查不出来。所以前缀应该封装在 embed 的调用约定里,而不是散在各处手拼。
    6. 可预期的追问:OpenAI 的模型要不要加前缀?不需要——它不属于这一族。所以这不是一条普遍规则,而是**每换一个模型都要重新读模型卡片确认**的事。

    How to reason about it · think before answering

    1. The core of this question is silent failure. Reciting 'e5 needs query: and passage: prefixes' is the baseline; explaining why nothing errors out and how you would catch it is what shows experience.
    2. The reason: these models are trained on pairs, short questions on one side and longer passages on the other, two genuinely different distributions. The prefix is a role marker learned during training. Omit it at inference and you are off-distribution.
    3. The consequence: the model still returns vectors, distances still compute, results still have an order, quality just degrades. Nothing throws, exactly like forgetting to normalise.
    4. How to catch it: run a small labelled question set against the same corpus twice, with and without prefixes, and compare hit rate. That is the evaluation gate built on day 8, and catching silent regressions is precisely what it is for.
    5. Mention the sneakier variant: prefixing at index time but not at query time, or using the same prefix on both sides. Everything sits in one coordinate space and looks healthier, yet the query-document alignment is wrong and the loss is just as invisible. Encapsulate prefixes in the embedding call convention rather than hand-writing them everywhere.
    6. Expected follow-up: do OpenAI models need prefixes? No, they are not in that family, so this is not a universal rule but a per-model detail you re-check on the model card every time you switch.

    答题要点

    • 这类模型用问句与段落的成对数据训练,前缀是区分两种角色的标记,缺了就落在训练分布之外。
    • 不加前缀不会报错,只会整体掉分,属于静默失效。
    • 唯一可靠的发现方式是拿一份标注问题集跑 A/B 对比命中率。
    • 更隐蔽的错法是两边前缀不一致或用了同一个前缀,看起来更正常但对齐是错的。
    • 前缀应封装在 embed 的调用约定里;换模型必须重读模型卡片,它不是普遍规则。

    Key points

    • These models are trained on question-passage pairs; the prefix marks which role a text plays, and omitting it puts you off-distribution.
    • Skipping prefixes never errors, it only degrades quality, so the failure is silent.
    • The reliable detection is an A/B run over a small labelled question set, comparing hit rate.
    • A subtler bug is mismatched or identical prefixes on both sides, which looks healthier but misaligns queries and documents.
    • Keep prefixes inside the embedding call convention, and re-read the model card whenever you switch models.
  • 向量检索能完全取代关键词检索吗?举一个向量必然失手的查询,并说说你会怎么补。Can vector search fully replace keyword search? Give a query where vectors are bound to fail, and say how you would fix it.
    国内高频海外高频进阶#hybrid-search#embeddings#retrieval-failure

    分析过程 · 先想清楚再作答

    1. 这题是典型的「立场题」,答「能」或「不能」都不重要,重要的是你能不能举出一个具体到能复现的反例。举不出例子,前面说得再漂亮也会被判成没做过。
    2. 先给失手的类型,一次给全:错误码与状态码(429、E1032)、版本号与型号(v2.3.1、X20 Pro)、人名与工号、订单号与文档编号、以及否定表达。前四类的共同点是**这些词的价值在于字面唯一,而向量只保留语义邻近**,模型会把 429 和「限流」「超时」这些话题相近的东西编到一起,反而把真正写着 429 的那篇挤下去。
    3. 拿一个能复现的例子说:问「限流超了返回 429 吗」,BM25 稳稳命中写着 429 的接口文档,向量却可能把话题相近但没提 429 的产品手册排在前面。这个现象在本课第 2 天的实验里就能亲眼看到。
    4. 否定表达要单独强调:「支持导出 PDF」和「不支持导出 PDF」在向量空间里几乎重合,因为它们谈的是同一件事。指望向量区分肯定与否定一定翻车,这一层要靠生成侧读原文来判断。
    5. 怎么补:两路并行跑再融合,关键词一路用 BM25、向量一路用最近邻,用倒数排名融合把两个名次合成一个。这就是混合检索,本课第 9 天展开。要点是**两套的错法不一样**,所以合起来才有增益——如果两套错在同一批查询上,融合是白做的。
    6. 可预期的追问:那关键词一路能不能扔掉、改成让模型改写查询?可以缓解一部分(第 10 天的查询改写),但改写救不了字面唯一的标识符——你没法把 429 改写成别的说法。

    How to reason about it · think before answering

    1. This is a stance question where the stance matters less than the counter-example. Without a concrete, reproducible failing query, the rest of the answer reads as theory.
    2. Enumerate the failure classes up front: error and status codes, version numbers and SKUs, names and employee IDs, order or document identifiers, and negation. The first four share one property: their value lies in exact literal identity, which embeddings deliberately blur into semantic neighbourhoods.
    3. Give a reproducible example: ask whether rate limiting returns 429. BM25 lands on the API document that literally contains 429, while vector search may rank a topically similar product manual that never mentions the code.
    4. Call out negation separately: 'supports PDF export' and 'does not support PDF export' sit almost on top of each other because they discuss the same thing. Vectors cannot carry that distinction; the generation step reading the source has to.
    5. The fix: run both retrievers and fuse the rankings, BM25 on the lexical side and nearest neighbour on the vector side, combined with reciprocal rank fusion. That is hybrid search, covered on day 9. Fusion helps precisely because the two systems fail on different queries.
    6. Expected follow-up: could you drop the keyword path and rewrite queries instead? Rewriting helps with vocabulary mismatch, but it cannot rescue exact identifiers, since there is no paraphrase of 429.

    答题要点

    • 不能取代:错误码、版本号、人名、单号这类词的价值在于字面唯一,向量只保留语义邻近。
    • 具体反例:问「限流超了返回 429 吗」,BM25 命中写着 429 的文档,向量把话题相近却没提 429 的文档排前面。
    • 否定表达是另一类失手:肯定句与否定句在向量空间里几乎重合。
    • 补法是混合检索:两路并行再用倒数排名融合合并名次。
    • 融合有增益的前提是两套的错法不同;查询改写能缓解词汇不匹配,但救不了字面唯一的标识符。

    Key points

    • No: codes, version numbers, names and IDs matter as exact literals, which embeddings blur into neighbourhoods.
    • Concrete example: asking whether rate limiting returns 429, where BM25 hits the document containing 429 and vectors surface a topically similar one that never mentions it.
    • Negation is a second failure class, since affirmative and negative statements sit almost on top of each other.
    • The remedy is hybrid retrieval: run both paths and merge with reciprocal rank fusion.
    • Fusion pays off because the two paths fail differently; query rewriting helps vocabulary mismatch but not exact identifiers.

评论

登录后即可参与讨论

还没有评论,来说第一句。