目的
本文目标是通过几个简单实验, 对向量搜索时 “计算文档相似度的细节” 建立初步印象, 以便更好使用 Obsidian 里这些知识库插件, 并部分解答如下问题:
- 向量搜索跟关键词搜索区别在哪?
- 向量搜索以相似度匹配文档, 有啥不符合直觉的地方?
- 啥样的知识库适合利用向量搜索?
本文的嵌入模型选 BGE-M3 (只讨论稠密检索的部分), 理由:支持中文, max-token 较富裕, 国内平台有免费资源, 本地也跑得动
相似度计算方式选 余弦相似度, 理由:Obsidian 一些知识库插件 smart connections smart composer 等都用这个, 而 copilot 是用向量数据库 Orama 的文本加向量混合搜索, 其中 find similar 也是这个
别的技术细节不说了, 我目标也没多严肃, 只为熟悉一下向量搜索的脾性, 解决一部分疑惑例如:这么写查询语句到底管用吗? 怎么优化写法才能更好的召回目标文档?
向量搜索的词义理解能力
据说向量搜索是能看懂 “句子含义” 的, 来简单验证一下
翻译后的关键词
我的第一个问题是, 向量搜索英文的豆腐 (Tofu) 可以找到豆腐吗?
# 查询语句
豆腐
Tofu
# 文档片段
豆腐
土豆
西红柿
黑曜石
Obsidian
| | 豆腐 | 土豆 | 西红柿 | 黑曜石 | Obsidian |
|:-----|-------:|-------:|---------:|---------:|-----------:|
| 豆腐 | 1 | 0.604 | 0.477 | 0.450 | 0.321 |
| Tofu | 0.549 | 0.460 | 0.439 | 0.336 | 0.408 |
第一个例子也顺便演示一下表格结构: 本文中每个表格由三部分组成:
- 最左面一列, 是一些查询语句 (
query_text
) - 最上面一行, 是几个文档片段 (
doc_chunk
)- 实际知识库里会有上万个文档片段, 这里拿人工造的几个片段演示相似度的计算结果
- 每个单元格里的数字, 就是这一对
查询语句~文档片段
的余弦相似度
顺便解释下大部分基础 RAG 工具背后做的事就是, 把查询语句跟知识库里所有文档片段算一下相似度, 然后把最接近的 topN 片段摘出来, 拼一段话发给聊天模型, 之后 AI 回复时就能利用这些本地资料了. 很容易想到其中关键就是 需要让 “查询语句” 和 “跟查询目的匹配的文档片段” 计算出尽量高的相似度, 这也是本文讨论的主要内容
解释一下这表格, 先看第一行 豆腐
跟自身相似度为 1 (这是必然的) 之后 豆腐~土豆=0.604
豆腐~西红柿=0.477
再往后相似度递减, 基本符合逻辑
第二行是我真正关心的, 其中 Tofu~豆腐=0.549
看着不太高, 但这仍然比相同一行其余几个相似度高, 意味着当文档库里只存在这几个片段时, 输入英文的 Tofu
仍然会最优先匹配到 豆腐
, 通常这就是用户想要的效果
别小看这例子, 一个看不懂中文也看不懂英文的人可能会觉得 Tofu~Obsidian
的相似度高于 Tofu~豆腐
, 而向量搜索是大致知道词义的, 它不是纯靠字符来统计相似度
换一个例子, 看看拿中文关键词去匹配英文文档的情况
# 查询语句
cherry
pick
cherry-pick
pick-cherry
樱桃
西瓜
挑樱桃
挑西瓜
挑灯
挑
# 文档片段
解释 "cherry-picking": 通常指从一组事物或人中挑选出最好的或最有利的。例如,选择对自己有利的数据或证据,而忽略其他不利的信息。在某些情况下可以表示“精选”或“精挑细选”,更常见的是用作贬义词,指选择性地采用对自己有利的信息或数据,而忽略其他不利的证据
| | 解释 cherry-picking... |
|:------------|-----------------------:|
| cherry | 0.511 |
| pick | 0.461 |
| cherry-pick | 0.657 |
| pick-cherry | 0.576 |
| 樱桃 | 0.332 |
| 西瓜 | 0.327 |
| 挑樱桃 | 0.533 |
| 挑西瓜 | 0.535 |
| 挑灯 | 0.439 |
| 挑 | 0.557 |
这次文档片段只有一句, 是 "cherry-picking" 的解释...
而查询词有很多个, 目的是看哪种查询词能更好的搜到文档
结果是好几个查询词都能对目标文档算出较高的相似度, 顺序为 cherry-pick > pick-cherry > 挑 > 挑西瓜 ≈ 挑樱桃 > cherry > pick ...
有意思的是 挑西瓜 ≈ 挑樱桃 > 挑灯
, 在查询词里提到无关联的 灯
会降低相似度
总结:
- 向量搜索能实现一定程度的 “词义理解”, 而不只是在字面上做近似匹配
- 向量搜索算出的相似度是比较 “模糊” 的, 大致看上去有规律, 但细节未必符合设想
备注:
只计算对一个文档片段的相似度, 结果可靠吗?文档库里又不止这一个片段
确实应该考虑这个情况, 其实这个例子的完整版如下:
| | 解释 cherry-picking... | 总计101chunk里的top20命中 |
|:------------|-----------------------:|:---------------------------------------------|
| cherry | 0.511 | .+.................. max(0.520) ~ min(0.367) |
| pick | 0.461 | .....+.............. max(0.499) ~ min(0.410) |
| cherry-pick | 0.657 | +................... max(0.657) ~ min(0.421) |
| pick-cherry | 0.576 | +................... max(0.576) ~ min(0.371) |
| 樱桃 | 0.332 | .................+.. max(0.402) ~ min(0.328) |
| 西瓜 | 0.327 | .................... max(0.441) ~ min(0.341) |
| 挑樱桃 | 0.533 | +................... max(0.533) ~ min(0.357) |
| 挑西瓜 | 0.535 | +................... max(0.535) ~ min(0.374) |
| 挑灯 | 0.439 | .+.................. max(0.455) ~ min(0.369) |
| 挑 | 0.557 | +................... max(0.557) ~ min(0.419) |
解释下那个 总计101chunk里top20
一栏是啥玩意:做相似计算时, 文档片段不只写出来的这些, 每个查询词还会跟 “随机 100 个对照组片段” 计算相似度, 因为光看局部相似数值没用, 得从文档库全局的角度确保 查询词~目标文档
真是相似度最好的
这里正面典型是 cherry-pick
挑樱桃
等几个词, 在全部 101 个片段里它们仍跟 “目标文档片段” (即 解释 cherry-picking...
这句话) 相似度最高;反面典型就是第五行 樱桃
, 它跟目标片段相似度 0.332, 这成绩如何呢?在总计 101 个片段里排第 18 名, 就是说以 樱桃
搜整个文档库试图找到目标文档, 有 17 段不相关的话排它前面
备注 2:
还有个细节是仅凭一个 挑
字居然能比 挑樱桃
的匹配度还好, 在这场景里其实并不符合我的意图
但我们把这一行也记录上, 本文尽量不去 “挑樱桃”
描述式的关键词
来看个更难, 更实际些的
这个问题是, 向量搜索能理解古文吗? 比如我想搜一下写侠客的那句诗, 具体是啥忘了, 能否用翻译版诗歌对应到原诗句呢?
# 查询语句
诗里写着 燕赵之地的侠客,头上系着没有纹饰的冠带,腰间佩带的吴钩宝剑如霜雪一样明亮 是哪句?
诗里写着 侠客头系冠带,腰佩宝剑 是哪句?
诗里写着 侠客头系冠带,腰佩宝剑, 骑马飞奔 是哪句?
诗里写着 Knight rush in nomad gear, Great sword bright as frost 是哪句?
诗里写着 Zhao Ke Man Hu Ying Wu Gou Shuang Xue Ming 是哪句?
# 文档片段
赵客缦胡缨,吴钩霜雪明。银鞍照白马,飒沓如流星。
十步杀一人,千里不留行。事了拂衣去,深藏身与名。
闲过信陵饮,脱剑膝前横。将炙啖朱亥,持觞劝侯嬴。
三杯吐然诺,五岳倒为轻。眼花耳热后,意气素霓生。
救赵挥金槌,邯郸先震惊。千秋二壮士,烜赫大梁城。
纵死侠骨香,不惭世上英。谁能书阁下,白首太玄经。
| | 赵客...四句 | 十步...四句 | 闲过...四句 | 三杯...四句 | 救赵...四句 | 纵死...四句 |
|:--------------------------|------------:|------------:|------------:|------------:|------------:|------------:|
| 燕赵之地的侠客... (直译) | 0.670 | 0.481 | 0.500 | 0.429 | 0.519 | 0.542 |
| 侠客头系冠带... (描述) | 0.545 | 0.514 | 0.451 | 0.453 | 0.487 | 0.571 |
| 侠客头系...骑马... (完善) | 0.588 | 0.543 | 0.467 | 0.465 | 0.499 | 0.558 |
| Knight rush ... (英译) | 0.544 | 0.495 | 0.445 | 0.433 | 0.480 | 0.432 |
| Zhao Ke ... (拼音) | 0.502 | 0.498 | 0.457 | 0.470 | 0.433 | 0.481 |
效果还不错, 这里每四句诗分在一个文档片段 (方便演示, 实际切分情况不一定如此), 换了五种问法, 目的都是希望能优先匹配到 “赵客…四句”, 逐行看一下:
第一行 的问法是拿古文直译版去找原诗, 看看能否 “用句子大意匹配到目标文本”, 效果非常好
第二行 的问法, 拿简短描述去找原诗, 诗里写着 侠客头系冠带,腰佩宝剑
这描述是我特意凑的, 删掉了几乎所有跟原诗相同的字, 模仿一次 “记性不好运气也不好” 的搜索, 最后得到 0.545 的相似度, 只是亚军 (在真正 RAG 系统里一般选召回 top 10 片段, 不得冠军也没事, 但这里目标就是研究怎么 “调整搜索词” 让它跟 “预期文档” 算出最高相似度)
多说一句, 这时冠军是 “纵死侠骨香… 四句”, 事实上从查询词里删掉 “侠” 字再搜, 就能交换一二名位置, 可这样改造查询语句过于刻意了, 前面说过, 尽量不挑樱桃
第三行 的问法, 是第二行的改进, 增加对马的描述, 这次成功了, 目标诗句获得了最高相似度
第四行 的问法, 诗里写着 Knight rush ...
依然找到了原句, 我觉得是个惊喜
这是 古诗词 => 现代中文 => 英语
间接翻译, 比一般的中英互译场景要难, 日常语境里很少把 客 翻译成 Knight, 但向量检索仍然看出这俩句子最相似, 事实上这例子如果仔细拆分, 会看到 bright
frost
对相似度的贡献较明显, 同时前面的 Knight rush ... Great sword
也对相似度有些帮助
第五行 用拼音来问, 基本跟别的文本没啥区分度, 有点遗憾
总结:
- 向量搜索能实现以大量语句去匹配一小段目标文本, 某些场景时这很有用
- 向量搜索能把白话文的, 英文的, 古文的对同一内容的描写, 联系在一起
- 向量搜索能容忍关键词有少许偏差
- 关键词要尽量独特, 要能代表目标文本的特点, 要多携带跟目标文本相关的要素, 少带噪音文本里的要素
备注:英文翻译是从网上搜的 Xia Ke Xing Knighted Verse 侠客行 Translation
关键词上下文对向量搜索的影响
上面是比较基础的功能演示, 就是介绍向量检索能做啥
现在研究对同样的搜索意图, 在关键词周围写不同文字, 对相似度计算有啥有影响, 讨论两方面:
- 查询语句的表达方式有何影响
- 文档片段的内容特征 (文档自身所有, 一般无法控制) 有何影响
查询语句的表达方式
讨论这话题是因为, 对知识库提问时, 查询关键词可能在完整一句话里, 想看看查询关键词的位置是否对于召回有影响?
假设红楼梦有些细节记不住了, 宝玉幻境看到的黛玉宝钗的判词是啥来着? 比方说我只记得有 “提到柳絮 提到玉带” 了
- 问法 1: “柳絮 树上玉带” 需要你帮我找到这句… (重点放开头)
- 问法 2: 需要你帮我找到… “柳絮 树上玉带” (重点放最后)
- 问法 3: 同上, 但把 “柳絮 树上玉带” 放在中间
- 问法 4: 只说 “柳絮 树上玉带”
上面四个查询语句中, 哪个能更好的找到目标?
# 查询语句
柳絮 树上玉带 需要你帮我找到这句诗 大概是模糊暗示了两人的结局的 评价钗黛的 具体的记不清了 那几句具体怎么写的来着 给我找找
需要你帮我找到这句诗 大概是模糊暗示了两人的结局的 评价钗黛的 具体的记不清了 那几句具体怎么写的来着 给我找找 柳絮 树上玉带
需要你帮我找到这句诗 大概是模糊暗示了两人的结局的 评价钗黛的 柳絮 树上玉带 具体的记不清了 那几句具体怎么写的来着 给我找找
柳絮 树上玉带
# 文档片段
只见头一页上便画着两株枯木,木上悬着一围玉带,又有一堆雪,雪下一股金簪。也有四句言词,道是:可叹停机德,堪怜咏絮才。玉带林中挂,金簪雪里埋。
宝玉看了仍不解。待要问时,情知他必不肯泄漏,待要丢下,又不舍。遂又往后看时,只见画着一张弓,弓上挂着香橼。也有一首歌词云:二十年来辨是非,榴花开处照宫闱。三春争及初春景,虎兕相逢大梦归。
后面又画着两人放风筝,一片大海,一只大船,船中有一女子掩面泣涕之状。也有四句写云:才自精明志自高,生于末世运偏消。清明涕送江边望,千里东风一梦遥。
后面又画几缕飞云,一湾逝水。其词曰:富贵又何为,襁褓之间父母违。展眼吊斜晖,湘江水逝楚云飞。
| | 钗黛判词 | 元春判词 | 探春判词 | 湘云判词 |
|:---------------------------|-----------:|-----------:|-----------:|-----------:|
| 柳絮 树上玉带 (重点放开头) | 0.666 | 0.651 | 0.582 | 0.567 |
| (重点放最后) 柳絮 树上玉带 | 0.666 | 0.652 | 0.577 | 0.570 |
| 重点放在中间 | 0.655 | 0.641 | 0.578 | 0.566 |
| 柳絮 树上玉带 (无废话) | 0.660 | 0.545 | 0.522 | 0.479 |
这里几个细节
第一, 我搜的是 树上玉带
其实这段文本里没有 “树” 字, 向量搜索的一个好处就是, 哪怕目标文本里一根草都没,向量搜索依然尝试返回 “尽可能相似的结果”;而传统的关键字搜索只要记错一个文本细节, 想精确匹配 “玉带+树” 关键字, 完了, 你不可能搜得到这段话了
第二, 关键词放在开头中间结尾似乎区别不大 (后再详细讨论)
第三, 不会因为你写上很多提示词, 向量搜索就更努力匹配, 实际看到四种查询语句对目标文档片段的相似度差不多
那么回到重点, 在这例子里最好的询问方式是哪种呢?我觉得 柳絮 树上玉带 (无废话)
的召回效果是最好的, 因为前三个废话版, 看起来跟目标文本的匹配度高, 但跟另几段噪音文本的匹配度也高, 反而不利于区分出目标片段
结论:
- 当语句较短时, 不必特别看重 “提到关键词的顺序”, 写前面后面都差不多
- 向量搜索时, 取得与目标文档的 “绝对的高相似度” 数值没啥用
- 而要从整个文档库角度考虑 “相对的高相似度” (不需要跑的比熊快, 只要跑的比队友快就行)
- 跟 “匹配目标文本” 的意图无关的文字, 尽量别写太多
- 这也是 “比较朴素的 RAG 系统” 的一个困境:召回片段不是目的, 召回后拿 LLM 生成回答才是目的, “拿用户查询语句直接跟文档算相似” 会导致 “查询语句里对 LLM 提示词的部分” 干扰相似度计算
以上下文消歧义
换另一个场景, 在遇到同样字面不同含义的词时, 如何消除关键词歧义呢?比方说, 文档片段里有两种 苹果
:
# 查询
苹果
# 文档片段
苹果的总部位于美国加州的库比蒂诺,与亚马逊、谷歌、微软、Meta并行为五大科技巨擘。目前的业务包括设计、研发、手机通信和销售消费电子、计算机软件、在线服务和个人计算机
苹果是蔷薇科落叶乔木,在世界上广泛种植。果实又称柰或林檎,一般呈红色,但需视品种而定,富含矿物质和维生素
| | 总部位于加州... | 是蔷薇科落叶乔木... |
|:---------------|----------------:|--------------------:|
| 苹果 | 0.671 | 0.590 |
可见脱离上下文时, 向量模型倾向于认为苹果跟 “Cook” 有关, 而不是跟 “cook” 有关
如果就想要按斤卖的那种苹果, 该怎么搜?
# 查询
苹果
苹果, 可以吃的
苹果, 可以吃的, 树上结的, 营养丰富
苹果, 做操作系统的
苹果, 做操作系统的, 商业巨头, 卖 iPhone iPad
# 文档片段
苹果的总部位于美国加州的库比蒂诺,与亚马逊、谷歌、微软、Meta并行为五大科技巨擘。目前的业务包括设计、研发、手机通信和销售消费电子、计算机软件、在线服务和个人计算机
苹果是蔷薇科落叶乔木,在世界上广泛种植。果实又称柰或林檎,一般呈红色,但需视品种而定,富含矿物质和维生素
| | 总部位于加州... | 是蔷薇科落叶乔木... |
|:-----------------------------|----------------:|--------------------:|
| 苹果 | 0.671 | 0.590 |
| 苹果, 可以吃的 | 0.553 | 0.644 |
| 苹果, 可以吃的, 树上结... | 0.510 | 0.715 |
| 苹果, 做操作系统的 | 0.649 | 0.542 |
| 苹果, 做操作系统的, 商业巨头 | 0.689 | 0.541 |
办法就是 把关键词放在环境里
提几个跟苹果的含义有关联的词, 即可区分两种版本的苹果 (这里选 “可以吃” 和 “操作系统” 的字眼, 仍是特意避开跟答案重合的词, 实际上按照直觉搜 苹果 造手机的
苹果 红色的
就完事了)
结论:
- 当关键词的含义不确定时, 可以用添加更多关键词的办法消除歧义
- 围绕一个关键词的含义, 描述越丰富, 召回的效果越好
- 描述时使用简单的词, 使用文档里出现过的词
- 字面相同最好, 字面不同但意思相同也凑合
讨论:
继续区分苹果, 苹果, 可以吃的
苹果, 做操作系统的
可以分别召回对应的文档片段, 有没有可能只用 可以吃的
和 做操作系统的
就达到目的?
| | 总部位于加州... | 是蔷薇科落叶乔木... | 总计102chunk里的top20命中 |
|:-------------------|----------------:|--------------------:|:---------------------------------------------|
| 苹果 | 0.671 | 0.590 | -+.................. max(0.671) ~ min(0.415) |
| 苹果, 可以吃的 | 0.553 | 0.644 | +-.................. max(0.644) ~ min(0.431) |
| 苹果, 做操作系统的 | 0.649 | 0.542 | +.-................. max(0.649) ~ min(0.447) |
| 可以吃的 | 0.373 | 0.509 | .+.................. max(0.527) ~ min(0.445) |
| 做操作系统的 | 0.425 | 0.349 | .................... max(0.562) ~ min(0.455) |
结论是不太行, 固然搜索 可以吃的
也是蔷薇科苹果的排名高, 但比起之前的搜法, 从 0.644 降到了 0.509, 当考虑更多的随机文档片段时, 仅仅搜 可以吃的
会匹配到大量噪音, 具体见 总计 102 chunk 里的 top 20 命中
这一栏, +
是预期要的那种苹果的文本 (+
越靠左侧越好), -
是另一种苹果的文本, .
表示文档库中其余不想匹配到的噪音片段
实际场景会有成千上万文本片段, 难度比 demo case 大很多, 所以还是得联合多个关键词, 把核心搜索意图描述出来, 这道理跟传统搜索是一样的
抽象的询问
上面两个例子是从 “怎么调整查询语句的角度” 讨论的, 下面讨论对具体一个查询语句, 可能遇到什么样的文档片段
以下例子我们询问 “有哪些 ob 的 AI 类插件?”
有哪些 ob 的 AI 类插件?
# 文档片段
Text Generator 是一个开源的 AI Assistant 工具,它将生成式人工智能的强大功能引入 Obsidian 中的知识创建和组织功能 例如,使用 Text Generator 根据您的知识数据库生成想法、有吸引力的标题、摘要、大纲和整个段落
Copilot for Obsidian 是 Obsidian 内部的开源 LLM 接口。它具有简约的设计,易于使用。使用 Copilot 命令或您自己的自定义提示以思考的速度提示 AI。与您的整个保险库交谈,以获取答案和见解。
Your Smart Second Brain 是一个免费的开源 Obsidian 插件,可改善您的整体知识管理。它充当您的私人助理,由 ChatGPT 或 Llama2 等大型语言模型提供支持。它可以直接访问和处理您的notes,无需手动提示编辑,并且可以完全离线运行,确保您的数据保持私密和安全。
我说 obsidian 啊, 它里面有很多 AI 插件呀, 所谓 AI 插件就是 ... , 对的, 很多 AI 插件,真的很多, 那么都有哪些 AI 插件呢?
| | TextGen | Copilot | Smart2Brain | 我说 obsidian 啊 |
|:-----------------------|----------:|----------:|------------:|-----------------:|
| 有哪些 ob 的 AI 类插件 | 0.467 | 0.528 | 0.547 | 0.747 |
即便不了解这方面, 也能看出文档片段 1 2 3 (分别是一段插件介绍) 是我们希望能召回的片段, 而 4 是句废话, 没提供任何具体的信息
结果可见 4 的匹配度是 0.747, 远高于其他三名选手…
讨论:
这是缺陷么?不, 这叫 “设计如此”, 客观的说, 我们查询语句确实跟 4 最相似, 但对于召回 “跟用户询问最相关的文档” 的任务, 这就是个缺陷
如果已经习惯跟 AI 对话, 那么会对此感到更加难受:
- 因为用 Chat 模型时, 是 问答, 补全下文 的逻辑:“有哪些 ob 的 AI 类插件” 是非常合理的问法
- 但是用 Embedding 模型时, 是 相似度匹配 的逻辑:问 “有哪些 ob 的 AI 类插件” 时, Embedding 找的是 “跟这句话最像的问题” 而非 “答案”
- 结合使用 Embedding + Chat 时, 就得在两个思维方式里跳, 难道还得把查询写成这样: “有哪些 ob 的 AI 类插件, 带 Smart 的, 带 Copilot 的, 能补充下文, 涉及知识库的都找找, 介绍一下”?
对一些了解内部怎么回事的人来说, 迅速切换对 Chat 和 Embedding 的思维方式是可以做到的, 但是我们更多时候只希望符合直觉的使用, 不想管它原理
结论:
这例子当然是个故意构造的极端情况, 但我实际体会, 该场景的弱化版其实相当常见:你文档里有一堆 key:value
结构的信息, 你询问 “找找关于 key 的内容有哪些” 期待 AI 总结 value;实际发生的事是召回了五花八门的 key, 唯独没召回一个有用的 value
在更广泛的 RAG 工程里怎么解决的?非对称语义搜索, Rerank, HyDE, … 这些已超出本文范围
在现有的一些 Obsidian RAG 插件里怎么解决?
- 设置
chunk_size
调大些, 让每个片段能容纳完整的key:value
信息- 比如尽量让
###标题\n\n\n该标题下的内容
别被切分在两个 chunk 里
- 比如尽量让
- 召回时放宽相似度阈值
- 对文档库风格有个大致了解, 比方说:
- 自描述的文档 优于 依赖外部解释的文档
- 好的例子: 笔记里写
创建者 = xxx ;修订者 = yyy
- 坏的例子:笔记里写
xxx / yyy
然后另起一备注笔记, 写:如果两个人名以斜杠隔开, 那么前为创建, 后为修订 (此时向量模型不可能知道这俩人名含义)
- 好的例子: 笔记里写
- 紧凑的描述 优于 松散隔开的描述
- 好的例子:把标签
#这里需要再看看
直接跟在具体一个笔记段落后面 - 坏的例子:把标签
#这里需要再看看
放在文档的 YFM 里
- 好的例子:把标签
- 注:不是说要改文档 (许多文档内容也没法改), 讨论这些只是为说明, RAG 效果差的其中一个原因是片段没切好
- 自描述的文档 优于 依赖外部解释的文档
- 查询语句的抽象程度不要太高,要带些具体实际的, 能指向文档细节的描述
- 好的例子:提升 RAG 效果的手段有啥 类似 HyDE 的, 给 chunk 分层的, 搞知识图谱的, 都总结一下
- 坏的例子:提升 RAG 效果的手段有啥
备注:为防误会再说明一下, 通常向量相似度计算是 “对称” 的, 它并不能区分这个句子是 query 类型还是 doc_chunk 类型, 它只是一视同仁算出句子向量, 然后把查询语句的向量跟所有文档的向量挨个算一遍相似度, 所以这一段的例子跟前面的从技术上没区别, 只是从场景上, 之前讨论 “预设目标文档, 该怎么调整多种询问语句”, 这里讨论 “固定一个询问语句, 可能会遇到啥样子的文档”
大海捞针
大海捞针实验是说在一个长文本中随机选择一个位置插入与上下文内容无关的句子, 然后向模型提问, 看模型能否准确的从文本中提取出这个信息
这里简单做个向量嵌入版的大海捞针:需要找到藏在一大段文本里的指定关键词, 这个任务向量检索能胜任吗?
以下是从十多个人名列表里尝试找目标人名 (Andreas Waldetoft, 有额外难度, 这不是英文名字) 的例子:
- 查询语句使用三种
准确拼写 / 错误拼写 / 只写姓氏
- 目标文本片段的形式, 我设计了四种:
- 第一段很简单, 一共就三个名字, 属于人眼就能发现的 (碗里捞针)
- 第二段稍微长些, 目标名字出现在较为靠后的位置 (湖里捞针)
- 第三段巨长, 目标名字出现在很靠后的位置 (海里捞针)
- 第四段巨长, 同时删掉了目标人名
那么在这些情况下, 还能找到这个名字吗?
# 查询语句
Andreas Waldetoft
Andrew Walteft
Waldetoft
# 文档片段
Composer(s) Andreas Waldetoft Bert Meyer Engine Clausewitz
Developer(s) Paradox Development Studio Publisher(s) Paradox Interactive Director(s) Henrik Fåhraeus Martin Anward (Post Release) Daniel Moregård (Post Release) Stephen Muray (Post Release) Producer(s) Rikard "Åslund" Jansson Anna Norrevik Designer(s) Henrik Fåhraeus Joakim Andreasson Daniel Moregård Johan Andersson Artist(s) Fredrik Toll Composer(s) Andreas Waldetoft Bert Meyer Engine Clausewitz Engine Platform(s) Linux macOS Microsoft Windows PlayStation 4 Xbox One
Industry Video games Genre Grand strategy games, 4X Founded 1999; 26 years ago Headquarters Stockholm, Sweden Key people Fredrik Wester (CEO) Products Video games Revenue Increase 1.793 billion kr (2020) Operating income Increase 628.0 million kr (2020) Net income Increase 490.6 million kr (2020) Owner WesterInvest AB (33.4%) Investment Aktiebolaget Spiltan (17.11%) Tencent (9.12%) Lerit Förvaltning (7.53%) State Street Bank and Trust Company (5.50%) Number of employees 656 (December 2022) Subsidiaries Paradox Development Studio Triumph Studios Playrion Game Studio Iceflake Studios / ### Paradox Development Studio Company type Subsidiary of Paradox Interactive Industry Video games Founded 1995; 29 years ago Headquarters Stockholm, Sweden Area served Worldwide Key people Johan Andersson (Studio manager) Products Europa Universalis, Hearts of Iron, Victoria, Crusader Kings, Imperator, and Stellaris series / ### Stellaris Developer(s) Paradox Development Studio Publisher(s) Paradox Interactive Director(s) Henrik Fåhraeus Martin Anward (Post Release) Daniel Moregård (Post Release) Stephen Muray (Post Release) Producer(s) Rikard "Åslund" Jansson Anna Norrevik Designer(s) Henrik Fåhraeus Joakim Andreasson Daniel Moregård Johan Andersson Artist(s) Fredrik Toll Composer(s) Andreas Waldetoft Bert Meyer Engine Clausewitz Engine Platform(s) Linux macOS Microsoft Windows PlayStation 4 Xbox One
Industry Video games Genre Grand strategy games, 4X Founded 1999; 26 years ago Headquarters Stockholm, Sweden Key people Fredrik Wester (CEO) Products Video games Revenue Increase 1.793 billion kr (2020) Operating income Increase 628.0 million kr (2020) Net income Increase 490.6 million kr (2020) Owner WesterInvest AB (33.4%) Investment Aktiebolaget Spiltan (17.11%) Tencent (9.12%) Lerit Förvaltning (7.53%) State Street Bank and Trust Company (5.50%) Number of employees 656 (December 2022) Subsidiaries Paradox Development Studio Triumph Studios Playrion Game Studio Iceflake Studios / ### Paradox Development Studio Company type Subsidiary of Paradox Interactive Industry Video games Founded 1995; 29 years ago Headquarters Stockholm, Sweden Area served Worldwide Key people Johan Andersson (Studio manager) Products Europa Universalis, Hearts of Iron, Victoria, Crusader Kings, Imperator, and Stellaris series / ### Stellaris Developer(s) Paradox Development Studio Publisher(s) Paradox Interactive Director(s) Henrik Fåhraeus Martin Anward (Post Release) Daniel Moregård (Post Release) Stephen Muray (Post Release) Producer(s) Rikard "Åslund" Jansson Anna Norrevik Designer(s) Henrik Fåhraeus Joakim Andreasson Daniel Moregård Johan Andersson Artist(s) Fredrik Toll Composer(s) Bert Meyer Engine Clausewitz Engine Platform(s) Linux macOS Microsoft Windows PlayStation 4 Xbox One
| | 碗里捞针 | 湖里捞针 | 海里捞针 | 海里没针 |
|:-----------------------------|-------------:|-------------:|-------------:|-------------:|
| Andreas Waldetoft (准确拼写) | 0.540 | 0.359 | 0.312 | 0.301 |
| Andrew Walteft (错误拼写) | 0.450 | 0.358 | 0.281 | 0.272 |
| Waldetoft (仅姓氏) | 0.471 | 0.287 | 0.287 | 0.277 |
实际上看到:文本片段的长度越长, 对某个局部关键词计算相似性时, 效果就越差
看上去就好像是, 目标关键词的信息被周围大量无关文本稀释了;换句话说, 向量检索的感知能力是不够用的, 它无法注意到所有细节:想象你在看两张 ppt, 第一张只有三行大字, 第二张是密密麻麻几十行小字, 现在给你个陌生人名, 让你找哪张 ppt 里出现过这个人
结论:
- 如果关键词藏在一大片文本里, 则这些无关文本会对找目标词产生严重干扰
- 在设计文档分割时, 需要尽量避免这种情况, 谨慎考虑切片不能太大: 各类 RAG 工具一般都是建议设置 512-1024 tokens
- 向量搜索能容忍单词里的少量误拼写
- 比如你搜
int mian() {}
ture
flase
其实问题也不大…
- 比如你搜
最后, “精确的关键词匹配” 是传统搜索的优势领域, 向量搜索不适合弄这个
备注:
另一发现是, 如果把目标人名手动放在文本最前, 则可显著提升召回效果
以下是把 Andreas Waldetoft 手动放在文本最开头和末尾的测试
| | 湖里针在中间 | 湖里针在开头 | 湖里针在末尾 | 海里针在中间 | 海里针在开头 | 海里针在末尾 |
|:------------------|-------------:|-------------:|-------------:|-------------:|-------------:|-------------:|
| Andreas Waldetoft | 0.359 | 0.427 | 0.383 | 0.312 | 0.464 | 0.312 |
文本越短, 前置关键词的收益越不明显;但对于很长的文本, 把关键词搁文本开头是很有效果的
似乎可以认为, 长文本生成的嵌入向量里, 其 “重心” 在靠前的文字上, 而靠中后部分字词的权重很低
向量嵌入模型的知识量
这部分研究几个问题
- 当遇到新鲜词时, 向量嵌入会怎么处理?
- 向量嵌入模型是否具有常识?
未知的关键词
如果是个没见过的词, 相似度检索还能工作吗?下面例子用来演示, 在向量嵌入的视角所看到的词不是通常的完整单词
# 查询语句 (合成词)
Pengull
Catcoon
Kitcoon
Koalefant
Deerclops
# 文档片段 (原型词)
Penguin
Seagull
Cat
Raccoon
Kitty
Koala
Elephant
Deer
Cyclops
# 文档片段 (无关词)
Dolphin
Dog
Squirrel
Kangaroo
Giraffe
Panda
Fox
Rabbit
Tiger
| | Penguin | Seagull | Cat | Raccoon | Kitty | Koala | Elephant | Deer | Cyclops |
|:----------|----------:|----------:|------:|--------:|--------:|--------:|---------:|-------:|--------:|
| Pengull | 0.700 | 0.447 | 0.336 | 0.343 | 0.314 | 0.408 | 0.408 | 0.368 | 0.314 |
| Catcoon | 0.482 | 0.418 | 0.686 | 0.694 | 0.535 | 0.517 | 0.54 | 0.473 | 0.428 |
| Kitcoon | 0.468 | 0.422 | 0.548 | 0.683 | 0.587 | 0.553 | 0.521 | 0.457 | 0.46 |
| Koalefant | 0.382 | 0.442 | 0.472 | 0.448 | 0.419 | 0.682 | 0.619 | 0.438 | 0.401 |
| Deerclops | 0.409 | 0.472 | 0.388 | 0.469 | 0.346 | 0.409 | 0.485 | 0.661 | 0.494 |
| | Dolphin | Dog | Squirrel | Kangaroo | Giraffe | Panda | Fox | Rabbit | Tiger |
|:----------|----------:|------:|---------:|---------:|--------:|--------:|------:|---------:|--------:|
| Pengull | 0.445 | 0.423 | 0.500 | 0.416 | 0.401 | 0.454 | 0.318 | 0.408 | 0.363 |
| Catcoon | 0.523 | 0.493 | 0.458 | 0.540 | 0.404 | 0.439 | 0.45 | 0.549 | 0.472 |
| Kitcoon | 0.514 | 0.484 | 0.497 | 0.546 | 0.452 | 0.438 | 0.428 | 0.574 | 0.464 |
| Koalefant | 0.482 | 0.494 | 0.368 | 0.531 | 0.549 | 0.467 | 0.398 | 0.437 | 0.409 |
| Deerclops | 0.436 | 0.491 | 0.472 | 0.487 | 0.471 | 0.429 | 0.392 | 0.426 | 0.447 |
这组例子的解释:
- Pengull Catcoon Kitcoon Koalefant Deerclops 是人工造词, 字典里没有
- Penguin+Seagull; Cat(Kitty)+Raccoon; Koala+Elephant; Deer+Cyclops 是上述合成词来源
- 其余的 Dolphin Dog … 等是没啥关系的几个词, 用来做对照
计算结果基本靠谱, 相似度结构是大致符合我预期的, 向量嵌入能知道 Pengull 这个词 (企鸥) 的某些局部跟 Penguin 和 Seagull 相似
此外还能看到, 词头相同比词尾相同更容易得高分:词头相同有非常明确的高分;而词尾相同时 Catcoon~Raccoon
, Kitcoon~Raccoon
, Koalefant~Elephant
这几对联系也算紧密, 另几对差些 (实测 bge-m3 会区分大小写, 如果把单词全改小写, 则 Deerclops~Cyclops=0.494
会提升至 deerclops~cyclops=0.627
其余几对也有提升)
可能有人会说, 这就是单纯的字母相似吧, 向量嵌入其实根本不认识 Pengull 这种词, 自然也不理解词的含义
不是的, 这里真有 “词义相似” 的要素, 把动物翻译成中文, 计算 英文合成词~中文动物
如下:
| | 企鹅 | 海鸥 | 猫 | 浣熊 | 小猫 | 考拉 | 大象 | 鹿 | 独眼巨人 |
|:----------|-------:|-------:|------:|-------:|-------:|-------:|-------:|------:|---------:|
| Pengull | 0.565 | 0.373 | 0.367 | 0.361 | 0.325 | 0.281 | 0.288 | 0.352 | 0.297 |
| Catcoon | 0.427 | 0.388 | 0.544 | 0.274 | 0.502 | 0.323 | 0.389 | 0.432 | 0.423 |
| Kitcoon | 0.474 | 0.400 | 0.510 | 0.303 | 0.509 | 0.312 | 0.397 | 0.425 | 0.418 |
| Koalefant | 0.371 | 0.361 | 0.511 | 0.272 | 0.477 | 0.334 | 0.461 | 0.402 | 0.394 |
| Deerclops | 0.382 | 0.409 | 0.436 | 0.385 | 0.390 | 0.303 | 0.490 | 0.424 | 0.376 |
如果之前 Pengull~Penguin=0.700
的高相似度完全是归功于 “模型看到它们首字母一致”, 那就没法解释为啥 Pengull~企鹅
也能有 0.565 的相似度
同时也能看到, 英文合成词~中文动物
的表格不像之前那么有规律了, 可能因为翻译后 “部分字母相似” 的信息丢失了
结论:
- 在向量模型视角里, 即使某个词是生造的, 字典里没有的, 它也能处理
- 表现得就像把生词分解后再匹配到熟悉的词, 有点像英语词根词缀的感觉
- 关于向量模型具体怎么拆字的, 可以参考 从词到数:Tokenizer与Embedding串讲
- 向量模型认单词时, 更多注意力集中在单词开头
- 所以搜索时, 即使不能完整拼写出某个单词, 也要尽量把开头的字母写对
- 向量模型 (至少对于 bge-m3) 能区分字母大小写 (其实 bge-m3 还能区分不同的标点符号)
世界知识
这里看看向量嵌入模型能具有什么程度的世界知识
# 查询语句
正则语言
单子论
# 文档片段
乔姆斯基
乔姆斯基体系
re
莱布尼茨
莱布尼茨哲学
monad
| | 乔姆斯基 | 乔姆斯基体系 | re | 莱布尼茨 | 莱布尼茨哲学 | monad |
|:---------|-----------:|-------------:|------:|-----------:|---------------:|--------:|
| 正则语言 | 0.274 | 0.403 | 0.446 | 0.318 | 0.380 | 0.379 |
| 单子论 | 0.338 | 0.480 | 0.361 | 0.239 | 0.362 | 0.353 |
原本意图是 乔姆斯基体系 3-型文法 正则语言 正则表达式 regexp re
是一串东西, 莱布尼茨 单子论 单子 monad
是另一串东西, 预期向量嵌入能明显的区分这两套
实际可以看到无论是竖着比较, 还是横着比较, 都完全是没道理的, 根本没规律
如果日常使用大模型, 我们知道现在随便挑个模型, 把上述概念贴进去让它 “连连看”, AI 不靠联网都能给解释好长一堆, 以至于得点击:停, 别说了, 记不住了…
这例子我尝试了很久, 有时感觉向量嵌入有常识, 有时又很傻, 有时候又带着知识图谱般的强调实体类型的意味 (在这里表现为, 它知道 “理论” 不该是 “人”, 即使前者就是后者造的:在向量模型眼里, 人物怎么对号入座或许有错乱, 但至少啥是人名 / 啥是学说 / 啥是概念缩写, 模型是拎的清的)
所以 Embedding 模型的 “认字” (见 没有思考过 Embedding,不足以谈 AI 的解释) 到底是认识到了什么?感觉这里有些非常深刻的道理, 目前我没能理解…
累, 换个简单点的
汤姆
杰瑞
# 文档片段
猫
熊猫
树袋熊
奶牛猫
奶牛
橘猫
蓝猫
鼠
猫和老鼠
| | 猫 | 熊猫 | 树袋熊 | 奶牛猫 | 奶牛 | 橘猫 | 蓝猫 | 鼠 | 猫和老鼠 |
|:-----|------:|-------:|---------:|---------:|-------:|-------:|-------:|------:|-----------:|
| 汤姆 | 0.479 | 0.410 | 0.376 | 0.401 | 0.409 | 0.388 | 0.38 | 0.340 | 0.365 |
| 杰瑞 | 0.353 | 0.311 | 0.292 | 0.281 | 0.309 | 0.320 | 0.317 | 0.365 | 0.309 |
说实话除了 汤姆~猫
之间的联系得分高些, 其他所有该产生联系的配对, 都不算明显
有趣的是, 猫这个词的含义有部分分量是指向汤姆的, 但是鼠的含义并不能够指向杰瑞, 也许 汤姆~猫
的高度联系单纯就来自同义词 汤姆和杰瑞 / 猫和老鼠
的词头?
下表是汤姆和他的朋友们
| | Tom | Jerry | Spike | Tyke | Butch | Nibbles | Tuffy |
|:--------|------:|--------:|--------:|-------:|--------:|----------:|--------:|
| 汤姆 | 0.800 | 0.394 | 0.379 | 0.448 | 0.398 | 0.396 | 0.38 |
| 杰瑞 | 0.320 | 0.631 | 0.33 | 0.327 | 0.25 | 0.328 | 0.295 |
| | Tom | Jerry | Spike | Tyke | Butch | Nibbles | Tuffy |
|:--------|------:|--------:|--------:|-------:|--------:|----------:|--------:|
| Tom | 1 | 0.491 | 0.398 | 0.502 | 0.393 | 0.397 | 0.404 |
| Jerry | 0.491 | 1 | 0.400 | 0.376 | 0.342 | 0.436 | 0.396 |
| Spike | 0.398 | 0.400 | 1 | 0.618 | 0.385 | 0.506 | 0.358 |
| Tyke | 0.502 | 0.376 | 0.618 | 1 | 0.395 | 0.422 | 0.434 |
| Butch | 0.393 | 0.342 | 0.385 | 0.395 | 1 | 0.379 | 0.394 |
| Nibbles | 0.397 | 0.436 | 0.506 | 0.422 | 0.379 | 1 | 0.479 |
| Tuffy | 0.404 | 0.396 | 0.358 | 0.434 | 0.394 | 0.479 | 1 |
Spike~Tyke=0.618
非常合理, Nibbles~Tuffy=0.479
也合理, 因为这俩是同一个老鼠 (泰菲)
但 Tom~Tyke=0.502
就很离谱, Tyke 是那个小狗, 汤姆跟 Tyke 羁绊那么深的吗?比 Tom~Jerry=0.491
都高?
- 用户:这不对啊, 这
Tom~Tyke=0.502
怎么比Tom~Jerry=0.491
还高? - 向量嵌入模型:你看这俩都是 T 开头的, 你就说像不像吧!
上面理由当然是我瞎猜的… 但是, 如果给大家都加个 T.
开头 (尝试削弱首字母的影响), T. Tom~T. Jerry=0.707
真就变成最配的一对了!
| | T. Tom | T. Jerry | T. Spike | T. Tyke | T. Butch | T. Nibbles | T. Tuffy |
|:-----------|---------:|---------:|---------:|--------:|---------:|-----------:|---------:|
| T. Tom | 1 | 0.707 | 0.674 | 0.672 | 0.620 | 0.649 | 0.611 |
| T. Jerry | 0.707 | 1 | 0.667 | 0.636 | 0.594 | 0.630 | 0.635 |
| T. Spike | 0.674 | 0.667 | 1 | 0.787 | 0.609 | 0.673 | 0.630 |
| T. Tyke | 0.672 | 0.636 | 0.787 | 1 | 0.622 | 0.622 | 0.609 |
| T. Butch | 0.620 | 0.594 | 0.609 | 0.622 | 1 | 0.594 | 0.602 |
| T. Nibbles | 0.649 | 0.630 | 0.673 | 0.622 | 0.594 | 1 | 0.643 |
| T. Tuffy | 0.611 | 0.635 | 0.630 | 0.609 | 0.602 | 0.643 | 1 |
所以能不能找到个办法, 剔除掉 “浅显的相似”, 把底层相似呈现出来?可如果有这办法, 各家模型应该早就做了
结论:
- 向量嵌入所计算的相似, 是字面相似+意义相似+实体有关联的奇妙混合
- 能从字母, 翻译文本, 词语含义, 常见的联想词… 等各方面都能找到点相似的线索
- 但无论单独考察哪一项, 都会觉得有点微妙和困惑
- 向量嵌入看起来具有一些常识, 但表现得占比不很大
- 使用向量查询时, 不该依赖过分纤巧的词义来妄图影响相似度排名, 不能期待嵌入模型和聊天模型一个认知能力
- 要用简单可靠的办法来让目标文档排到较高的排名
写在最后
大部分结论都写在对应章节里了, 最后, 尝试回答开头几个问题:
向量搜索, 跟关键词搜索区别在哪?
- 向量搜索有一定的根据语义搜索的能力
- 能容忍少量误拼写, 能容忍关键词不准确或包含着无关内容
- 翻译水平挺好 (得看具体模型), 能处理多语言的场景
- 可以用较长的语句去匹配较短的语句
向量搜索以相似度匹配文档, 有啥不符合直觉的地方?
- 向量嵌入模型的能力跟聊天模型不是一回事
- 向量嵌入模型可能也有一定逻辑推理能力, 但感觉不太可靠
- 主要还是该用它 “字词匹配” 功能, 不能想太多
- 向量搜索所采用的关键词, 是一种机器视角里的很奇特的关键词
- 向量搜索是笼统模糊的, 搜的是句子整体语义
- 不要只顾追求匹配度高, 而是要追求 “跟目标文档的匹配度能甩开无关文档”
- 向量搜索的可解释性不如传统搜索
- 分析一些搜索实例可以找到点规律, 但不总是那么稳
- 应该追求多次使用中搜索质量可靠, 别太在意单次搜索结果细节
- 应该争取宏观上目标文档能稳定召回, 不争文档排名的细微差异
啥样的知识库适合利用向量搜索?
大致等同于, 什么时候适合用 RAG, 什么时候别用, 用哪种 RAG 工具?这问题太复杂了, 目前回答不了, 泛泛的说:
- 笔记内容覆盖面广, 都是常识性质, 没啥歧义, 话题跳跃但具体到每个片段的主题都很明确
- 从特点和形式上, 感觉比较适合 RAG (是否有这需求另说)
- 笔记内容是专业知识, 且用词非常微妙精深, 稍微改动一两个字, 指向意图就差出几百里
- 其实我很好奇能利用 RAG 整到什么程度, 我也在研究这个话题
- 笔记内容是专业知识, 风格是技术类文档
- 这类文档已经为人类扫读和机器搜索做了大量优化, 包括不限于分级标题, 打标签, 做链接, 规范用词, 标注重点句…
- 这时传统搜索已经能做的挺好了
- 可以从 RAG 里获得 “连查带写” 的好处, 很多时候我们并不只查资料, 还希望 AI 顺手把字写了, 或把操作实际执行了
其实还有非常多需要讨论的, 比如:
- 各种检索的优缺点是啥?推荐 大模型 RAG 基础:信息检索、文本向量化及 BGE-M3 embedding 实践(2024)
- BGE-M3 的稀疏检索是怎么回事?推荐 Enhancing Information Retrieval with Sparse Embeddings 从上一行的文章里找到的
- 哪个 Obsidian 知识库插件用上了较先进的 RAG 技巧 (比如 HyDE, Rewrite Query, GraphRAG, …) ?这些真能管用吗?
- 在个人知识库里去折腾嵌入模型的微调, 是好主意吗?…
但写到这里已经太长了
我自己感觉, 对于 “在 Ob 里整一套适合自己的 RAG 流程” 这任务, 其中 “熟悉向量搜索特性并善加运用” 的重要程度估计占 5%~15% 左右, 所以这文章只是巨大拼图上的一块, 且限于我个人能力拼的也不怎么好
感谢大家能看到这里, 希望本文有助于建立对向量搜索的一些基本认识, 之后无论你是要决心尝试, 亦或打算避开这坑, 只要这是经过仔细思考和谨慎判断的, 我写本文的目的就达到了, 欢迎大家讨论~