RAG 解决了什么问题

大模型有一个天生的短板:它的知识截止到训练那天,而且没法直接读你公司内部的文档。你想让它回答「我们公司的报销流程是什么」「这份合同里关于违约金怎么写」,它要么一本正经地编,要么说自己不知道。微调可以塞进新知识,但成本高、更新慢,文档一改就得重新训练。RAG(检索增强生成)换了个思路:先从你的文档库里把相关内容找出来,再连同问题一起塞进 prompt,让模型基于检索到的资料回答。

这个思路的好处很直接:知识是实时的,文档更新只要重新建索引就行;答案有出处,用户可以点回原文核对;不需要动模型本身,换任意一个大模型都能复用同一套检索。代价是你得维护一套文档处理与检索的流水线,而且检索质量直接决定回答质量——检索不到相关内容,再强的模型也答不对。

RAG 的本质不是「让模型更聪明」,而是「给模型递正确的参考资料」。检索环节做不好,后面全是白费。
本文核心知识点
  • RAG通过先检索文档再生成答案,解决大模型知识过时与私有数据缺失的问题。
  • 文档切分把长文档切成合适大小的片段,是检索质量的第一道关。
  • 向量检索用 Embedding 把文本变成向量,按语义相似度召回相关片段。
  • 引用溯源让答案标注来源,是企业场景可信度的底线要求。
RAG 配图:文档页经漏斗检索汇入中央发光芯片大脑,光束延伸至右侧回答气泡,蓝色渐变扁平风格
RAG 的最小闭环:用户提问 → 从文档库检索相关片段 → 把片段和问题一起交给大模型 → 生成带引用的答案。

文档切分与 Embedding

RAG 流水线的第一步是把原始文档变成可检索的片段。PDF、Word、网页都要先解析成纯文本,然后按一定规则切成小块。切分粒度很关键:块太大,检索回来的内容混杂不相关信息;块太小,又丢掉上下文,模型看不懂这段话在说什么。

切分策略做法适用场景
固定长度切分按字符数硬切,带重叠窗口格式不规整的长文、日志
按结构切分按标题、段落、表格边界切说明书、合同、技术文档
语义切分用模型判断语义边界再切文章、问答对、自由文本

切好之后,每个片段要过一遍 Embedding 模型,把文字变成一个固定长度的向量。向量的含义是:语义相近的文本,向量在空间里也靠近。用户提问时,同样把问题变成向量,去向量库里找距离最近的那几个片段。

# 文档切分与向量化的最小流程
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每块约 500 字
    chunk_overlap=50,      # 相邻块重叠 50 字,避免切断句子
    separators=["\n\n", "\n", "。", ",", " "],
)

chunks = splitter.split_text(document_text)

# 每个块生成向量并存入向量库
for chunk in chunks:
    vector = embedding_model.encode(chunk)
    vector_db.insert(text=chunk, vector=vector, source=doc_name)

切分参数没有万能值,要拿你自己的文档试:先按经验值跑一版,看检索回来的片段是不是真的相关,再调 chunk_size 和 overlap。

向量检索与上下文拼接

用户提问进来后,先把问题向量化,然后去向量库里召回 top-k 个最相近的片段。k 不是越大越好——塞太多片段进 prompt,既占上下文窗口,又会稀释重点。常见做法是召回 5 到 10 个片段,再做一次重排:用一个更精细的模型给这些片段和问题的相关性打分,只留最相关的 3 到 5 个。

重排之后,把选中的片段按顺序拼进 prompt,结构通常是:系统提示词告诉模型「只根据下面提供的资料回答,资料里没有就说不知道」,然后是检索到的资料,最后是用户的问题。这个结构看起来简单,但每一步都有讲究。

# 检索 + 拼接 prompt 的最小实现
def answer(question):
    q_vec = embedding_model.encode(question)
    candidates = vector_db.search(q_vec, top_k=10)
    reranked = rerank_model.score(question, candidates)[:5]

    context = "\n---\n".join(c.text for c in reranked)
    prompt = f"""请根据以下资料回答问题。如果资料中没有相关信息,直接说"未在知识库中找到"。

资料:
{context}

问题:{question}"""

    return llm.generate(prompt)

「资料里没有就说不知道」这句话是 RAG 防幻觉的关键。不加这句话,模型会硬着头皮根据自己的训练记忆编答案,你就失去了 RAG 的意义。

引用溯源与答案可信度

在企业场景里,用户不仅要一个答案,还要知道这个答案从哪来。RAG 的天然优势就是你检索过哪些片段是已知的,把它们的文档名和页码一起返回给前端,就能做成可点击的引用。

实现上有两种做法。一种是在生成时就让模型在句子后面标注引用编号,另一种是生成完之后,用检索到的片段和答案做一次对齐,自动找出答案里哪些句子对应哪些来源。前者简单但模型可能标错,后者更准但要多一步处理。生产环境通常两者结合:先让模型标注,再用规则校验引用编号是否在合法范围内。

引用设计的三条经验
  • 来源可点每个引用都能跳回原文对应位置,不要只给一个文档名。
  • 无引用不答检索结果为空时明确告知,不要让模型自由发挥。
  • 时效标注在答案旁标注文档最后更新时间,让用户判断信息是否过期。

引用溯源不是锦上添花,而是企业 RAG 能不能用的分界线。没有出处的回答,在法务、财务、医疗这些场景里根本不敢用。

常见问题与优化方向

跑通一个 RAG demo 只需要半天,但要做到好用,会反复遇到下面几类问题:

  • 检索不相关:用户问的和文档里写的用词不一样,向量相似度高但语义不对。对策是在检索前加一步查询改写,让模型把用户的口语化问题改写成更适合检索的表述。
  • 上下文被截断:长答案需要的信息分散在多个片段里,单召回一个片段不够。对策是用父子块策略:小块用于精准检索,命中后把它所在的大块一起送进上下文。
  • 文档更新不同步:业务方改了文档,向量库还是旧的。对策是给每个文档加版本号和更新时间,文档一变就触发增量重建索引。
  • 跨语言检索差:用户用中文问,文档里有英文术语,向量匹配不上。对策是用多语言 Embedding 模型,或者在预处理时做术语翻译。

这些问题没有银弹,每一个都需要拿你自己的真实问答对去测,然后针对性地改流水线的某一环。