Embedding 效果不好怎么办?
Embedding 效果不好?先别急着换模型
换了三个 Embedding 模型,召回率还是上不去?
这大概是很多 RAG 开发者都踩过的坑。但今天我要泼一盆冷水:问题大概率不在模型,而在你的文档。
上篇文章我们聊过 Embedding 的本质:把文本变成向量,让计算机能计算相似度。原理很简单对吧?但一动手做就发现,召回回来的内容要么不相关,要么缺胳膊少腿。
这种情况我见过太多了。说句不客气的话:与其花一周调参换模型,不如先把文档处理流程捋顺。为什么?因为数据质量才是天花板。

Embedding 效果差的根源,80% 在文档处理
我来还原一个真实场景。
你上传了一份 PDF 文档,里面有表格、有图片、有多级标题,甚至还有代码块。Embedding 模型吭哧吭哧一顿处理,生成了一堆向量存进数据库。
然后你问了一个问题,RAG 召回了一段话。你一看——
这段话在讲"用户增长率",但表格里的具体数字没了。
这段话说"根据第三条规定",但规定的具体内容被截断了。
这不是 Embedding 模型的错。模型只是老老实实地把你给的内容向量化了。问题出在你给它喂的数据本身就有问题。
就像你不能让米其林大厨用烂食材做出米其林菜品一样。把文档处理管线做到位,比换一百个 Embedding 模型都管用。
这是做 RAG 第一个要转变的观念。

文档到向量的六个关卡
把文档变成向量,要过六道关。每一道都有坑,每一道都有人踩过。
第一关:格式校验。
你得先确认这份文档能读。有些文件是损坏的,有些是加密的,有些格式根本不兼容。上传阶段就做校验,能省很多后续的麻烦。
第二关:Layout 解析。
这一步是把文档的物理结构识别出来。PDF 的每一页、Word 的每个章节、表格在哪里、图片在哪里。这些信息丢了,后面就很难还原。
常见的工具有 PyMuPDF、python-docx、pypdf。结构复杂的文档可以用 layoutparser 或者商业方案。
第三关:清洗去噪。
文档里有很多"脏东西"。页眉页脚、页码、水印、重复的导航链接。这些东西混进去会干扰 Embedding。
很多人忽略的是:很多 PDF 的表格其实是图片,不是文字。这种表格直接用 OCR 提取才有救。
第四关:Chunking,也就是切分。
这是今天的主题,我们后面重点讲。
第五关:Metadata 注入。
这一步经常被忽视,但超级重要。
Metadata 就是元数据。你得记录:这段文本来自哪个文件、第几页、属于哪个章节、文档的版本是什么、有没有权限限制。
没有 Metadata,召回的时候就只知道"这段话好像相关",但不知道这段话该用在什么场景。
第六关:入库。
向量入库,同时关联好 Metadata。这一步做好了,后面的过滤、分级召回才有可能。
六关环环相扣。哪个环节拉胯,效果都好不了。

四种切分策略怎么选
终于到重头戏了:怎么把长文档切成适合 Embedding 的小块。
这就像切蛋糕。要顺着纹理切,切出来的才好看。
第一种:固定长度切分。
最简单粗暴。按字数或者 Token 数一刀切,比如每 512 个 Token 切一块。
优点就是简单,实现容易,没啥门槛。
缺点也很明显:可能在句子中间切断,可能把一个完整的表格拆成两半,可能让"用户增长率同比上升 15%"变成"用户增长率同比上升"和"15%"两个不相关的块。
这种策略适合简单文档、兜底场景。不推荐作为主力方案。
第二种:递归字符切分。
这个方法按层级来切:先试着按段落切,段落太大就按句子切,句子还太大就按单词切。递归的意思就是:这一级搞不定,就用下一级。
LangChain 的 RecursiveCharacterTextSplitter 就是这个思路。好处是能尽量保留自然语言的边界,坏处是实现有点复杂,参数调试要多花点时间。
第三种:语义切分。
这是最"聪明"的方式。用 Embedding 模型来判断:哪些句子放在一起语义最接近,就切成一块。
实现方式一般是滑动窗口遍历全文,计算相邻窗口的相似度,相似度骤降的地方就是断点。
效果最好,但成本也最高。每次切分都要调用 Embedding 模型,大文档处理起来慢而且贵。
适合对召回质量要求极高的场景,比如法律文档分析、医疗记录检索。
第四种:按文档结构切分。
这是最"听话"的方式。Markdown 文件就按 # 标题层级切,代码文件就按函数切,PDF 有目录就按章节切。
Markdown 用 HeaderSplitter 就行。代码文件可以用 AST 解析器,按函数签名切。
为什么这种方式好?因为文档结构本身就是作者帮你划分的语义边界。顺着这个边界切,最少破坏原文的完整性。
代码文档推荐每块 100 Token 左右。通用文本 400-512 Token 是比较通用的范围。
没有万能钥匙,只有最适合当前文档的切法。

