我要提问
ARTICLE / 003 · 人工智能

原创文章

资深开发者执笔的深度技术长文,从原理到工程落地,逐层拆解。

大模型 RAG 检索增强生成工程化落地

大模型 RAG 检索增强生成工程化落地

大模型有两个天生短板:知识截止时间之后的"新知识"它不知道,私有领域的"专属知识"它也没学过。RAG(Retrieval-Augmented Generation,检索增强生成)用一套"先检索、后生成"的范式,把外部知识"喂"给模型,让它在生成时有据可依。但 RAG 的真正难点不在 demo,而在工程化——切分、检索、重排、引用,每一步都决定最终答案的质量。本文按真实落地链路逐层拆解。

一、RAG 的本质:把"记忆"外置

大模型的参数相当于"凝固在训练时刻的记忆",无法在推理时动态更新。RAG 的思路是:不在模型参数里存知识,而是建一个外部知识库(向量库 + 原文),每次提问时先从知识库检索最相关的若干片段,再把片段作为上下文拼进 prompt,让模型基于这些片段作答。这样知识可以随时增删改,无需重新训练。

RAG 的核心价值不是"让模型变聪明",而是"让模型回答得有依据、可追溯、可更新"。它把知识从参数里解放出来,变成可运维的数据资产。

二、文档处理:切分是质量的起点

切分(chunking)决定了检索单元的粒度,直接影單召回质量。切得太粗,一个 chunk 里塞了多个主题,检索时相关性被稀释;切得太细,语义被割裂,模型难以理解上下文。没有"一刀切"的最佳长度,需要根据文档类型选择策略。

2.1 常见切分策略

  • 固定长度切分:按字符数(如 500 字)切,简单但易割裂语义,适合均匀的纯文本。
  • 语义切分:按段落、标题、句子边界切,保留语义完整性,适合 Markdown、HTML 等结构化文档。
  • 递归切分:先按大单位(章节)切,超长再按小单位(段落、句子)递归,兼顾结构与长度。
  • 重叠切分:相邻 chunk 保留一定重叠(如 50 字),避免边界信息丢失。
# 基于 LangChain 的递归字符切分示例
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每个 chunk 最大 500 字符
    chunk_overlap=50,      # 重叠 50 字符
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)
chunks = splitter.split_text(document)

2.2 切分的工程考量

切分时要为每个 chunk 附加元数据:来源文档、章节标题、页码、版本号。这些元数据在检索时可用于过滤(如只搜某文档)和引用追溯(告诉用户答案出自哪篇文档第几页)。mrgr.cn 的实践是:为每个 chunk 生成"标题路径"前缀,让 chunk 自带上下文,显著提升检索相关性。

三、向量化与索引:embedding 模型选型

切分后的 chunk 需要 embedding 成向量才能做语义检索。embedding 模型的选择决定了语义理解的"分辨率"。选型要考虑三个维度:语言支持(中英文)、维度(影响存储与检索速度)、领域适配(通用 vs 专业)。

  • BGE / m3e:开源中文 embedding 主流选择,支持中英文,部署成本低。
  • OpenAI text-embedding-3:多语言通用能力强,但需调用 API,数据出域需谨慎。
  • 领域微调 embedding:在通用模型基础上用领域语料微调,对专业术语检索更准。

向量化后存入向量数据库。主流选择有 Milvus(大规模、分布式)、Qdrant(轻量、性能好)、PGVector(Postgres 扩展,适合已有 PG 的团队)。索引上普遍用 HNSW(分层小世界图)做近似最近邻检索,在召回率与速度间平衡良好。

# 向量化与入库(伪代码)
from sentence_transformers import SentenceTransformer
import faiss

model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

chunks = ["chunk1 文本...", "chunk2 文本..."]
embeddings = model.encode(chunks, normalize_embeddings=True)

