Skip to content

RAG:让模型用上你的数据 ​

RAG(Retrieval-Augmented Generation)= 先检索,再生成。

它是当前落地最广的 AI 技术形态。原因很简单:知识会变、企业数据私有、模型无法重新训练。

一、完整链路 ​

离线建库(文档变了才重跑):

1
文档解析PDF/Word/HTML → 纯文本
2
清洗去页眉页脚、修表格
3
切分按结构切成 chunk
4
向量化每个 chunk → 向量
5
入库向量 + 原文 + 元数据

在线查询(每次请求都走):

1
问题改写补全指代、同义扩展
2
混合检索向量 + 关键词召回
3
重排精排出真正相关的几条
4
组装上下文带编号、控长度
5
生成带引用作答,允许说不知道

效果不好的时候,按这个链路的顺序逐段排查,而不是直接换模型。

二、切分(Chunking):最关键又最被忽视 ​

策略做法适用
固定长度每 500 字切一刀,留 50 字重叠通用基线
按结构切按标题/段落/表格切文档类首选
语义切分按句子相似度变化处切长段落、散文
父子切分小块检索,返回大块上下文精度与完整性的平衡
按问答对每条 FAQ 一个 chunk客服知识库
python
def chunk_by_heading(text: str, max_chars=600, overlap=80):
    """按 Markdown 标题切,超长再按段落二次切。输出带标题路径的 chunk。"""
    chunks, buf, path = [], [], []

    for line in text.splitlines():
        if line.startswith("#"):
            if buf:
                chunks.append((" > ".join(path), "\n".join(buf)))
                buf = []
            level = len(line) - len(line.lstrip("#"))
            path = path[:level - 1] + [line.strip("# ").strip()]
        else:
            buf.append(line)
    if buf:
        chunks.append((" > ".join(path), "\n".join(buf)))

    out = []
    for heading, body in chunks:
        if len(body) <= max_chars:
            out.append({"heading": heading, "text": body})
        else:
            for i in range(0, len(body), max_chars - overlap):
                out.append({"heading": heading, "text": body[i:i + max_chars]})
    return out

三个立竿见影的改进

  1. 每个 chunk 前置标题路径(产品手册 > 退款政策 > 时效)—— 大幅提升可检索性与答案准确性
  2. 保留重叠(10%–15%)—— 防止答案被切在边界
  3. 600–1000 字/块 —— 太小丢上下文,太大稀释精度

三、向量化与检索 ​

python
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("BAAI/bge-m3")     # 多语言、中文友好

def embed(texts):
    return model.encode(texts, normalize_embeddings=True, batch_size=32)

# 建库
embs = embed([c["text"] for c in chunks])       # (N, d),已归一化

# 查询:余弦相似度 = 内积
q = embed(["退款需要多久"])[0]
scores = embs @ q
top = np.argsort(-scores)[:20]
选择说明
embedding 模型中文优先 bge-m3 / bge-large-zh;多语言 multilingual-e5
向量库小规模(<10万)用 FAISS / numpy 足够;生产用 Milvus/Qdrant/pgvector
归一化归一化后内积 = 余弦,省一次计算

混合检索:向量 + 关键词 ​

纯向量检索的弱点:专有名词、型号、编号、精确匹配会漏。

python
from rank_bm25 import BM25Okapi

tokenized = [c["text"].lower().split() for c in chunks]
bm25 = BM25Okapi(tokenized)

def hybrid_search(query, k=20, alpha=0.6):
    vec_scores = embs @ embed([query])[0]           # 0~1
    kw_scores = np.array(bm25.get_scores(query.lower().split()))
    kw_scores = kw_scores / (kw_scores.max() + 1e-8)
    fused = alpha * vec_scores + (1 - alpha) * kw_scores
    return np.argsort(-fused)[:k]

# 更标准的做法:RRF(倒数排名融合),无需调权重
def rrf(rank_lists, k=60, top=20):
    score = {}
    for ranks in rank_lists:
        for r, idx in enumerate(ranks):
            score[idx] = score.get(idx, 0) + 1 / (k + r + 1)
    return sorted(score, key=score.get, reverse=True)[:top]

