RAG 从 0 到 1:让 LLM 基于你的数据回答
企业 AI 应用 90% 都是 RAG。这一篇带你搭一个能跑的 RAG 系统——从分块到部署。
L0-04 我们讲过:RAG 是 LLM 幻觉的最实用解药。
RAG 可视化 我们看过整个流程。 这一篇打开每一步——从零搭建一个能跑的 RAG 系统。
你可以照着这一篇用 100 行 Python 跑出来。
为什么需要 RAG
裸 LLM 的两大问题:
- 知识有截止日期——训练数据到某个时间为止,之后的事不知道
- 不知道你的私有数据——你公司文档、你客户邮件、你 wiki
RAG 是怎么解决的?
检索(Retrieval) → 增强(Augmented) → 生成(Generation)
把 LLM 当作”会读材料、会写人话”的实习生。 你不让它凭记忆答,你给它材料让它现读现答。
整个流程(5 步)
1. 索引阶段(离线一次性)
你的文档 → 分块 → 向量化 → 存到向量库
2. 查询阶段(每次用户提问)
用户问题 → 向量化 → 在向量库里查相似 → 取 top-K → 塞进 prompt → LLM 回答
一步一步搭
Step 1:准备文档
假设你有一个企业 Wiki:
docs/
├── product_intro.md
├── pricing.md
├── faq.md
├── tutorial.md
└── ...
读进 Python:
import os
documents = []
for filename in os.listdir('docs/'):
with open(f'docs/{filename}', 'r', encoding='utf-8') as f:
documents.append({
'source': filename,
'text': f.read()
})
Step 2:分块(Chunking)
为什么不直接整篇塞 LLM?
- LLM 上下文有限(GPT-4 是 128k token)
- 每次塞太多内容浪费钱
- “找精确的小段” 比 “塞一整篇” 检索准
分块策略(按从简到难):
A. 固定长度分块
def fixed_chunk(text, chunk_size=500, overlap=50):
chunks = []
for i in range(0, len(text), chunk_size - overlap):
chunks.append(text[i:i + chunk_size])
return chunks
简单粗暴,但可能把一个完整段落切碎。
B. 按段落分块
def paragraph_chunk(text):
return [p.strip() for p in text.split('\n\n') if p.strip()]
更智能,但段落长度不均匀。
C. 递归分块(实战首选)
LangChain 的 RecursiveCharacterTextSplitter:先按段落切,太长就按句子,再太长按字符。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ".", " ", ""]
)
chunks = splitter.split_text(text)
D. 语义分块(高级)
用 embedding 模型判断”哪几句话语义连贯,放一起”。效果最好,工程最复杂。
一个 RAG 系统效果好不好,70% 在 chunking 策略。这是最容易被忽视的工程细节。
Step 3:向量化(Embedding)
把每个 chunk 转成向量。
用 OpenAI Embeddings
from openai import OpenAI
client = OpenAI()
def embed(text):
resp = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return resp.data[0].embedding # 1536 维向量
vectors = [embed(chunk) for chunk in chunks]
用开源(不花钱)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-large-zh-v1.5') # 中文最强之一
vectors = model.encode(chunks)
主流 embedding 模型对比(2026):
| 模型 | 维度 | 优势 | 用途 |
|---|---|---|---|
| OpenAI text-embedding-3 | 1536/3072 | 通用、英文最强 | 通用 RAG |
| Cohere embed-multilingual-v3 | 1024 | 多语言 | 跨语言 RAG |
| BGE (BAAI) | 1024 | 开源中文最强 | 中文 RAG |
| Voyage AI | 1024 | 长文本好 | 复杂场景 |
选 embedding 模型比选 LLM 重要——一个 RAG 系统的检索精度首先取决于 embedding 模型。
Step 4:存到向量库
不能每次查询都用 numpy 计算几万次余弦相似度——慢。需要专门的向量数据库。
选择
| 数据库 | 类型 | 何时用 |
|---|---|---|
| FAISS(Facebook 开源) | 本地库 | 原型、单机 |
| ChromaDB | 本地/远程 | 中小型 |
| Pinecone | 云服务 | 生产、托管 |
| Weaviate | 自托管/云 | 复杂查询 |
| Qdrant(Rust 写的) | 自托管/云 | 性能首选 |
| Milvus | 自托管 | 超大规模 |
| pgvector(PostgreSQL 扩展) | 嵌入现有 PG | 不想加新依赖 |
用 ChromaDB 起步
import chromadb
client = chromadb.Client()
collection = client.create_collection("my_docs")
# 添加
collection.add(
documents=chunks, # 原始文本
embeddings=vectors, # 向量(也可让 Chroma 自己生成)
metadatas=[{'source': doc['source']} for doc in documents for _ in chunks_of(doc)],
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
Step 5:检索
用户提问时:
def retrieve(query, k=5):
query_vec = embed(query)
results = collection.query(
query_embeddings=[query_vec],
n_results=k
)
return results['documents'][0] # 返回 top-k chunk
Step 6:(可选但强烈推荐)Rerank
检索回来的 top-K 不一定都好用——可能 top-1 比 top-5 差。
加一个 rerank(重排) 模型——用更精确的模型对 top-N 打分,挑出真正最相关的 top-K:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-large')
def rerank(query, candidates, top_k=3):
pairs = [(query, c) for c in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c for c, _ in ranked[:top_k]]
经典做法:retrieve top-20 → rerank → top-3。这一步显著提高 RAG 准确率。
Step 7:组装 prompt + 调 LLM
def rag_answer(query):
# 1. 检索
candidates = retrieve(query, k=20)
# 2. 重排
top_chunks = rerank(query, candidates, top_k=3)
# 3. 拼 prompt
context = "\n\n".join([f"[{i+1}] {chunk}" for i, chunk in enumerate(top_chunks)])
prompt = f"""请基于下面的资料回答用户问题。
如果资料里没有相关信息,请明确说"我不知道"。
【资料】
{context}
【用户问题】
{query}
【你的回答】"""
# 4. 调 LLM
resp = client.chat.completions.create(
model="claude-haiku-4-5",
messages=[{"role": "user", "content": prompt}]
)
return resp.choices[0].message.content
完整 RAG,约 100 行代码。
真实部署里的”坑”
理论 RAG 很简单,工程 RAG 都是细节:
1. 复杂查询拆解
用户问”对比一下我们 A 产品和 B 产品的性价比”—— 单纯检索”A 产品” 或 “B 产品” 都不够。 需要先用 LLM 拆解查询,分别检索,再综合。
2. 元数据过滤
“我们 2024 年的政策是什么”—— 不应该把 2023、2022 的检索结果混进来。 向量库要支持按元数据(年份、部门)过滤。
3. 混合搜索(Hybrid Search)
向量搜索擅长”语义相似”,但对精确关键词(产品 SKU、人名、专有名词)不擅长。 解决:同时跑 BM25(词项匹配)和向量搜索,融合结果。
4. Chunk 太小或太大
太小(100 字)——上下文丢失。 太大(2000 字)——稀释了关键信息。 经验值:300-800 字 + 50 字 overlap。
5. 引用与可溯源
LLM 答完,要给出”答案来自哪个 chunk”——让用户能溯源。
# 在 prompt 里要求模型引用
"请在每个论断后标注 [1] [2] 等来源编号"
6. 性能优化
- 缓存:相同问题的检索结果缓存
- 异步:检索和 prompt 拼装并行
- 流式:LLM 边生成边返回
7. 评估
怎么知道 RAG 系统好不好?
- 召回率:相关 chunk 是否被检索到(需要标注 ground truth)
- 生成质量:人工评 / LLM 评(用 GPT-4 做评委)
- 响应时间:从问到答多久
- 成本:每次查询多少美元
建议用 RAGAS 库(开源 RAG 评估框架)来做。
生态工具
不想从 0 写?用框架:
| 工具 | 特点 |
|---|---|
| LangChain | 最流行、生态最大、但代码冗长 |
| LlamaIndex | 专为 RAG 设计、API 优雅 |
| Haystack(Deepset) | 老牌、企业级 |
| Verba(Weaviate 出) | UI 现成 |
| Dify | 低代码、可视化 |
起步推荐:自己用 LangChain / LlamaIndex 写一遍,理解每一步。生产推荐:根据团队和场景选——没有一招通吃的。
真实业务里的 RAG
企业 AI 应用 90% 都是 RAG。
- 客服机器人:检索 FAQ + 历史工单
- 法律助手:检索法条 + 判例
- 医疗咨询:检索医学指南 + 病历
- 公司 AI 知识库:员工问”我们公司怎么做出差报销”
- AI 写作助手:检索品牌指南 + 历史文章
一个项目从”概念验证”到”上线”,技术上 90% 在调 RAG——分块策略、embedding 选型、rerank、prompt 优化、评估。
2026 年:
- 几乎所有企业 AI 项目都基于 RAG
- 几乎所有 AI 创业公司都在做某种 “X 行业的 RAG”
- 大公司在用更复杂的变体(GraphRAG、Agentic RAG)
学会 RAG = 学会 LLM 应用工程。
下一篇:《LoRA 微调入门》 —— RAG 让 LLM “知道” 你的数据;微调让 LLM “成为” 你想要的样子。
读到这里说明你认真在学 🎯
订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。
讨论区
· 用 GitHub 账号登录评论src/components/Comments.astro 顶部填入
仓库 ID 和分类 ID(见组件注释里的配置步骤)。