RAG 检索增强生成:让模型用上你的语料


文档摘要

RAG 检索增强生成:让模型用上你的语料 本节摘要:你的 LLM 知道训练截止日之前的一切,却对公司内部文档、你的代码库、上周的会议纪要一无所知。RAG(检索增强生成,Retrieval-Augmented Generation)解决的就是这个问题:在回答前先检索相关文档,把它们拼进提示,让模型基于这些段落作答。它是生产 AI 里部署最广的模式,如果你整套课程只做一件东西,就做一条 RAG 管线。

RAG 检索增强生成:让模型用上你的语料

本节摘要:你的 LLM 知道训练截止日之前的一切,却对公司内部文档、你的代码库、上周的会议纪要一无所知。RAG(检索增强生成,Retrieval-Augmented Generation)解决的就是这个问题:在回答前先检索相关文档,把它们拼进提示,让模型基于这些段落作答。它是生产 AI 里部署最广的模式,如果你整套课程只做一件东西,就做一条 RAG 管线。本节带你吃透 RAG 的全貌:为什么它比微调更适合知识接地型应用(成本、新鲜度、可审计、可与任意模型搭配);嵌入模型如何把文本变成稠密向量;三种向量相似度(余弦、点积、L2)的差异;固定/语义/递归三种分块策略与 256~512 token 的甜蜜区;向量数据库的选型;以及一条从零实现的「分块-TF-IDF 嵌入-余弦检索-提示增强-生成」完整管线,只换两个函数就能接 OpenAI/Anthropic 真实模型。

对应原课程:Phase 11 · Lesson 06 · rag(原英文 phases/11-llm-engineering/06-rag/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 搭建一条完整的 RAG 管线:文档加载、分块、嵌入、向量存储、检索、生成。
  2. 用向量数据库(Chroma、FAISS、Pinecone)实现带正确索引的语义搜索
  3. 说清为什么 RAG 比微调更适合知识接地型应用(成本、新鲜度、可归因)。
  4. 用检索指标(精确率、召回率)和生成指标(忠实度、相关度)评估 RAG 质量。

一、问题与直觉

你给公司做了个客服聊天机器人。客户问:「企业版的退款政策是什么?」LLM 给出了一段关于「典型 SaaS 退款政策」的泛泛回答。而真正的政策——企业客户有 60 天窗口、按比例退款——埋在一份 200 页的内部 wiki 里。LLM 从没见过这份文档,它不可能知道训练数据里没有的东西。

微调是一种解法:把 LLM 拿来,在你的内部文档上训练,部署更新后的模型。可行,但问题严重:每次训练要花几千美元算力;文档一改模型就过时;你无法追溯答案来自哪个来源;下个月公司收购新产品线,又得重训一次。

RAG 是另一种解法:模型原封不动。问题进来时,搜索你的文档库找相关段落,把它们贴在问题前面拼进提示,让模型基于这些段落作答。文档库几分钟就能更新;你能看到具体检索到了哪些文档;模型本身永不改变。这就是 RAG 在生产里占主导的原因:更便宜、更新鲜、更可审计、可与任意 LLM 搭配。

RAG 范式

整个模式只有四步:

查询 → 检索 → 增强提示 → 生成。每个 RAG 系统都遵循这个模式。生产级 RAG 之间的差异,全在每一步的细节:怎么分块、怎么嵌入、怎么搜、怎么拼提示。

为什么 RAG 胜过微调

关注点 微调 RAG
成本 每次训练 1000~100000 美元以上 每次查询 0.01~0.10 美元(嵌入 + LLM)
新鲜度 重训前一直过时 重新索引文档,几分钟更新
可审计 无法追溯答案来源 能展示具体检索到的段落
幻觉 依然自由幻觉 锚定于检索文档
数据隐私 训练数据烘焙进权重 文档留在你的向量库里

微调永久改变模型权重,RAG 临时改变模型上下文。对多数应用,临时上下文才是你要的。

微调胜出的唯一场景:你需要模型习得某种特定的风格、语气或推理模式,而这种模式靠提示做不到。对事实知识检索,RAG 永远胜出。

嵌入模型

嵌入模型把文本转成稠密向量。语义相近的文本在这个高维空间里向量也靠近。「如何重置密码?」和「我需要改密码」几乎没有共同词,却产生几乎相同的向量;而「猫坐在垫子上」的向量则截然不同。

主流嵌入模型(2026 年阵容,详见第 5 章 22 节):

模型 维度 提供方 备注
text-embedding-3-small 1536(Matryoshka) OpenAI 多数场景的最佳性价比
text-embedding-3-large 3072(Matryoshka) OpenAI 精度更高,可截到 256/512/1024
Gemini Embedding 2 3072(Matryoshka) Google MTEB 检索榜首,8K 上下文
voyage-4 1024/2048(Matryoshka) Voyage AI 有代码/金融/法律领域变体
Cohere embed-v4 1024(Matryoshka) Cohere 多语言强,128K 上下文
BGE-M3 1024(稠密+稀疏+ColBERT) 智源(开源) 一个模型三种视图
Qwen3-Embedding 4096(Matryoshka) 阿里(开源) 开源检索分榜首
all-MiniLM-L6-v2 384 开源(Sentence Transformers) 原型基线

本节我们用 TF-IDF 自己做一个简单嵌入。不是因为生产系统用 TF-IDF,而是因为它把概念变得具体:文本进去,向量出来,相似文本产生相似向量。

向量相似度

给定两个向量,怎么量相似度?三种选择。

余弦相似度:两向量夹角的余弦,范围 -1(相反)到 1(相同)。忽略模长,只看方向。这是 RAG 的默认选择。

cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)

