RAG 解决了什么问题
大模型有一个天生的短板:它的知识截止到训练那天,而且没法直接读你公司内部的文档。你想让它回答「我们公司的报销流程是什么」「这份合同里关于违约金怎么写」,它要么一本正经地编,要么说自己不知道。微调可以塞进新知识,但成本高、更新慢,文档一改就得重新训练。RAG(检索增强生成)换了个思路:先从你的文档库里把相关内容找出来,再连同问题一起塞进 prompt,让模型基于检索到的资料回答。
这个思路的好处很直接:知识是实时的,文档更新只要重新建索引就行;答案有出处,用户可以点回原文核对;不需要动模型本身,换任意一个大模型都能复用同一套检索。代价是你得维护一套文档处理与检索的流水线,而且检索质量直接决定回答质量——检索不到相关内容,再强的模型也答不对。
RAG 的本质不是「让模型更聪明」,而是「给模型递正确的参考资料」。检索环节做不好,后面全是白费。
- RAG通过先检索文档再生成答案,解决大模型知识过时与私有数据缺失的问题。
- 文档切分把长文档切成合适大小的片段,是检索质量的第一道关。
- 向量检索用 Embedding 把文本变成向量,按语义相似度召回相关片段。
- 引用溯源让答案标注来源,是企业场景可信度的底线要求。
文档切分与 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 模型,或者在预处理时做术语翻译。
这些问题没有银弹,每一个都需要拿你自己的真实问答对去测,然后针对性地改流水线的某一环。
评测与线上监控
RAG 系统好不好用,不能靠主观感觉。你需要一组评测集:收集几十上百个真实用户问题,每个问题配上标准答案和应该命中的文档片段,定期跑一遍。
评测要看两个层面:检索准不准、答案对不对。检索层面看召回率——标准答案里提到的文档片段,有多少被 top-k 捞回来了;答案层面看生成的回答和标准答案的一致性,以及有没有胡编。线上跑起来之后,还要持续收集用户的反馈(有用 / 没用),把差案例补进评测集,形成闭环。
# RAG 评测的最小指标
def evaluate(question, expected_sources, expected_answer):
retrieved = vector_db.search(question, top_k=5)
hit = len(set(expected_sources) & set(retrieved.sources))
recall = hit / len(expected_sources)
answer = generate_with_rag(question)
faithfulness = check_grounded(answer, retrieved) # 答案是否都有出处
relevancy = judge(answer, expected_answer)
return {"recall": recall, "faithfulness": faithfulness, "relevancy": relevancy}
把这三个指标定期跑出来,你改切分、改 Embedding 模型、改 prompt 时,就有客观依据判断是变好还是变差了。
从 demo 到生产的落地路径
如果要把 RAG 真正用到业务里,建议按下面的顺序推进:
- 先选文档范围:挑一个文档量小、问答场景明确的业务线起步,不要一上来就把全公司文档全塞进去。
- 手动跑通检索:选 20 个真实问题,手动看向量库召回的片段对不对,先把切分和检索调到可用。
- 再接大模型生成:检索靠谱之后,再把大模型接进来,调好 prompt 里的约束和引用格式。
- 建评测集:积累 50 条以上问答对,把评测流程自动化。
- 上线灰度:先给一小部分用户用,收集反馈,持续把 bad case 补进评测集优化。
RAG 不是一个「搭好就完」的系统,而是一个需要持续喂数据、持续调检索的工程。早期把评测和反馈闭环搭好,比一开始追求多复杂的检索算法更重要。
RAG 的工程价值在于:它把大模型的不可控,用检索这一层可控的数据补上了。检索这层越扎实,整个系统就越靠谱。