RAG(检索增强)
RAG可以用来补充外部知识与上下文
补充整理:下面是课后补充的完整说明与可运行示例,用于复习时对照。
为什么需要 RAG
大模型有三个绕不开的短板,RAG 正是用来补这些洞的:
| 短板 | 表现 | RAG 的解法 |
|---|---|---|
| 知识有截止日期 | 不知道训练之后发生的事 | 外部知识库随时更新,检索即得 |
| 容易幻觉 | confidently 编造不存在的细节 | 回答基于检索到的原文,可溯源 |
| 不懂私有数据 | 没见过公司内部文档、业务库 | 把私有语料切分入库再检索 |
一句话概括:RAG 不改动模型参数,而是在提问时把相关资料一起塞进上下文,让模型「照着材料答题」。
整体流程
text
离线(建库) 在线(问答)
文档 → 切分 chunk → 向量化 → 向量库 问题 → 向量化 → 召回 Top-K
↓
(可选)Rerank 重排
↓
拼进 Prompt → LLM → 答案 + 引用来源1. 切分(Chunking)
- 切太大:召回的内容塞不进上下文,噪声多。
- 切太小:语义被截断,检索时丢失关键句。
- 常用策略:按固定长度切 + 相邻块之间保留 overlap(重叠部分)避免切断句子。
2. 向量化(Embedding)
把文本映射成高维向量,语义相近的文本在空间里距离更近。检索阶段把问题也向量化,用余弦相似度找最相近的片段。
3. 召回与重排
- 召回:向量库返回 Top-K 个候选片段,追求「别漏掉」。
- 重排(Rerank):用更精细的交叉编码模型对候选重新打分,追求「排得准」。
- 两者配合是工业界标配:粗排要快、精排要准。
最小可运行示例(LangChain)
python
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# 1. 建库:切分 → 向量化
raw_text = "公司规定:每年可申请 10 天带薪年假,需提前 3 个工作日在 OA 提交。"
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30)
docs = splitter.create_documents([raw_text])
vectorstore = FAISS.from_documents(docs, OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 2. 问答:检索到的内容拼进 prompt
prompt = ChatPromptTemplate.from_messages([
("system", "只依据下列资料回答问题,资料中没有的就说不知道。\n\n资料:\n{context}"),
("human", "{question}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
print(chain.invoke("年假要提前几天申请?"))关键点:
retriever在链里当「数据源」用,自动把召回结果格式化成context。- system prompt 里加「没有就说不知道」能显著压住幻觉。
- 本地部署可把
ChatOpenAI换成 vLLM 的 OpenAI 兼容接口,只改base_url。
常见坑
- 检索质量差:先别急着换模型,八成是切分和 embedding 的问题。
- 上下文溢出:Top-K 给太大,塞爆窗口,反而不如少而精。
- 没有溯源:生产环境一定要把命中的原文片段一起返回,方便人工核验。