点积:原始内积。模长大的向量得分更高。当模长承载信息时(更长的文档可能更相关)有用。

dot(a, b) = sum(a_i * b_i)

L2(欧氏)距离:向量空间里的直线距离。距离越小越相似,对模长差异敏感。

L2(a, b) = sqrt(sum((a_i - b_i)^2))

余弦相似度是标准。它优雅地处理不同长度文档,因为它按模长归一化。当有人说「向量搜索」,几乎总是指余弦相似度。

分块策略

文档太长,不能整篇嵌成一个向量。一份 50 页 PDF 嵌出来的向量会很糟,因为它涉及几十个主题。正确做法是把文档切成块,每块单独嵌。

固定大小分块:每 N 个 token 切一刀。简单可预测。512 token 块、50 token 重叠,意味着块 1 是 token 0511,块 2 是 token 462973,依此类推。重叠确保你不会在倒霉的边界上把句子劈成两半。

语义分块:在自然边界切。段落、小节、Markdown 标题。每块是一个连贯的语义单元。实现更复杂,但检索质量更好。

递归分块:先尝试在最大的边界切(小节标题);某节还是太大就在段落边界切;某段还是太大就在句子边界切。这是 LangChain RecursiveCharacterTextSplitter 的思路,实战效果好。

块大小比人们以为的更重要:

  • 太小(64~128 token):每块缺上下文。「上季度增长了 15%」脱离了「它」指代什么,毫无意义。
  • 太大(2048+ token):每块涵盖多个主题,稀释相关性。你搜营收数据,却拿到一块 10% 讲营收、90% 讲人头的。
  • 甜蜜区(256~512 token):上下文足够自洽,焦点足够相关。

多数生产 RAG 系统用 256~512 token 块、50 token 重叠。Anthropic 的 RAG 指南推荐这个区间。

向量数据库

有了嵌入,得有个地方存和搜。选项:

数据库 类型 适合
FAISS 库(进程内) 原型、中小数据集
Chroma 轻量数据库 本地开发、小规模部署
Pinecone 托管服务 不想运维的生产
Weaviate 开源数据库 自托管生产
pgvector Postgres 扩展 已经在用 Postgres
Qdrant 开源数据库 高性能自托管

本节我们做一个简单的内存向量库:把向量存在列表里,做暴力余弦搜索。这等价于 FAISS 的 flat 索引,大概能撑到 10 万向量才变慢。生产系统用 HNSW 这类近似最近邻(ANN)算法,毫秒级搜百万向量。

完整管线

索引阶段每份文档跑一次(或文档更新时跑);查询阶段每次请求都跑。生产里,索引可能数小时处理百万文档;查询必须在 1 秒内响应。

真实参数

多数生产 RAG 系统用这些参数:

  • k = 5~10:每次查询检索的块数。
  • 块大小 = 256~512 token,50 token 重叠。
  • 上下文预算:每次查询 2500~5000 token 检索内容。
  • 总提示:约 8000~16000 token(系统提示 + 检索块 + 历史 + 查询)。
  • 嵌入维度:384~3072,视模型而定。
  • 索引吞吐:用 API 嵌入时 100~1000 文档/秒。
  • 查询延迟:检索 50200ms,生成 5003000ms。

