RAG 是什么
Retrieval-Augmented Generation——检索增强生成。
LLM 不知道你的私有数据(公司文档、日记、笔记)。RAG 流程:
1. 索引阶段(一次性)
文档 → 切块 → 向量化 → 存向量库
2. 查询阶段(每次问)
用户问题 → 向量化 → 检索 top-k 相关块 → 喂给 LLM 一起回答
效果:让 LLM "知道"你的私有数据,不需要微调。
完整 RAG 实现(80 行能用版本)
from sentence_transformers import SentenceTransformer
from anthropic import Anthropic
import faiss
import numpy as np
from pathlib import Path
embed_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
client = Anthropic()
# ---------- 1. 切块 ----------
def chunk_text(text, size=400, overlap=80):
chunks = []
i = 0
while i < len(text):
chunks.append(text[i:i+size])
i += size - overlap
return chunks
# ---------- 2. 索引 ----------
class KnowledgeBase:
def __init__(self, dim=1024):
self.index = faiss.IndexFlatIP(dim)
self.chunks: list[str] = []
self.sources: list[str] = []
def add_file(self, path: Path):
text = path.read_text(encoding="utf-8")
chunks = chunk_text(text)
vecs = embed_model.encode(chunks, normalize_embeddings=True)
self.index.add(vecs.astype(np.float32))
self.chunks.extend(chunks)
self.sources.extend([path.name] * len(chunks))
print(f"{path.name}: 加入 {len(chunks)} 块")
def search(self, query, k=5):
q = embed_model.encode([query], normalize_embeddings=True).astype(np.float32)
scores, ids = self.index.search(q, k)
return [
{"text": self.chunks[i], "source": self.sources[i], "score": float(s)}
for i, s in zip(ids[0], scores[0])
]
# ---------- 3. 用 LLM 生成 ----------
def ask(kb, question):
hits = kb.search(question, k=5)
context = "\n\n".join(
f"[来源: {h['source']}]\n{h['text']}" for h in hits
)
prompt = f"""你是知识助手。根据下面提供的上下文回答用户问题。
- 答案必须来源于上下文
- 上下文里没的信息,回答"我不知道"
- 引用时注明来源(如 [来源: xxx.md])
上下文:
\"\"\"
{context}
\"\"\"
问题:{question}"""
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
# ---------- 4. 用起来 ----------
kb = KnowledgeBase()
for f in Path("./docs").glob("*.md"):
kb.add_file(f)
print(ask(kb, "Python 装饰器是什么?"))
切块策略
糟糕的切块直接毁掉 RAG——太碎丢失上下文,太长稀释相关度。
| 策略 | 适合 |
|---|---|
| 固定字数(上面那种) | 通用,最简单 |
| 按段落 / 句子 | 自然文本(小说 / 博客) |
| 按 Markdown 标题 | 文档 / 教程 |
| 按代码段 | 代码库 |
| Late chunking | 先 embed 整篇再切 |
实践:先固定 400-600 字 + 80 重叠,跑通后再优化。
检索:top-k 选多少
太少 → 漏掉信息;太多 → 噪声 + 上下文太长。
经验:k=5 起步。每块 400 字 × 5 = 2000 字 上下文——大多数 LLM 都吃得下。
重排(Rerank):精排前 30 → 选前 5
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large")
def search_with_rerank(kb, query, k=5):
candidates = kb.search(query, k=30)
pairs = [(query, c["text"]) for c in candidates]
rerank_scores = reranker.predict(pairs)
for c, s in zip(candidates, rerank_scores):
c["rerank"] = s
candidates.sort(key=lambda c: c["rerank"], reverse=True)
return candidates[:k]
向量召回 + cross-encoder 精排——是 2026 RAG 的标配组合。
评估 RAG
不能凭感觉。准备一组(问题, 期望答案)测试集:
- 召回率:相关 chunk 有没有进 top-k
- 答案准确性:跑 LLM 评分("这个回答 vs 期望,1-5 分")
- 来源标注率:模型有没有正确引用
何时选 LangChain / LlamaIndex
上面 80 行能跑——加上分页 / 多文档 / 持久化 / 缓存就要重新发明轮子。这时候用 LlamaIndex。
下一篇就讲它们。