嵌入并非魔法:RAG检索的可预测故障模式

📰 来源:Towards Data Science | 📅 翻译日期:2026年5月31日
🔗 原文查看原文
🤖 翻译:DeepSeek AI · 仅供参考

两个场景,都很熟悉。

场景一:RAG系统初显神通

一个基于 RAG 的系统,覆盖了几百页政策文档,在一个小团队中上线。

首先让大家印象深刻的是:它能够处理 同义改写。有人问“如何取消?”,文档中从未出现“取消”一词,而是用了“终止程序”,但系统仍然找到了它。

另一位用户用法语提问,而政策文档是英文的,系统依然返回了正确的页面。这里有一个拼写错误,那里有一个语音拼写,都没问题。几天后,团队确实被折服了。RAG 最接近魔法的东西就在他们面前,而且不需要任何手工编码的同义词表。

场景二:两周后的失灵

同样的系统,两周后。

用户问:“关于合同工加班的规则是什么?”系统回答:“未找到相关信息。”而这位用户恰好是编写了这本手册一半内容的业务专家,他皱起眉头,打开 PDF,在 Ctrl+F 中输入“非雇员劳动”,三秒钟就找到了确切的段落。正确的关键词不是“加班”,而是文档实际使用的术语。专家知道这一点,但嵌入模型不知道。

很快,更多类似的情况浮现出来。否定句处理出错。确切的合同编号失败。一个内部产品代码返回了错误的等级。这些问题都无法通过更换嵌入提供商来解决。

本系列文章的观点提前说明:大多数企业级可靠性提升来自于强大的上游过滤(专家关键词、文档结构),而不是在薄弱的检索之上堆叠重排序器。

经典架构按成本分层

  • 最底层:廉价的嵌入相似度
  • 中间层:可选的交叉编码器重排序器
  • 最上层:聊天补全的 LLM

它们都不是魔法;每一种都有特定的失效方式。

本文是更广泛的 企业文档智能系列 的一部分,该系列从基线管道到语料规模架构,逐步构建企业级 RAG。

1. 嵌入擅长的领域

在讨论失败之前,先看看嵌入真正让人印象深刻的地方。失败只有对比之下才有意义。

嵌入 将一段文本转换成一个向量。具有相似术语的文本在向量空间中会靠得很近。嵌入是一组数字,捕捉了一段文本的含义:更长的列表可以携带更多的细微差别。每一代嵌入都在改进。下面的每个案例都在四个模型上运行,从最弱到最强:

  • GloVe-avg (2014):平均化的 300 维词向量。无上下文。严重偏向于字面词重叠。Apache 2.0 许可,在 HuggingFace 模型卡上声明。
  • all-MiniLM-L6-v2 (2021):小型开源上下文句子编码器,2200 万参数,384 维。Apache 2.0 许可,在 HuggingFace 模型卡上声明。
  • text-embedding-ada-002 (OpenAI 2022):1536 维。专有;受 OpenAI 使用条款约束。
  • text-embedding-3-large (OpenAI 2024):3072 维。专有;受 OpenAI 使用条款约束。

加载每个模型只需一行代码。两个本地模型来自 sentence-transformers(首次调用时从 HuggingFace 下载权重到磁盘);两个 OpenAI 模型通过 API 客户端调用。所有四个模型的调用方式相同,返回一个向量。

from sentence_transformers import SentenceTransformer
from openai import OpenAI

# Local models: weights downloaded from HuggingFace, run in-process.
glove = SentenceTransformer("average_word_embeddings_glove.6B.300d") # 2014, 300-dim
minilm = SentenceTransformer("all-MiniLM-L6-v2") # 2021, 384-dim

# OpenAI models: called through the API.
client = OpenAI()
def openai_embed(text: str, model: str) -> list[float]:
    return client.embeddings.create(input=text, model=model).data[0].embedding

# Same call shape across all four; each returns a vector of its own dimension.
v_glove = glove.encode("policy renewal")
v_minilm = minilm.encode("policy renewal")
v_ada = openai_embed("policy renewal", "text-embedding-ada-002") # 2022, 1536-dim
v_large = openai_embed("policy renewal", "text-embedding-3-large") # 2024, 3072-dim

每个模型都存在于自己的向量空间中,具有各自的余弦分布,因此各列之间的原始分数不可比较。有意义的是同一列内的分离度:目标是否胜出干扰项,以及胜出多少?观察差距在梯度上扩大,是嵌入确实变好的经验证据。

下面每个比较表使用的基本原语相同:用四个模型嵌入查询和每个候选文本,用余弦相似度评分,每个候选返回一行:

def _cos(u, v):
    """Cosine similarity : dot-product of two vectors, normalised by their lengths."""
    return float(np.dot(u, v) / (np.linalg.norm(u) * np.linalg.norm(v)))

def compare_models(query, candidates, target=None):
    qg = glove.encode(query)
    qm = minilm.encode(query)
    qa = openai_embed(query, "text-embedding-ada-002")
    ql = openai_embed(query, "text-embedding-3-large")
    rows = []
    for c in candidates:
        rows.append({
            "candidate": c,
            "GloVe-avg": _cos(qg, glove.encode(c)),
            "MiniLM": _cos(qm, minilm.encode(c)),
            "ada-002": _cos(qa, openai_embed(c, "text-embedding-ada-002")),
            "3-large": _cos(ql, openai_embed(c, "text-embedding-3-large")),
        })
    return pd.DataFrame(rows).set_index("candidate")

1.1 概念近似

“车”匹配关于“车辆”、“汽车”、“机动车”的段落。“火灾损坏”找到“烟雾损坏”和“烧焦”的段落。“经理审批”匹配关于“高管批准”的条款。模型捕捉的是语义场,而不仅仅是表面词汇。这就是嵌入让人感觉强大的原因:用户不需要猜测文档的词汇;嵌入架起了桥梁。

随意查询桥接至正式释义。所有四个模型都找到了目标;更大的模型扩大了差距。

1.2 同义词与释义

“电话号码”匹配“电话”。“政策取消”匹配标题为“终止程序”的章节。“费用”匹配“收费”。“月成本”匹配“保费”。“到期”匹配“保单结束日期”。


📌 *本文由 DeepSeek AI 自动翻译排版,如有不准确之处欢迎指正*
©版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

评论已关闭