二、从零实现

完整代码见原课程 code/rag_pipeline.py,这里给出关键骨架。

步骤 1:文档分块

def chunk_text(text, chunk_size=200, overlap=50): words = text.split() chunks, start = [], 0 while start < len(words): end = start + chunk_size chunks.append(" ".join(words[start:end])) start += chunk_size - overlap # 步长 = 块大小 - 重叠 return chunks

步骤 2:TF-IDF 嵌入

TF-IDF(词频-逆文档频率)不是神经嵌入,但它把文本转成向量,且能体现词的重要性:文档里高频的词 TF 高,整个语料里罕见的词 IDF 高,乘积让重要且独特的词拿到高值。

import math from collections import Counter def build_vocabulary(documents): vocab = set() for doc in documents: vocab.update(doc.lower().split()) return sorted(vocab) def compute_tf(text, vocab): words = text.lower().split() count = Counter(words) total = len(words) return [count.get(w, 0) / total for w in vocab] def compute_idf(documents, vocab): n = len(documents) idf = [] for w in vocab: doc_count = sum(1 for doc in documents if w in doc.lower().split()) idf.append(math.log((n + 1) / (doc_count + 1)) + 1) # 平滑 return idf def tfidf_embed(text, vocab, idf): tf = compute_tf(text, vocab) return [t * i for t, i in zip(tf, idf)]

步骤 3:余弦相似度搜索

def cosine_similarity(a, b): dot = sum(x * y for x, y in zip(a, b)) norm_a = math.sqrt(sum(x * x for x in a)) norm_b = math.sqrt(sum(x * x for x in b)) if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b) def search(query_embedding, stored_embeddings, top_k=5): scores = [(i, cosine_similarity(query_embedding, emb)) for i, emb in enumerate(stored_embeddings)] scores.sort(key=lambda x: x[1], reverse=True) return scores[:top_k]

步骤 4:提示构造

「增强」就发生在这里。把检索到的块格式化进提示,要求 LLM 只基于给定上下文作答。

def build_rag_prompt(query, retrieved_chunks): context = "\n\n---\n\n".join( f"[来源 {i+1}]\n{chunk}" for i, chunk in enumerate(retrieved_chunks) ) return f"""只根据以下上下文回答问题。 若上下文信息不足,就说"我没有足够的信息来回答这个问题"。 上下文: {context} 问题:{query} 答案:"""

💡 「只根据上下文」这句护栏很关键。没有它,模型会偷偷调用训练知识填补空白,产生「看起来对、其实编造」的答案——这正是 RAG 要消灭的幻觉。

步骤 5:完整 RAG 管线

class RAGPipeline: def __init__(self): self.chunks, self.embeddings = [], [] self.vocab, self.idf = [], [] def index(self, documents): all_chunks = [] for doc in documents: all_chunks.extend(chunk_text(doc)) self.chunks = all_chunks self.vocab = build_vocabulary(all_chunks) self.idf = compute_idf(all_chunks, self.vocab) self.embeddings = [tfidf_embed(c, self.vocab, self.idf) for c in all_chunks] def query(self, question, top_k=5): q_emb = tfidf_embed(question, self.vocab, self.idf) results = search(q_emb, self.embeddings, top_k) retrieved = [(self.chunks[i], score) for i, score in results] prompt = build_rag_prompt(question, [c for c, _ in retrieved]) return prompt, retrieved

步骤 6:生成(模拟)

生产中这里调 LLM API。本节用一个简化版:从检索上下文里抽出与查询重叠最多的句子。

def simple_generate(prompt, retrieved_chunks): query_words = set(prompt.lower().split("问题:")[-1].split()) best_sentence, best_score = "", 0 for chunk in retrieved_chunks: for sentence in chunk.split("。"): sentence = sentence.strip() if not sentence: continue words = set(sentence.lower().split()) overlap = len(query_words & words) if overlap > best_score: best_score, best_sentence = overlap, sentence return best_sentence if best_score else "我没有足够的信息。"

三、框架对比

换成真实嵌入模型和 LLM,代码几乎不变:

# from openai import OpenAI # client = OpenAI() # def embed(text): # r = client.embeddings.create(model="text-embedding-3-small", input=text) # return r.data[0].embedding # def generate(prompt): # r = client.chat.completions.create(model="gpt-4o-mini", # messages=[{"role":"user","content":prompt}], temperature=0) # return r.choices[0].message.content

Anthropic 同理:

# import anthropic # client = anthropic.Anthropic() # def generate(prompt): # r = client.messages.create(model="claude-sonnet-5", max_tokens=1024, # messages=[{"role":"user","content":prompt}]) # return r.content[0].text

管线不变:换嵌入函数、换生成函数。检索逻辑、分块、提示构造——无论用哪家模型都一样。

大规模向量存储,把暴力搜索换成正经向量库:

# import chromadb # client = chromadb.Client() # collection = client.create_collection("my_docs") # collection.add(documents=chunks, ids=[f"chunk_{i}" for i in range(len(chunks))]) # results = collection.query(query_texts=["退款政策是什么?"], n_results=5)

Chroma 内部处理嵌入(默认用 all-MiniLM-L6-v2),把向量存进本地数据库。模式相同,管道不同。

LangChain 与 LlamaIndex 提供更高层抽象:RecursiveCharacterTextSplitter 做分块,VectorStoreRetriever 做检索,RetrievalQA 链把检索与生成串起来。优点是少写胶水代码,代价是把分块大小、重叠、k 值这些关键旋钮藏在默认值后面——你不调,就吃它的默认。本节的从零管线让你看见每一个旋钮

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-rag-architect.md:一个元提示,为特定用例设计 RAG 系统。喂给它你的语料规模、查询类型、延迟预算,它给出分块策略、嵌入模型、k 值、提示模板的建议。
  • skill-rag-pipeline.md:一个技能文件,教 Agent 如何搭建和调试 RAG 管线。

Python 代码(code/rag_pipeline.py)是一条独立管线,把 tfidf_embed 换成真实嵌入 API、simple_generate 换成真实 LLM,即可投产;分块、检索、提示构造逻辑无需改动。

五、练习

  1. 换词袋嵌入:把 TF-IDF 换成简单的二值词袋(词出现为 1,不现为 0),在样例文档上对比检索质量。TF-IDF 应当胜出,因为它给罕见词更高权重。

  2. 块大小实验:在同一文档集上试 50、100、200、500 词四种块大小。每种跑同样 5 条查询,统计有多少查询在 top-3 里返回了相关块。找出检索质量峰值的甜蜜区。

  3. 加元数据与溯源:给每块加元数据(源文档名、块位置)。修改提示模板加入来源归因,让 LLM 引用来源。

  4. 检索评估:给定 10 个问答对,让每个问题过 RAG 管线,测量有多少比例的检索块包含答案——这就是 retrieval recall@k。

  5. 对话感知 RAG:维护最近 3 轮对话历史,与检索块一起放进提示。用「那企业版呢?」这种追问测试,看历史上下文是否让追问被正确理解。

本节要点回顾

  1. RAG 解决「模型不知道你的数据」:训练截止后的、私有的、会变的知识,靠检索注入而非烘焙进权重。
  2. 四步范式:查询 → 检索 → 增强提示 → 生成,所有 RAG 系统都是这个骨架,差异在每步细节。
  3. RAG 多数场景胜过微调:更便宜(0.01~0.10 美元/查询 vs 千美元训练)、更新鲜(分钟级)、可审计(能看检索到了什么)、不幻觉(锚定文档)。
  4. 微调只在「习得风格/推理模式」时胜出:事实知识检索永远选 RAG。
  5. 嵌入把文本变成可比的向量:相似语义 → 相近向量;2026 主流模型多为 Matryoshka,可按需截短维度。
  6. 余弦相似度是默认:按模长归一化,优雅处理不同长度文档。
  7. 块大小是关键旋钮:256~512 token、50 重叠是甜蜜区;太小缺上下文,太大稀释相关性。
  8. 暴力搜索撑到 10 万向量:再大就上 HNSW 这类 ANN,毫秒级搜百万向量。
  9. 「只根据上下文」是防幻觉护栏:没这句,模型会偷用训练知识填空。
  10. 管线与模型解耦:换嵌入函数、换生成函数,检索/分块/提示逻辑无需改动。

下一节,我们进入进阶 RAG——更聪明的分块、混合检索(稠密 + 稀疏 BM25)、重排序、查询改写,把这条朴素管线打磨成生产级。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U