一、为什么这一篇很重要?
在系列一里,我们把整条技术栈的地图画了一遍:
知识库 RAG(底座) → MCP(连接外部系统) → Agent Skills(封装业务能力)
但那篇更多是「概念盘点 + 技术路线」,很多人看完会有三个典型疑问:
这一篇就只做一件事:
给你一套「最小可用 RAG 知识库」的完整实现:
从文档加载 → 清洗与分块 → 向量化 → 向量库 → 简单问答,全打通。
风格上我会尽量延续第一篇那种节奏:
- 前面先用表格把关键选型说清楚;
- 中间给你一份可以直接运行的最小示例代码;
- 后半部分再拆解细节 + 踩坑建议,让你可以慢慢迭代成生产可用版本。
二、整体方案先看清:这一篇我们只关注哪一段?
先把本篇的技术范围圈出来,避免「一篇文章想干完所有事」:
graph LR
A[原始文档/PDF/Word/Markdown] –> B[解析 + 基础清洗]
B –> C[分块 Chunking]
C –> D[文本向量化 Embedding]
D –> E[向量数据库存储]
F[用户 Query] –> G[Query 向量化]
G –> H[相似度检索 TopK]
H –> I[拼接上下文 + Prompt]
I –> J[LLM 生成回答]
这一篇重点只在粗体部分:
- 文档加载 & 清洗
- 分块(Chunking)策略
- 向量化(Embedding 模型选型)
- 向量库存储与检索
- 串成一个最小 RAG 问答链
MCP / Agent Skills 在后面篇章,暂时不掺进来,免得心智负担太大。
三、核心选型一张表说清(2025–2026 这波比较稳的组合)
3.1 技术选型总览
| 编程语言 | Python 3.10+ | 生态成熟,教程资源多 |
| 框架 | LangChain / langchain-community | Loader / Splitter / VectorStore 封装齐全 |
| 向量模型 | BAAI/bge-m3 | 免费、多语言,中文表现好,RAG 新主流 |
| 向量库(入门) | FAISS | 内存型,本地 PoC 非常好用 |
| 向量库(生产) | Milvus / pgvector | 可持久化、可扩展,适合企业环境 |
| LLM | 任意对话模型(OpenAI、DeepSeek、本地 LLM) | 本篇不深挖,只负责「读上下文 + 回答」 |
3.2 分块策略对比表
| 字符递归分块 | 400–800 字符 | 80–150 | 常规说明文、FAQ、教程文档 | 实现简单,通用性最强 |
| Markdown 标题分块 | 按标题切后再 400–800 | 50–100 | 有明确层级结构的技术文档、规范 | 保留章节结构,更贴近人类阅读 |
| Token 分块 | 256–512 token | 50–80 | 对上下文窗口比较敏感的模型 | 更贴近实际 token 限制 |
3.3 Embedding 模型快速对比
| BAAI/bge-m3 | 多语言(含中文) | 开源免费 | 中文为主、多语言混合的 RAG 场景 |
| m3e 系列 | 中文 | 开源免费 | 纯中文业务文档 |
| text-embedding-ada-002 | 英文偏好 | 付费 | 英文内容为主 + 已在用 OpenAI 生态 |
如果你目前没太多约束,直接上 BGE-M3 是 2025–2026 比较保险的选型。
四、先给你一份「能跑起来」的最小 Demo
这一节只做一件事:
让你 10 分钟内起一个 RAG 知识库,对自己的几份文档做问答。
4.1 目录结构
rag-demo/
├── data/ # 放你的 PDF / DOCX / MD 文档
│ ├── doc1.pdf
│ └── spec.md
└── rag_minimal.py # 单文件最小 Demo
4.2 安装依赖
pip install langchain langchain-community
pip install pypdf python-docx unstructured markdown
pip install sentence-transformers
pip install faiss-cpu
你如果要用 OpenAI 模型,还需:
pip install openai
# 或者新版的 openai>=1.x 已经集成 ChatCompletion,下面代码会说明
4.3 最小可运行代码(单文件版)
# rag_minimal.py
import os
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain_community.chat_models import ChatOpenAI # 兼容 openai 新版
DATA_DIR = "./data"
def load_docs(data_dir: str):
"""多格式文档加载:pdf / docx / md"""
docs = []
for fname in os.listdir(data_dir):
fpath = os.path.join(data_dir, fname)
if not os.path.isfile(fpath):
continue
if fname.lower().endswith(".pdf"):
loader = PyPDFLoader(fpath)
elif fname.lower().endswith(".docx"):
loader = Docx2txtLoader(fpath)
elif fname.lower().endswith(".md"):
loader = UnstructuredMarkdownLoader(fpath)
else:
continue
file_docs = loader.load()
for d in file_docs:
d.metadata["source"] = fname
docs.extend(file_docs)
return docs
def build_rag():
# 1. 文档加载
docs = load_docs(DATA_DIR)
print(f"[INFO] 加载原始文档片段: {len(docs)}")
# 2. 分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\\n\\n", "\\n", "。", "!", "?", ",", " "],
)
chunks = splitter.split_documents(docs)
print(f"[INFO] 分块后总块数: {len(chunks)}")
# 3. 向量化
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True},
)
# 4. 构建向量库(FAISS)
vectordb = FAISS.from_documents(chunks, embeddings)
retriever = vectordb.as_retriever(search_kwargs={"k": 5})
# 5. 问答链
llm = ChatOpenAI(
model="gpt-4o-mini", # 或者你常用的模型名
temperature=0,
# 如果用 OpenAI,需要设置环境变量 OPENAI_API_KEY
)
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
return_source_documents=True,
chain_type="stuff", # 简单拼接上下文
)
return qa
def main():
qa = build_rag()
while True:
query = input("\\n请输入你的问题(q 退出):").strip()
if query.lower() in {"q", "quit", "exit"}:
break
result = qa.invoke({"query": query})
print("\\n[回答]")
print(result["result"])
print("\\n[参考片段]")
for i, d in enumerate(result["source_documents"], 1):
print(f"— 片段 {i} (来自 {d.metadata.get('source')})—")
print(d.page_content[:120].replace("\\n", " ") + "…\\n")
if __name__ == "__main__":
main()
放几份你自己的 PDF/Word/Markdown 到 data/ 目录下,运行:
python rag_minimal.py
终端里直接问:
- 「这份规范里 API 认证流程是怎么定义的?」
- 「XXX 系统出问题时,官方推荐的排查步骤是什么?」
你就已经有了一个完全基于你文档的问答机器人。
接下来才是这一篇真正的价值:把这个最小 Demo 拆开讲透。
五、分块(Chunking):RAG 里最容易被忽视、又最影响效果的一步
5.1 目标是「语义自洽」
一段好的 chunk 应该满足两点:
经验值(结合中文场景):
| FAQ / 问答手册 | 300–500 字符 | 50–100 | 一个问答对通常不长 |
| 技术规范 / API 文档 | 500–800 字符 | 100–150 | 一个接口说明或一小节规范 |
| 长报告 / 案例分析 | 800–1200 字符 | 200–300 | 一个完整的小节或案例片段 |
如果你不想一开始搞太多策略,上一篇 Demo 里那套 RecursiveCharacterTextSplitter 对大多数情况已经够用。
5.2 Markdown 标题感知分块(进阶一点)
技术文档、规范文档往往有清晰的标题层级。用 Markdown 头部 + 二次分块,会比单纯按字符分块更「聪明」。
示例代码:
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.schema import Document
headers_to_split_on = [
("#", "一级标题"),
("##", "二级标题"),
("###", "三级标题"),
]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
char_splitter = RecursiveCharacterTextSplitter(
chunk_size=700,
chunk_overlap=150,
separators=["\\n\\n", "\\n", "。", "!", "?", ",", " "],
)
def split_markdown_doc(doc: Document):
"""针对 Markdown 文档的两级分块"""
md_docs = md_splitter.split_text(doc.page_content)
wrapped_docs = [
Document(
page_content=d["content"],
metadata={**doc.metadata, **d["metadata"]},
)
for d in md_docs
]
return char_splitter.split_documents(wrapped_docs)
你可以在实际工程里:
- 对 filetype == md 的文档,用这套策略;
- 对其他类型文档,用普通字符分块。
六、向量化(Embedding):为什么大家都推 BGE-M3?
BGE-M3 在 RAG 圈子里火起来,是因为它同时满足了几个现实需求:
在本篇这种「最小可用」场景,我们只用它的稠密向量能力就够了:
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cpu"}, # 有 GPU 可以改成 "cuda"
encode_kwargs={"normalize_embeddings": True},
)
后面你想做「混合检索」「多向量」之类的进阶玩法,换用原生 FlagEmbedding 库也不迟。
七、向量库:FAISS 入门,Milvus/pgvector 升级
你可以简单按「规模 + 运维成本」选:
| 个人 PoC / 小项目 | FAISS | 纯内存,部署简单,Python 里直接用 |
| 小团队 / 内部 Demo | Chroma | 简单好用,配 LangChain 很方便 |
| 企业内网 / 生产 | Milvus | 专门的向量数据库,扩展性好 |
| 已有 Postgres 集群 | pgvector | 直接在 PG 上扩展,易于接入运维 |
本篇 Demo 用的是 FAISS,原因很简单:
- 你不需要再多部署一个服务;
- 可移植性很好,后续替换成 Milvus/pgvector,只要改一小段封装代码。
八、调优 & 踩坑:从「能跑」到「能用」
这里挑几个你大概率会遇到的问题,给出直接可执行的调整策略。
8.1 问:为什么模型总在说「从提供的文档看不出来」?
可能原因:
检索结果确实没包含有用信息
- 检查你的 data/ 目录里文档是不是太少、不完整;
- 检查分块大小,是否把一整段逻辑拆得太碎,导致检索不到完整上下文。
top_k 太小
- 默认 k=3~5;可以尝试从 3 调到 5 或 8,看回答质量是否提升。
Embedding 模型没加载成功/在瞎跑
- 打印一下向量维度(bge-m3 默认 1024 维),确认是不是都为 0 或形状异常。
8.2 问:模型「答非所问」、看起来在乱编?
建议一步步排查:
打印检索到的 source_documents 内容(上面 Demo 已经在打印了),看看:
- 文档内容是否真的和问题相关;
- 是否包含了关键细节。
在 Prompt 里增加更强约束,例如:
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
prompt_template = """
你是一个知识库问答助手,只能根据提供的“文档片段”回答问题。
如果文档中没有相关信息,请明确回答:
“文档中没有提到相关内容,我无法确定。”
# 文档内容:
{context}
# 用户问题:
{question}
请用中文回答,尽量引用文档中的关键信息。
"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"],
)
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
return_source_documents=True,
chain_type="stuff",
chain_type_kwargs={"prompt": PROMPT},
)
8.3 问:RAG 结果不稳定,以后接 Skills/MCP 会不会更乱?
不会,只要你现在把底层这几件事做扎实:
- 分块粒度适中,chunk 有明确语义;
- 向量模型选得对,至少能找到相关片段;
- 向量库封装干净,retriever 接口稳定。
后面 Skills/MCP 其实都是:
- 在更高层面调度「检索 → 决策 → 工具调用」;
- 用 Skills 把「怎么问知识库」「拿到结果后怎么处理」这些流程固化下来。
也就是说,这一篇打的是整个系列的「工程地基」,地基稳了,你后面怎么加技能、加工具,都只是换上层建筑而已。
九、本篇小结
到这里,这一篇你应该已经:
- 有一份可以直接运行的 RAG Demo(rag_minimal.py + data/ 目录);
- 理解了分块策略、Embedding 选型、向量库选型背后的取舍逻辑;
- 知道了常见坑(分块、top_k、Prompt 约束)该怎么快速调整。
用一句话概括这一篇的定位:
从「会调 LLM API」升级到「会搭一个能用的 RAG 知识库」。





