从零搭一个能用的 RAG:分块、向量库、检索重排一次讲透

ClawLihai · · 共 2,459 字 · 约 8 分钟读完
📖 阅读精排增强版(衬线长文排版 + 交互式图解)→

你大概遇到过这种情况:问大模型一个公司内部的问题,它一本正经地胡说八道。或者问它 2026 年的新鲜事,它告诉你”我的知识截止到某某时间”。

这就是 RAG 要解决的事——让模型在回答之前,先去你的资料里查一遍

RAG 不是什么新架构,它就是”检索 + 生成”。但很多人搭出来效果差,问题不在模型,在流水线没搭对。这篇顺着一条真实流水线,把每一步拆开讲。

RAG 解决什么问题:幻觉与知识截止

大模型有几个天生短板:

  • 知识截止 + 私有数据:训练数据有截止日期,之后的事、以及你的私有数据(文档、数据库、内部 wiki),它压根没见过。
  • 幻觉:没见过的事实,它倾向于编一个像样的答案。

RAG 的思路很朴素:既然模型不知道,那就先把相关资料找出来,塞进上下文,让它”开卷答题”。开卷比闭卷靠谱,人也是这样。

一条完整的 RAG 流水线长什么样

两条线,一个建库、一个查询,共用同一个向量库:

RAG 流水线:建库与查询两条线

就这几步。难点不在”有没有”,在每一步的参数和取舍。下面挨个说。

先说个真实场景:公司知识库问答

假设你公司有个内部 wiki,攒了几年——HR 政策、报销流程、技术规范、产品 FAQ,上千篇。新员工天天问重复问题,你想做个问答机器人。

摆你面前三个选择:

  • 把 wiki 全塞进 prompt? 上千篇塞不进上下文窗口,token 成本也爆炸。
  • 微调一个模型? 文档天天在变,你总不能天天重新微调一遍。
  • RAG? 把 wiki 灌进向量库,每次提问现查现答,文档更新了重新入库就行。

RAG 之所以对口,是因为这个场景有三个特征凑到了一起:数据是私有的、内容还在频繁更新、回答得能追溯到来源。客服机器人、法律文书检索、医疗指南问答、代码库问答,套路都差不多。下面每一步,都可以套回这个场景来理解。

分块(Chunking):最容易被低估的一环

新手最爱踩的坑:把整篇文档丢进去算一个向量。结果一个问题只命中文档的一个角落,却把整篇的向量拉过来,检索精度稀烂。

分块就是切香肠,把长文档切成一小段一小段,每段单独算向量。切法主要有三种:

  • 固定长度切:每 N 个字切一段。最简单,但会把一句话拦腰切断。
  • 按语义切:在段落、标题、句号边界切。上下文更完整。
  • 重叠切:相邻块留一段重叠,避免边界信息丢失。

光说没用,拿一段真实文档当样本,看三种切法的区别。样本是一条团队代码规范:

代码规范:所有依赖用 pnpm 安装,禁止 npm 或 yarn,因为硬链接省磁盘。
每次提交前必须跑 npm test 全量通过,CI 红了禁止合并到 main。
API 密钥统一放 .env,绝不硬编码进源码,.env 要进 .gitignore。

① 固定长度切(每 30 字硬切)——最朴素的写法就是按字符切片,不看任何语义:

doc = ("代码规范:所有依赖用 pnpm 安装,禁止 npm 或 yarn,因为硬链接省磁盘。"
       "每次提交前必须跑 npm test 全量通过,CI 红了禁止合并到 main。"
       "API 密钥统一放 .env,绝不硬编码进源码,.env 要进 .gitignore。")

chunk_size = 30
chunks = [doc[i:i+chunk_size] for i in range(0, len(doc), chunk_size)]

上面这段代码实跑出来的结果:

块1: 代码规范:所有依赖用 pnpm 安装,禁止 npm 或 ya
块2: rn,因为硬链接省磁盘。每次提交前必须跑 npm test
块3: 全量通过,CI 红了禁止合并到 main。API 密钥统一放
块4:  .env,绝不硬编码进源码,.env 要进 .gitign
块5: ore。

灾难现场:yarn 被切成 ya + rn(块1 末尾 + 块2 开头),gitignore 更惨,被切成 .gitign + ore。“禁止 npm 或 yarn”这条关键规则在块1 末尾断裂——用户问”能不能用 yarn”时,命中块1,但向量把半截 ya 和一堆无关内容混在一起,检索精度直接塌方。这就是固定长度的硬伤——它不认语义,只数字符。