dim = embeddings.shape[1]
index = faiss.IndexHNSWFlat(dim, 32)
index.add(embeddings)
# 检索
query_vec = model.encode(["用户问题"], normalize_embeddings=True)
scores, ids = index.search(query_vec, k=5)

四、检索:从纯向量到混合检索

纯向量检索擅长语义匹配("怎么提高接口性能" 能匹配到 "优化 QPS"),但对精确关键词(产品名、错误码、型号)不敏感。BM25 等关键词检索则相反。生产环境几乎都用混合检索(Hybrid Search):同时跑向量检索与 BM25,把两路结果按权重融合。

4.1 融合策略:RRF

倒数排名融合(Reciprocal Rank Fusion)是常用的无参融合方法:对每个文档,取它在两路检索中的排名,按 1/(k+rank) 求和作为最终分数。k 通常取 60。RRF 不依赖原始分数的尺度,鲁棒性强。

# RRF 融合
def rrf(rankings, k=60):
    scores = {}
    for ranking in rankings:        # 多路检索结果
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] = scores.get(doc, 0) + 1 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

final = rrf([vector_results, bm25_results])
混合检索是 RAG 质量的分水岭。在 mrgr.cn 的评测中,引入 BM25 + 向量 + RRF 后,Top-5 召回率比纯向量提升约 18%,尤其是带专有名词的查询提升更明显。

五、重排序:精排提升 Top-K 质量

检索阶段为了速度用了轻量模型,召回的 Top-K 里难免混入"看起来相关实则跑题"的片段。重排序(Rerank)用一个更强的交叉编码器(cross-encoder)对 query 与每个候选片段做精细化打分,重新排序后只取最相关的几个送入生成。Rerank 模型如 bge-reranker、Cohere Rerank 计算量大但精度高,通常只对 Top-20 以下的候选做精排。

  • 检索(双塔):query 与文档分别编码,用向量距离近似,快但粗。
  • 重排(交叉):query 与文档拼接后送入模型,交互更充分,慢但准。
  • 典型流程:向量+BM25 召回 Top-50 → Rerank 精排取 Top-5 → 送入生成。

六、生成与引用追溯

最后一步是把检索到的片段拼进 prompt,让大模型生成答案。工程上的关键不是 prompt 模板,而是引用追溯幻觉控制。要求模型在每个论断后标注来源片段编号,并在前端把答案与来源片段高亮关联,让用户可点击核查。同时明确指示模型:当上下文中没有相关信息时,必须回答"不知道",而非编造。

prompt = f"""你是一个严谨的知识助手,请仅基于以下资料回答问题。
若资料中没有答案,请直接回答"根据现有资料无法回答",不得编造。

【资料】
{[f'[{i+1}] {c.text}' for i, c in enumerate(chunks)]}

【问题】{question}

请在回答中用 [编号] 标注引用来源,例如:xxx[1]。"""

七、质量评测与持续优化

RAG 上线后必须有量化评测。常用指标:召回率(Recall@K,检索是否召回了正确片段)、答案准确率(生成是否正确)、引用命中率(引用是否指向正确来源)。构建一个标注好的评测集,每次切分或模型调整后回归测试,避免"改了一处、坏了全局"。检索与生成的优化要分开评测,否则定位不了问题出在哪一环。

  • 检索差 → 优化切分、换 embedding、调混合检索权重、加 Rerank。
  • 检索好但答案差 → 优化 prompt、换更强生成模型、控制上下文长度。
  • 引用不准 → 强化引用指令、对无引用答案做惩罚、补充来源校验。

结语

RAG 不是"接个向量库"的简单集成,而是一条完整的工程链路。切分决定下限,检索决定上限,重排与引用决定可信度。在 mrgr.cn 的实践中,把切分、检索、重排、生成分别做成可独立评测与替换的模块,是 RAG 长期可维护的关键。下一篇我们会从零搭建一个领域问答系统,把 RAG 落地为可交互的产品,欢迎持续关注。

返回文章列表