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。

下一篇就讲它们。