② 按语义切(在句号 / 换行处切)

块1: 所有依赖用 pnpm 安装,禁止 npm 或 yarn,因为硬链接省磁盘。
块2: 每次提交前必须跑 npm test 全量通过,CI 红了禁止合并到 main。
块3: API 密钥统一放 .env,绝不硬编码进源码,.env 要进 .gitignore。

每块正好一条完整规则,干净。语义切的代价是要识别边界(句号、标题、段落),但换来的是每块”自成一个可被独立理解的小单元”,这对检索是决定性的。

③ 重叠切(在语义切基础上,相邻块尾部 / 头部重叠一段)

块1: [...所有依赖用 pnpm...] [禁止 npm 或 yarn。]
块2:                          [禁止 npm 或 yarn。] [每次提交前必须跑 npm test...]

为什么要重叠?因为切完的边界是”人为”的。比如一条规则恰好横跨块1 和块2 的接缝,切了之后两块都只剩半句,谁都不完整。留个重叠(通常 10%-20%),让边界附近的内容在两个块里都出现一次,检索时至少有一块能完整命中。

工业界实际怎么切:几乎没人手写切分逻辑,都用现成切分器。最主流的是 LangChain 的 RecursiveCharacterTextSplitter,它”智能”在哪?看它的分隔符优先级:

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,        # 每块目标 200 字符
    chunk_overlap=40,      # 相邻块重叠 40 字符
    # 关键:按优先级递进地切,能保住大单元就不切碎小单元
    separators=["\n\n", "\n", "。", ",", " ", ""],
)

chunks = splitter.split_text(doc)

separators 那行是灵魂。它的逻辑是逐级降级:先用段落(\n\n)切,只有某段仍超过 chunk_size 时,才对那一段退到行(\n)切;还超长,再退到句号()、逗号()、空格,最后才是任意字符。没超长的段原样保留,不会被无端切碎。能保住段落就不切行,能保住句子就不切逗号——这就是递归切分的精髓,等于把”固定长度”和”按语义”两套思路缝起来了。

经验值:一块 200-500 字、重叠 10%-20%(这里按字符数算,中文场景;英文场景一般按 token 谈,500-1000 token 起步,别把两套单位混了)。太大召回粗(一个块塞了好几件事,命中了却混在一起),太小上下文碎(每块信息量不够撑起回答)。没有银弹,得拿你自己的数据试——切完随机抽 10 块人眼看一遍,是不是每块都能独立讲清一件事,这是最快的自检。

向量库选型:Milvus / Qdrant / Weaviate 怎么选

向量库存的就是”这段话的向量 + 原文”,检索时按相似度(通常用余弦)找最近的几条。

具体点,库里每条记录长这样:

{
  "id": "doc_42_chunk_3",
  "embedding": [0.0231, -0.1142, 0.0877, ... ],   // 几百到几千维浮点数
  "document": "发版前必须跑 npm test,否则不让合并。",
  "metadata": { "source": "团队规范.md", "chunk_idx": 3, "updated": "2026-06-01" }
}

向量就是那句话被嵌入模型压成的一串数字——可以理解成”语义的坐标”,意思接近的句子坐标也接近。检索时,你的问题也被压成同样维度的向量,然后算两串数字的余弦相似度(夹角越小越相关)。

metadata 别小看,它是”带过滤的检索”的关键:比如”只在今年更新的文档里查""只在某个产品线的文档里查”,全靠它。Qdrant 这类向量库的强项就在这。

向量库适合特点
Chroma入门、原型一行起,单机轻量,自带嵌入(默认英文)
Qdrant生产、强过滤Rust 高性能,过滤与检索深度融合
Milvus大规模、企业级分布式,扛亿级向量,标量过滤也强
Weaviate模块化、多模态内置向量化模块,GraphQL 查询灵活

我的建议:先 Chroma 跑通,量大或要上生产再换 Qdrant / Milvus。别一上来就 Milvus,运维成本够你喝一壶。

检索之后还有一步:重排(Rerank)

向量检索(用嵌入算相似度)召回率高,但精度一般——它擅长”找相关”,不擅长”排哪个最相关”。

所以检索完 top-20 之后,再加一个 reranker 精排成 top-5。重排模型(像 bge-reranker、Cohere Rerank)用的是 cross-encoder,比双塔的嵌入更准,但慢,所以只对候选集跑。

这一步经常被省,但加上之后效果肉眼可见地变好。top-K 检索召回,rerank 定序,这是工业界标配。

一个能跑的最小例子 + 常见坑

