1. 别暴力切分,做好语义分块与元数据注入

挺多初级的 RAG 项目,第一步就死在了长文档(比如 PDF、Word)处理上。最粗暴的做法就是按固定长度,比如 500 个 Token 一刀切开。

这种做法用在生产环境真的是灾难。

设想有一份财务报表,一个表格正好被这一刀从中间切成了两半。表头在上一块,具体数据在下一块。当用户提问时,单独被检索出来的下半块数据根本没有表头的定义,别说大模型看了是一头雾水,换我去看也是一样懵逼。

生产上比较好的做法是采用结构化感知分块

对于文档,必须先用专门的解析工具将其还原为 Markdown 等带有结构化标记的文本。切分的时候,要按段落、按标题层级、甚至按完整的表格边界进行切分。

此外,还有极其重要的一步:为切出来的每一个文本块注入上下文元数据(Metadata)。

比如你切出来一段文本是“该设备的维护周期为六个月”。如果只把这句话存进向量库,将来检索出来也是毫无意义的,因为你根本不知道它说的是哪个设备。

必须在构建索引时,让这段文本带上它的层级信息:{"文件":"2023维修手册", "章节":"发动机保养", "内容":"该设备的维护周期为六个月"}

这样大模型在回答时,手里才有充分的语境。

2. 混合检索

绝大多数的开源 RAG 默认使用的是单一的稠密向量检索。向量检索擅长处理语义相似度,比如你搜“苹果手机”,它能给你找出来“iPhone”,这没错。

但业务线上的很多提问是非常精准且死板的。

比如售后人员搜“设备报错 Error-0x9F4A 是什么原因”。这里的 Error-0x9F4A 是一个极具特征的业务专有名词。如果纯靠向量模型,这种无语义的纯字符编号在做 Embedding 之后,特征往往会被严重稀释,导致底层根本检索不到包含这个报错码的文档。

要解决这个问题,必须引入传统的关键词检索,采用双路召回(Hybrid Search)

一方面用向量库做语义泛化召回,另一方面用 ElasticSearch 或者同类全文引擎做纯文本的精确匹配召回。两路数据拿回来之后,再通过 RRF(倒数排序融合)算法把两份结果交叉打分,合并成一份列表。这就彻底兜底了“同义词找不到”和“专业词汇匹配不上”的两个极端。

3. 引入重排(Rerank)

检索结果拿回来后,怎么给大模型?很多团队为了提高命中率,会把检索库返回的前 20 个文档全部塞进大模型的 Prompt 里让它去总结。

但这会直接触发大模型一个著名的缺陷:“迷失在中间(Lost in the Middle)”。上下文过长且包含大量无关噪音时,大模型会遗忘关键信息,甚至被错误信息带偏,最终产生幻觉。而且超长的输入又会大量消耗 Token,推高 API 成本的同时严重拉高响应延迟。

标准的工程解决方案是分为两阶段检索(Retrieve & Rerank)

第一阶段(粗排),利用便宜且快速的向量检索和 BM25,粗略地召回 50 篇可能相关的文档。

第二阶段(精排),引入一个专门的 Cross-Encoder 模型,也就是重排模型,比如 BGE-Reranker。重排模型的底层机制是将“用户提问”和“候选文档”拼在一起同时输入神经网络,它对相关性的判断极其精准,缺点就是计算耗时。

通过重排模型对这 50 篇文档进行精准打分排序,最后只挑出得分最高的 3 到 5 篇核心文档,喂给大模型。这样既保证了整体速度,也降低了大模型的阅读负担。

4. 提问预处理:Query 重写与扩展

回到用户端,实际业务里用户提的问题往往是极度口语化,且严重缺乏上下文的。

比如在一个多轮对话场景中,用户第一句先问了:“OA系统怎么登录?” 紧接着第二句问:“密码忘了怎么办?”

如果 RAG 系统直接拿着“密码忘了怎么办”这几个字去检索文档库,召回的结果大概率是各种乱七八糟业务系统的密码找回规则,毫无针对性。

所以在把问题扔进检索器之前,必须增加一个提问预处理层。通常的做法是调用一个轻量级、响应极快的小模型,把历史聊天记录和当前提问结合起来,重写成一个标准的检索词:

// 伪代码演示 Query Rewrite 逻辑
String userQuery = "密码忘了怎么办";
String chatHistory = "[User: OA系统怎么登录, System: 您可以通过企业微信扫码...]";

// 利用小模型进行意图重写
String rewrittenQuery = fastLlm.generate(
    "根据以下聊天历史,将用户的最新提问重写为一个具体且独立的查询请求。历史:" + chatHistory + " 提问:" + userQuery
);

// rewrittenQuery 此时变成了:"OA系统密码忘了怎么找回"
// 用这个词再去执行底层的混合检索
List<Document> docs = retrievalPipeline.search(rewrittenQuery);

除了重写,还可以顺手做 Query 扩展,让模型根据用户的原始提问,变异出三四个近义问题,用多路查询去并发检索,这能极大弥补用户自身表达不准导致的数据遗漏。

5. 动态评估

传统的 RAG 就是一条单向流水线:提问 -> 检索 -> 生成。只要检索这个环节出了错,最后生成的绝对是错的。

目前更先进的做法,是演进到带有评估反馈逻辑的架构。在这个架构里,大模型不仅是文本生成器,还充当了业务流的“裁判员”。

当底层返回检索结果后,先用大模型跑一次轻量级的打分机制:

评估检索回来的这几篇文档里,到底包不包含能回答用户问题的线索?

  • 如果包含,进入生成环节。

  • 如果判定所有文档都不相关,系统主动放弃基于知识库生成,要么老老实实告诉用户“知识库暂无记录”,要么触发外接的搜索引擎(比如 Bing Search API)去公网上找答案。总之,决不允许它强行拿着无关文档去编故事。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