四、重排(Rerank):性价比最高的提升 ​

检索召回 20 条,用交叉编码器精排出真正相关的 5 条。

python
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

cands = [chunks[i]["text"] for i in top]
pairs = [(query, c) for c in cands]
rerank_scores = reranker.predict(pairs)
best = [cands[i] for i in np.argsort(-rerank_scores)[:5]]
环节速度精度
向量检索(双塔)极快(百万级)中
关键词检索快精确匹配强
Cross-Encoder 重排慢(几十条)高

标准架构:召回 50–100 → 重排 5–10 → 交给 LLM。 实测通常能提升 10–25 个点的答案准确率。

五、上下文组装:怎么把片段喂给模型 ​

python
PROMPT = """你是{domain}助手。只依据下面的资料回答,不要编造。

<资料>
{context}
</资料>

规则:
1. 每个结论后用 [编号] 标注来源
2. 资料中找不到答案时,回答「资料中未提及」,并说明需要什么信息
3. 引用原文时保持准确,不要改写数字与条款

问题:{question}
"""

def build_context(docs, max_chars=6000):
    parts, total = [], 0
    for i, d in enumerate(docs, 1):
        block = f"[{i}] {d['heading']}\n{d['text']}"
        if total + len(block) > max_chars:
            break
        parts.append(block); total += len(block)
    return "\n\n".join(parts)

要点:

  • 编号 + 要求引用 → 可溯源,便于人工核查,也显著降低幻觉
  • 显式允许「不知道」 → 比强制作答准确得多
  • 按相关度排序放置,最相关的放最前(首尾位置效应)

六、查询改写:解决「问题不像文档」的问题 ​

用户问「这个能退吗」,文档写的是「退货政策」。直接检索可能失败。

技术做法
查询扩展让 LLM 生成 2–3 个同义改写,多路检索后融合
HyDE让 LLM 先「编一个假答案」,用它去检索(假答案和真文档更接近)
多轮改写结合会话历史把「那它呢」补全为完整问题
子问题拆分复杂问题拆成多个检索
python
def multi_query(question, n=3):
    prompt = f"把下面的问题改写成 {n} 个不同表述的检索查询,每行一个,不要解释:\n{question}"
    return [question] + [l.strip() for l in call_llm(prompt).splitlines() if l.strip()]

七、RAG 效果差?按顺序排查 ​

症状先查常见原因
答「不知道」但文档里有检索切分太大/太小、embedding 不适配、缺关键词检索
答错但检索到了对的内容生成Prompt 没约束、上下文太杂、模型太弱
检索到无关内容索引切分破坏语义、chunk 太短无上下文
数字/条款答错解析PDF 解析丢表格、OCR 错字
多跳问题答不出架构需要多轮检索或 Agent,不是单次 RAG
新旧政策混答版本索引没有版本/时间过滤

排查工具:把「检索到的片段」打印出来。 90% 的 RAG 问题在这一步就能看清。

八、评测 RAG:拆成两段评 ​

环节指标怎么测
检索Recall@k、MRR、命中率标注「问题 → 正确文档 id」
生成忠实度(无编造)、答案相关性、完整性LLM-as-Judge + 人工抽检

分开评的价值:如果 Recall@5=0.95 但答案准确率只有 60%,问题一定在生成端(Prompt/模型),换 embedding 是浪费时间。

九、进阶方向 ​

方向说明
GraphRAG抽取实体关系建图,解决多跳与全局性问题
父子/多层索引小块检索、大块生成
自适应检索判断「要不要检索」,简单问题直接答
结构化 RAG表格/数据库走 Text-to-SQL,而不是塞进向量库
增量更新文档变更时按 chunk 增量重建,不全量重跑

通过标准 ​

下一步 ​

→ Agent:规划、工具调用与记忆

AI 学习路线 · 从入门到专家