语义丢失的四大坑和应对方法
切分的时候有四个坑,踩进去召回效果直接崩。
坑一:业务逻辑被拆散。
假设你的文档写着:"如果用户年龄大于 18 岁且已绑定银行卡,则开通高级功能。"
切分的时候,"年龄大于 18 岁"切到了一块,"且已绑定银行卡"切到了另一块。
召回的时候,模型只看到"年龄大于 18 岁",不知道还有绑定银行卡的附加条件。回答出来的内容就是错的。
这种问题叫条件截断。业务规则有多个条件时,最容易中招。
坑二:Chunk 丢失位置信息。
假设一段代码在文件里属于第三章。但切分之后,块本身不知道自己属于哪一章。
召回的时候,这段代码可能被用在完全不相干的场景里。
怎么解决?靠 Metadata 记录章节路径。块里要保存"来自第X章第Y节"这样的信息。
坑三:多列表格变成混乱文本。
表格的每一列可能有语义关联。比如第一列是"月份",第二列是"销售额",第三列是"增长率"。
如果按单元格切,列与列之间的对应关系全丢了。读起来就是三个独立的列表,完全无法理解。
坑四:专有名词被截断。
"人工智能"可能被切成"人工"和"智"两个字,或者英文单词被 hyphen 断开,变成乱码。
这种情况用 tokenizer 做切分就能避免。LangChain 的 TokenTextSplitter 就是基于 Token 数来切的,不会把一个词从中间断开。
那怎么填这四个坑?三个方法:
重叠控制。 相邻的两个块重叠 15%-20% 的内容。比如第一块 1-500 Token,第二块 450-950 Token。这样即使断点选得不太对,重要的信息也会在重叠区域被保留。
Parent-Child Chunk。 大块叫 Parent,小块叫 Child。检索的时候用小块匹配,但关联的时候把大块的内容一起召回。这样既保证精确度,又保证完整性。
Metadata 记录章节路径。 每个块都要知道自己从哪来、属于哪个章节。召回的时候可以按章节过滤。
切文档就像玩拼图。切得太碎,就拼不回完整的画面。但也不能切得太大,否则每块的信息太杂,Embedding 抓不住重点。找到平衡点才是关键。

降级处理与异常应对
文档处理管线跑着跑着,总会遇到各种异常。怎么处理?
空文件,直接拒绝入库。 发个通知告诉上传者,这份文件是空的,没法处理。
格式不支持。 给用户一个提示,建议他转成 PDF 或者 Markdown 再试。
解析失败。 进入人工队列,或者尝试备用解析器。商用文档处理方案通常会内置多个解析引擎,主引擎挂了自动切换备用。
Chunking 异常。 比如遇到一个巨长的段落,怎么切都超限。直接改用固定长度兜底,牺牲一点效果但保证不崩。
部分成功。 有些文档可能只有部分内容能解析。比如 PDF 里有一页是扫描件,文字提取不出来。这时候就把能解析的部分入库,给这批数据打上标签,标记"部分内容缺失"。
整个设计思路就是:飞机起飞前做多层检查,任何环节出问题都有备选方案。不能因为一块短板导致整个流程挂掉。

实战选型建议
说了这么多,具体怎么落地?
我的建议是按文档类型选工具。
Markdown 文件,用 HeaderSplitter 按标题层级切。块大小设 400-600 Token,重叠 15%。
PDF 文件,先看有没有目录结构。有的话按章节切;没有的话按页切,每页 400-500 Token。
代码文件,用 AST 解析器按函数切。每块 100-150 Token,不重叠或者重叠 5%。
表格多的文档,单独处理表格内容,不要混在正文里一起切。
通用文本没有固定规则。测试几种策略,看哪种召回效果最好就用哪种。
Metadata 要保存这些信息:来源文件名、页码或章节路径、文档版本、最后更新时间。如果有权限控制字段也要加上。
重叠率建议 15% 左右。太高会增加存储成本和 Embedding 次数,太低又容易漏掉边界信息。
没有最优切分策略,只有最适合你场景的策略。多测试,多调参,找到最合适的组合。
向量归一化:容易被忽视的隐藏参数
切分聊完了,再补充一个配套的话题:向量归一化。
Embedding 模型生成的向量,长度可能不一样。有的模型输出的向量长度是固定的,有的则不是。
向量的长度会影响余弦相似度的计算。如果你用余弦相似度做检索,最好先把向量做归一化,让所有向量的长度都变成 1。
归一化之后的向量,余弦相似度就是点积。计算更简单,检索更快。
这个步骤很简单,但很多人会忘掉。
怎么判断需不需要归一化?这取决于你用的向量数据库和检索算法。有些数据库内置了归一化,有些则需要你手动处理。
混合检索:向量检索不是万能的
向量检索很强,但不是万能的。
它擅长找语义相关的内容。比如你搜"如何做红烧肉",它能找到"东坡肉的烹饪方法"。
但它不擅长找精确匹配。比如你搜"订单号 20240607001",向量检索可能返回一堆无关内容。
这种场景适合用关键词检索。BM25、TF-IDF 这些传统算法,对精确匹配的召回率反而更高。
所以实践中经常是两种检索配合用:向量检索负责语义相近,关键词检索负责精确匹配。
这就是混合检索。
但混合检索也有问题:怎么融合两种检索的结果?权重怎么分配?什么时候用向量,什么时候用关键词?
这些问题,我们留到下篇聊。到时候你会发现,向量检索和稀疏检索的对比,远比你想的有趣。
