构建一个能用的RAG系统:切块、嵌入、混合检索与融合排序

多数 RAG 教程停在"调 embedding API + 相似度排序"的 demo 层,而真实系统在复杂问题上的失败------检索不到、召回混乱、答案引用错段落------都发生在 demo 没讲的部分。这篇文章从语料处理到生成引用完整走一遍,每一层给出可运行的实现与调参依据。

一、切块:整个系统的第一性环节

切块质量的下限就是检索质量的上限。朴素固定长度切块的两个失败模式:切在句子中间破坏语义(嵌入质量下降)、切得过小丢失上下文(检回来的片段读不懂)。实用的切块器要同时解决两者------语义边界优先、大小约束兜底、相邻块重叠:

python 复制代码
def chunk_text(text, target=512, overlap=64):
"""段落优先的层级切块: 句子聚合成接近 target 的块"""
paras = [p.strip() for p in text.split("\n\n") if p.strip()]
sents = [s for p in paras for s in sent_tokenize(p)]

chunks, cur = [], []
cur_len = 0
for s in sents:
n = len(s.split())
if cur_len + n > target and cur:
chunks.append(" ".join(cur))
# 重叠: 回退若干句, 保持跨块语义连续
back = max(0, len(cur) - max(1, overlap // 20))
cur, cur_len = cur[back:], sum(len(x.split()) for x in cur[back:])
cur.append(s); cur_len += n
if cur: chunks.append(" ".join(cur))
return chunks

调参依据:块大小不是越大越好------嵌入模型把整块压成一个向量,块越大,向量里的信息被平均得越稀,与查询的匹配就越钝。512 token 上下是一个经验甜点;检索型任务可以更小,摘要型任务可以更大。另一层常被忽略的工程:块与元数据绑定(来源、章节、时间),检索后过滤与引用都靠它,切块阶段不埋好,后面全是补丁。

二、混合检索:稠密与稀疏的互补

纯向量检索有盲区:专有名词、产品编号、人名这类精确词在嵌入空间里不突出------嵌入捕捉语义相似,而"RFC-4823"与"RFC-4824"的向量几乎一样近。BM25 这类词频检索恰好相反:精确匹配强、语义变换弱。两者互补是混合检索的依据:

python 复制代码
class HybridRetriever:
def __init__(self, chunks, embed_fn, k1=1.5, b=0.75):
self.chunks = chunks
self.vectors = normalize(embed_fn(chunks))     # (N, d)
self.bm25 = BM25Okapi([c.split() for c in chunks], k1=k1, b=b)

def search(self, query, topk=20):
v = normalize(embed_fn([query]))[0]
dense_scores = self.vectors @ v                # 余弦相似度
sparse_scores = self.bm25.get_scores(query.split())
# RRF 融合: 只用排名, 免去两路分数不可比的问题
fused = rrf_fuse([dense_scores, sparse_scores], k=60)
return top_k(fused, topk)

def rrf_fuse(score_lists, k=60):
"""Reciprocal Rank Fusion: score += 1 / (k + rank)"""
fused = defaultdict(float)
for scores in score_lists:
for rank, idx in enumerate(np.argsort(scores)[::-1]):
fused[idx] += 1.0 / (k + rank + 1)
return fused

RRF 的妙处在于只用排名不用分数:稠密余弦与 BM25 分数不同量纲,直接加权融合需要校准,排名融合则天然无量纲。k=60 是原论文给的经验值,对头部排名敏感度适中。

三、重排:从"相关候选"到"相关排序"

粗检索的向量是单向量压缩 ------查询与文档块各压成一个点,相似度是粗粒度的。重排器用交叉编码器把查询与候选拼在一起过一遍完整模型,token 级交互的精度远高于双塔,代价是每个候选一次前向,所以只能伺候粗检索筛出的 top 20--50:

python 复制代码
def rerank(query, candidates, ce_model, topk=5):
pairs = [(query, c.text) for c in candidates]
scores = ce_model.predict(pairs, batch_size=16)    # 交叉编码器打分
order = np.argsort(scores)[::-1][:topk]
return [candidates[i] for i in order]

三层流水线的分工由此清晰:稠密+稀疏负责召回 (从十万级到百级,要快、要全),交叉编码器负责精排(从百级到个位,要准、可以慢)。漏斗每一级的宽度按"下一级的精度与成本"定:精排器越强,粗排可以放宽 recall@k。

四、生成与引用:让答案可追责

检索的产物进入生成上下文时,给每个片段编号并在提示里要求引用编号,这是答案可信度的最低成本方案。生成后做引用一致性校验------检查答案里的引用是否真的支持其声明,是防"引用造假"的最后一道闸:

python 复制代码
SYSTEM = """基于给定的片段回答问题。
每个陈述后标注支持它的片段编号, 如 [2]。
若片段不足以回答, 明确说"依据不足", 不要编造。"""

def answer(query, retriever, llm):
ctx = retriever.search(query, topk=20)
top = rerank(query, ctx, ce, topk=5)
prompt = "\n".join(f"[{i}] {c.text}" for i, c in enumerate(top, 1))
out = llm.generate(SYSTEM + prompt + query)
return out, top    # 附带片段, 前端可点开溯源

最后一层是评估。检索侧的核心指标是 recall@k(正确片段是否进了候选)与 MRR(正确片段排多前)------需要一个小而真的标注集(50--200 个问题,人工标出应命中的片段),这在系统迭代时是不可替代的回归测试。生成侧则看引用准确率与拒答率(该拒答的问题是否拒答)。一个诚实的经验:多数"RAG 效果差"的问题,归因顺序应当是切块 → 粗检索召回 → 重排 → 提示词,从前往后查,90% 的问题死在前两级------而大多数团队花的时间恰好反着。

相关推荐
Dawson Zhu21 分钟前
【LLM推理与Serving⑪】MoE推理与MLA:DeepSeek-V3架构下的显存与通信权衡
人工智能·语言模型·架构·aigc·agi
github_is_geeksKai42 分钟前
50 个 AI 与开源工具怎么选:从实际用途到实现边界
人工智能
IT_陈寒1 小时前
Vite的HMR把我整不会了:一个生产环境的热更新坑
前端·人工智能·后端
github_is_geeksKai1 小时前
浏览器 AI 小工具怎么选?40 个项目与 5 类商业任务的参考指南
人工智能
styshoo1 小时前
[snapshot]7-placeholder与恢复流程
人工智能
这个DBA有点耶1 小时前
库内计算推理:一条SQL背后的算子融合,和外挂方案差了几次网络往返
数据库·人工智能·sql
SelectDB2 小时前
AI Agent 的分析数据存在哪里?Trace、Prompt 与会话数据的存储选型
人工智能
SelectDB2 小时前
京东 Apache Doris 湖仓查询优化实践:从多引擎统一到百万级查询
人工智能
伞伞悦读2 小时前
【第81期】特征缩放怎么选:标准化、MinMax 与 Robust 的原理、泄漏对照和完整实验
人工智能·python·深度学习·机器学习