用 Chroma 跑通要装两个包(pip install chromadb sentence-transformers),第一次会下载一个中文嵌入模型:

import chromadb
from chromadb.utils import embedding_functions

# 中文必须换掉默认嵌入——Chroma 默认是英文模型(all-MiniLM-L6-v2),对中文基本是瞎子
ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="BAAI/bge-small-zh-v1.5")

client = chromadb.PersistentClient(path="./rag_db")
# 文本检索通常用余弦:Chroma 默认是 L2,要显式切到 cosine,且建库时定死、事后改不了
coll = client.get_or_create_collection(
    "notes",
    embedding_function=ef,
    metadata={"hnsw:space": "cosine"},
)

# 建库:塞几段话进去
coll.add(
    ids=["1", "2", "3"],
    documents=[
        "我们公司用 pnpm,不要用 npm 装依赖。",
        "发版前必须跑 npm test,否则不让合并。",
        "API 密钥统一放 .env,禁止硬编码进代码。",
    ],
)

# 查询:问题被嵌入后,按余弦相似度找最近的 2 条
res = coll.query(query_texts=["装依赖用什么?"], n_results=2)
print(res["documents"])
# → [['我们公司用 pnpm,不要用 npm 装依赖。', 'API 密钥统一放 .env,禁止硬编码进代码。']]

# res 里还带了相似度分数和 id,可拿来做阈值过滤
# res["distances"] → 余弦距离 [[0.35, 0.59]]:第1条 0.35 是确信命中,
#                    第2条 0.59 明显跑偏了(命中的是无关的密钥规则)——
#                    设个距离阈值把这种边角结果丢掉,别照单全收塞给 LLM
# res["ids"]       → [['1', '3']]

最后把检索到的片段拼进 prompt,丢给 LLM:

context = "\n".join(res["documents"][0])
prompt = f"根据以下资料回答问题,找不到就说不知道。\n资料:{context}\n问题:装依赖用什么?"
# → 把 prompt 发给你的 LLM

能跑。但”能跑”和”能用”之间,隔着一个评估。

常见坑

  • 块切太大:一个块塞了三件事,检索命中了却混在一起。切小点。
  • 没重排:top-1 经常不是最该要的那条。加 reranker。
  • 嵌入模型选错:用英文嵌入模型处理中文,问题里恰好带答案关键词时(比如问”装依赖”命中”pnpm 装依赖”)还能靠词汇重叠蒙对;可一旦问题和答案是语义相关、字面没共同词(比如问”怎么回滚发版”,答案是”用 git revert 撤销提交”),英文模型就彻底召回不到。所以中文必须换 bge-large-zh 这类中文嵌入。
  • 不评估:感觉效果”还行”就上线。搭个简单的问答测试集,量化检索命中率。
  • 资料本身烂:垃圾进垃圾出,RAG 救不了结构混乱的文档。

RAG vs 微调 vs 长上下文:什么时候用哪个

很多人第一反应是”我是不是该微调一个模型”。先别急,看清楚三个方案各管什么:

方案适合不适合成本
RAG私有数据、频繁更新、要引用来源要改模型的说话风格或内在能力中(搭向量库 + 检索)
微调固定领域知识、改输出风格/格式数据天天变、要可追溯的来源高(要训练 + 持续迭代)
长上下文文档就几篇、临时一次性分析上千篇、成本敏感、要长期复用单次低(不用建库),规模化高(每次重传全部 token)

说人话:

  • 想让模型知道它不知道的事(你的私有数据)→ RAG。
  • 想让模型变得更像你(语气、格式、专业判断力)→ 微调。
  • 文档就两三页、用完即弃 → 直接塞长上下文,别折腾 RAG。

这三者也不是非此即彼。生产系统经常几个一起上:长上下文兜底,RAG 负责精准召回,微调来调说话风格。先把 RAG 这条地基打牢,再谈组合。


RAG 不神秘,就六步:分块、嵌入、存进向量库、检索、重排、生成。先把最小例子跑通,再一步步调参、加重排、做评估。别被各种”高级 RAG”名词吓住,地基没打好,GraphRAG 也救不了你。

下一篇可以聊聊怎么给 RAG 做评估——那才是真正拉开差距的地方。

这个话题还有

📺 视频版 · 6分21秒 已制作,即将上线 B站 · 视频号 · 抖音 · 小红书
📖 精排网页版 · 衬线长文 + 交互式图解 阅读增强版
🖼 图文卡片版 · 点击放大轮播
双击或滚轮缩放 · 拖动平移 · Esc 关闭