RAG 检索中容易忽略的三个问题

RAG 检索中容易忽略的三个问题

搭建内部知识库问答系统时,采用了标准 RAG 架构:文档切片、向量化、检索、导入 LLM 生成回答。上线后出现回答偏离用户意图的情况,排查日志发现检索返回的 chunk 与 query 匹配度不足。

梳理了整个检索链路后,定位到三个环节的问题。

切片粒度与语义完整性

最初的方案是按固定长度切片,每 500 字一段,overlap 50 字。这个方法对于结构化的文档会引入额外的问题:真实文档中有段落、列表、代码块等逻辑单元,固定长度切分可能把一个完整概念拆散到不同 chunk 中。

比如文档中有一段关于 Redis 持久化方式的说明:

Redis 持久化有两种方式:
1. RDB:定时快照,恢复快,但可能丢数据
2. AOF:追加日志,数据更安全,但恢复慢
选择建议:如果对数据安全要求高,用 AOF;否则 RDB 足够。

固定长度切片时,「选择建议」这一句可能被划入下一个 chunk。当用户查询「Redis 该选哪种持久化」,检索命中的 chunk 只包含前两行说明,缺少决策建议,LLM 的生成结果会缺失关键信息。

处理思路是以语义边界作为切分依据,使用段落、标题、列表项作为天然切分点。如果场景要求固定长度,overlap 需要提高到 20%~30%,确保关键跨越不会在 chunk 边界丢失。另一种做法是让 LLM 判断语义段落边界,做智能切片。

def semantic_chunk(doc: str, max_len=500, overlap_ratio=0.25):
    paragraphs = doc.split('\n\n')
    chunks = []
    current = ''
    for para in paragraphs:
        if len(current) + len(para) > max_len and current:
            chunks.append(current)
            current = current.split('\n\n')[-1] + '\n\n' + para
        else:
            current += '\n\n' + para
    if current:
        chunks.append(current)
    return chunks

单一向量相似度的局限

标准流程中,检索使用 query 向量与 chunk 向量计算 cosine similarity,取 top-k。实际场景中,用户 query 较短,文档 chunk 较长,两者的语义空间距离不一定可靠。

举例:用户查询「怎么部署 Kubernetes」,知识库中有「K8s 集群搭建指南」和「容器编排原理概述」两篇文档。前者是用户真正需要的内容,但后者的 chunk 可能因为全篇讨论容器概念导致相关词频更高,向量排名反而靠前。

混合检索可以缓解这个问题。在向量检索的基础上并行 BM25 关键词检索,双路召回后通过 RRF(Reciprocal Rank Fusion)合并排序:

results_vector = vector_store.search(query_embedding, top_k=10)
results_bm25 = bm25_index.search(query_keywords, top_k=10)
merged = reciprocal_rank_fusion([results_vector, results_bm25], k=60)
final = merged[:5]

另外,在检索前对 query 做一次改写也有效果。用 LLM 把用户的简短 query 扩展为更丰富的检索文本,增加与目标 chunk 的匹配度。例如「怎么部署 K8s」改写为「Kubernetes 集群部署步骤、环境准备、kubectl 配置、节点管理」,多一次 LLM 调用的成本,检索命中率有明显提升。

检索结果的噪声过滤

top-k 检索返回的 chunk 中经常夹杂部分相关但非直接相关的内容。如果直接全部喂给 LLM,这些噪声会影响生成质量,使回答偏离用户意图。

实际遇到过一个例子:用户查询「Python GIL 是什么」,top-5 结果中排第三的是一段讲 Python 装饰器的内容。原因是该段所属的文档标题为「Python 性能相关的几个话题」,向量相似度 0.72。LLM 在生成回答时混入了装饰器的相关内容,导致答案不聚焦。

解决方式是增加一轮重排。使用 Cross-Encoder 或轻量级重排模型对检索结果做 query-document 级别的精细匹配评分,过滤掉表面相关但实际无关的内容:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
scores = reranker.predict([(query, chunk) for chunk in retrieved_chunks])
ranked = sorted(zip(retrieved_chunks, scores), key=lambda x: x[1], reverse=True)
final_chunks = [c for c, s in ranked[:3]]

同时可以设置相似度阈值,低于阈值的结果直接丢弃。对于长度较大的 chunk,可以先用 LLM 提取与 query 相关的片段,压缩上下文后再进入生成环节,减少 token 消耗和噪声干扰。


切片、检索策略和重排这三个环节决定了 RAG 检索质量的上限。如果这个环节有缺陷,后续的 Prompt 调优和模型选择能改观的空间有限。

posted @ 2026-06-29 23:00  疯狂的yang  阅读(5)  评论(0)    收藏  举报