$catMANUAL||~28 min

RAG 实战指南:从零搭建一个真正能用的知识问答系统

advertisement

RAG 实战指南:从零搭建一个真正能用的知识问答系统

最近在折腾 Hermes Agent 的时候,碰到一个问题:怎么让 AI 记住我项目里的所有文档?

直接把文档塞进 prompt?上下文窗口不够用。fine-tune 模型?每次文档更新都得重新训练,成本太高。

后来发现,RAG 才是解决这类问题的标准答案。不是什么新概念了,但 2026 年的 RAG 工具链比两年前成熟太多了,值得重新聊聊。

RAG 是什么,用大白话说

RAG,全称 Retrieval-Augmented Generation,翻译过来就是"检索增强生成"。名字听着唬人,其实逻辑很简单:

先搜,再答。

你问 AI 一个问题,它不直接靠自己的"记忆"回答,而是先去你的知识库里搜一波相关内容,把搜到的东西作为参考,再组织语言回答你。

打个比方:一个医生不可能记住所有病例,但他知道怎么查病历系统。RAG 就是给 AI 装了一个"查病历"的能力。

这个思路的好处很明显:

  • 模型不用重新训练,你只管更新知识库就行
  • 回答可以引用来源,有据可查
  • 私有数据不用发给模型训练,安全性更好
  • 知识库可以随时更新,不存在"知识截止日期"的问题

最简 RAG 长什么样

一个能跑的 RAG 系统,核心流程就五步:

python
1
# 伪代码,别直接跑
2
docs = load_documents("./my_docs/")      # 1. 加载文档
3
chunks = split_into_chunks(docs)          # 2. 切分成小块
4
vectors = embed(chunks)                   # 3. 向量化
5
store(vector_db, vectors)                 # 4. 存入向量数据库
6
 
7
# 查询时
8
question = "什么是 RAG?"
9
relevant = search(vector_db, embed(question), top_k=5)  # 5. 检索
10
answer = llm(f"根据以下内容回答:{relevant}\n\n问题:{question}")

看起来很简单对吧?但魔鬼全在细节里。

第一步的"加载文档"就可能翻车——PDF 里的表格怎么处理?图片里的文字怎么办?中文 PDF 乱码怎么搞?

第二步的"切分"更是重灾区。切太大,检索精度低;切太小,上下文断裂,模型看不懂。

这些坑我一个个踩过,下面详细说。

文档切分:RAG 最容易翻车的环节

很多人觉得 RAG 的核心是向量数据库或者 embedding 模型,其实不是。文档切分的质量直接决定了 RAG 的上限。

切得不好,后面再怎么优化都是在给烂数据打补丁。

切分的基本策略

最简单的切法是按固定长度切,比如每 500 个字符切一块:

python
1
def fixed_size_split(text, chunk_size=500):
2
    return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]

这种切法的问题在于:它完全不考虑语义。一句话可能被从中间切断,导致前后两块都不完整。

稍微好一点的是按段落或章节切:

python
1
def split_by_paragraph(text):
2
    return [p.strip() for p in text.split("\n\n") if p.strip()]

但段落长度差异可能很大——有的段落只有两句话,有的段落写了两千字。

实战推荐:递归字符切分

LangChain 提供了一个 RecursiveCharacterTextSplitter,思路是先按大分隔符(比如 \n\n)切,切完如果还太长,就按小分隔符(\n)再切,依次递归:

python
1
from langchain.text_splitter import RecursiveCharacterTextSplitter
2
 
3
splitter = RecursiveCharacterTextSplitter(
4
    chunk_size=500,
5
    chunk_overlap=50,       # 相邻块重叠 50 字符,避免上下文断裂
6
    separators=["\n\n", "\n", "。", ",", " "]  # 中文文档用中文标点
7
)
8
 
9
chunks = splitter.split_text(document)

chunk_overlap 很关键。不设 overlap 的话,一个概念如果刚好跨了两个块,两边都检索不到完整信息。设 50-100 字符的 overlap 就够了。

中文切分的坑

中文没有天然的空格分词,所以切分的时候要特别注意:

  1. 分隔符要用中文标点。默认的 ["\n\n", "\n", " ", ""] 是为英文设计的,中文文档应该加 "。""!""?" 等。
  2. chunk_size 不能太小。中文信息密度高,500 字符大概相当于英文 200 词,已经很短了。再小的话上下文就不够了。
  3. 表格和代码块别硬切。如果你的文档里有代码或表格,最好先识别出来,整块保留,不要从中间切断。

我之前处理一批中文技术文档,用默认的英文分隔符切分,结果很多句子被从中间断开,检索出来的片段全是半句话,模型根本没法用。换成中文分隔符之后效果好了很多。

Embedding 模型怎么选

Embedding 是把文本转成向量的过程。向量之间的距离代表语义相似度——距离越近,意思越像。

2026 年主流的 embedding 模型:

  • OpenAI text-embedding-3-small/large:效果好,API 调用,按 token 计费。small 版本性价比很高。
  • BGE 系列(BAAI):开源,中文效果好,可以本地部署。bge-large-zh-v1.5 是中文场景的首选之一。
  • Jina embeddings v3:支持多语言,开源,可以自托管。
  • Cohere embed-v4:多语言效果不错,API 调用。

如果你的数据主要是中文,BGE 系列是首选。不用付 API 费用,中文语义理解也比 OpenAI 的模型好一些。

本地部署 BGE 的代码:

python
1
from sentence_transformers import SentenceTransformer
2
 
3
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
4
embeddings = model.encode(["这是一段测试文本", "RAG 是检索增强生成"])

需要注意的是,embedding 模型一旦选定就别轻易换。换模型意味着所有已存的向量都得重新生成,成本不小。

向量数据库:存和查的学问

向量数据库负责存向量、做相似度检索。2026 年的选择很多:

  • ChromaDB:轻量,Python 原生,适合原型和小项目。pip install 就能用。
  • Milvus/Zilliz:生产级,支持大规模数据,分布式部署。
  • Qdrant:Rust 写的,性能好,API 设计干净。
  • Pinecone:全托管 SaaS,不用自己运维,但要花钱。
  • Weaviate:支持混合搜索(向量 + 关键词),功能丰富。
  • pgvector:PostgreSQL 插件,如果你已经在用 PG,加个插件就行,不用引入新组件。

对于个人项目或者小团队,ChromaDB 或者 pgvector 就够了。别一上来就搞 Milvus 集群,杀鸡用牛刀。

我自己的 Hermes Agent 项目用的是 ChromaDB,主要是图省事:

python
1
import chromadb
2
 
3
client = chromadb.PersistentClient(path="./chroma_db")
4
collection = client.get_or_create_collection("my_docs")
5
 
6
# 添加文档
7
collection.add(
8
    documents=["RAG 是检索增强生成的缩写", "向量数据库存储 embedding"],
9
    ids=["doc1", "doc2"]
10
)
11
 
12
# 查询
13
results = collection.query(query_texts=["什么是 RAG?"], n_results=3)

代码就这么点。不需要配置服务器,不需要管连接池,数据存在本地文件夹里。

检索策略:不只是搜最近的

最基础的检索是向量相似度搜索——找到跟问题最"像"的几个文档块。但实际用下来,光靠这个不够。

问题一:语义相似 ≠ 答案相关

用户问"怎么给 Python 项目配置虚拟环境",向量搜索可能返回一堆跟"Python"和"环境"相关的段落,但具体到 venvconda 的段落排在很后面。

解法:Reranking

先用向量搜索粗筛出 top-20,再用一个 reranker 模型精排,把真正相关的段落排到前面:

python
1
from sentence_transformers import CrossEncoder
2
 
3
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
4
 
5
# 粗筛
6
candidates = vector_search(query, top_k=20)
7
 
8
# 精排
9
scores = reranker.predict([(query, doc) for doc in candidates])
10
reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
11
top_results = [doc for doc, score in reranked[:5]]

Reranker 比 embedding 模型慢,但它看的是 query 和 document 的完整交互,精度高得多。先粗筛再精排,是精度和速度的平衡。

问题二:用户的问法和文档的写法不一样

用户说"怎么部署",文档里写的是"安装和配置指南"。语义上接近,但关键词不匹配。

解法:混合搜索

把向量搜索和关键词搜索结合起来:

python
1
# 向量搜索结果
2
vector_results = collection.query(query_texts=[query], n_results=10)
3
 
4
# 关键词搜索(如果用 pgvector + PostgreSQL,直接用全文搜索)
5
keyword_results = db.execute(
6
    "SELECT * FROM docs WHERE content ILIKE %s", [f"%{keyword}%"]
7
)
8
 
9
# 合并去重,可以用 RRF (Reciprocal Rank Fusion) 算法排序
10
final_results = reciprocal_rank_fusion(vector_results, keyword_results)

很多向量数据库已经内置了混合搜索,比如 Qdrant 和 Weaviate。不用自己手写融合逻辑。

2026 年 RAG 的新玩法

RAG 这两年进化了不少。基础的"切分-向量化-检索-生成"流程还是那个流程,但上面叠加了很多新的优化手段。

Agentic RAG

这是 2026 年最火的方向。核心思路是:让 AI Agent 来决定怎么检索,而不是写死一套检索流程。

传统的 RAG 流程是固定的:收到问题 → 搜索 → 生成回答。

Agentic RAG 把"搜索"这一步也交给 Agent 来决策:

  • 这个问题需要搜哪个知识库?
  • 搜出来的结果够不够?不够的话换个关键词再搜一次
  • 这个问题其实不需要搜索,模型自己就能答
  • 这个问题需要先拆成几个子问题,分别搜索再汇总

用 LangChain 的 Agent 来实现:

python
1
from langchain.agents import create_react_agent
2
from langchain.tools import Tool
3
 
4
def search_knowledge_base(query: str) -> str:
5
    results = collection.query(query_texts=[query], n_results=3)
6
    return "\n".join(results["documents"][0])
7
 
8
tools = [
9
    Tool(name="SearchDocs", func=search_knowledge_base,
10
         description="搜索内部文档库")
11
]
12
 
13
agent = create_react_agent(llm, tools, prompt)
14
response = agent.invoke({"input": "项目怎么部署到生产环境?"})

Agent 会自己决定要不要搜索、搜几次、用什么关键词。比写死的流程灵活得多。

Graph RAG

微软搞了个 GraphRAG,思路是在 RAG 的基础上加一层知识图谱。

普通的 RAG 检索的是"文本块",Graph RAG 检索的是"实体和关系"。比如你问"张三负责哪个项目?",普通 RAG 可能搜到一段包含"张三"的文档,但那段文档可能没提到项目信息。Graph RAG 可以通过知识图谱里的关系直接找到"张三 → 负责 → 项目X"这条边。

适合处理关系复杂的场景,比如组织架构、供应链、法律条文之间的引用关系。

缺点是构建知识图谱本身就很麻烦,小项目不值得搞。

Contextual Retrieval

Anthropic 提出的一个优化:在切分文档的时候,给每个块加上一段"上下文说明"。

比如原文是:"该产品支持单点登录(SSO)。"

切分之后,这个块可能会丢失它是哪个产品的信息。Contextual Retrieval 的做法是让模型给每个块补一句上下文:"这是关于 XX 产品的功能说明:该产品支持单点登录(SSO)。"

检索的时候,有上下文的块比没上下文的块准确率高得多。Anthropic 的测试显示,这个简单的方法把检索失败率降低了 35%。

RAG 效果怎么评估

搭完 RAG 之后,怎么知道它好不好用?光靠"我试了几个问题感觉还行"是不够的。

核心指标

RAG 的质量主要看两个维度:

检索质量:搜出来的文档块跟问题相不相关。指标是 Recall@K(top-K 个结果里有多少是真正相关的)和 MRR(第一个相关结果排在第几位)。

生成质量:模型基于检索结果生成的回答好不好。指标包括准确性(回答是否正确)、完整性(是否遗漏关键信息)、忠实度(是否编造了检索结果里没有的内容)。

最后这个"忠实度"特别重要。RAG 的一个常见问题是模型"幻觉"——检索结果里没有的信息,模型自己编了一段看起来很对的话。你得专门检测这种情况。

简单的评估方法

如果你不想搞复杂的评估框架,可以用这种土办法:

  1. 准备 20-30 个测试问题,覆盖你知识库的主要内容
  2. 每个问题手动标注"正确答案"和"应该检索到的文档"
  3. 跑一遍 RAG pipeline,对比检索结果和生成回答
  4. 统计检索召回率和回答准确率
python
1
test_cases = [
2
    {
3
        "question": "怎么创建 Python 虚拟环境?",
4
        "expected_docs": ["doc_0"],  # 应该检索到的文档 ID
5
        "expected_answer_contains": ["venv", "python -m venv"]
6
    },
7
    # ...更多测试用例
8
]
9
 
10
for case in test_cases:
11
    results = collection.query(query_texts=[case["question"]], n_results=3)
12
    retrieved_ids = results["ids"][0]
13
 
14
    # 检查是否检索到了正确文档
15
    hit = any(doc_id in retrieved_ids for doc_id in case["expected_docs"])
16
    print(f"问题: {case['question'][:30]}... 检索命中: {hit}")

正式一点的话,可以用 RAGAS 这个评估框架,它专门做 RAG 效果评估,支持 Faithfulness、Answer Relevancy、Context Recall 等指标。

python
1
from ragas import evaluate
2
from ragas.metrics import faithfulness, answer_relevancy
3
 
4
result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
5
print(result)

说实话,很多人搭完 RAG 就不管了,从来没评估过效果。结果用户用起来各种问题,回头一看,检索召回率才 40%,一大半问题都搜不到正确文档。花半天时间做个评估,能省很多后面的麻烦。

RAG vs Fine-tuning vs 长上下文

2026 年,解决"让模型理解你的数据"这个问题,有三条路:

RAG:不改模型,实时检索参考内容。适合数据经常更新、需要引用来源、数据量大的场景。

Fine-tuning:用你的数据重新训练模型的部分参数。适合需要改变模型行为(比如输出风格、专业术语)的场景。但数据更新了就得重新训,成本高。

长上下文:直接把所有数据塞进 prompt。现在模型的上下文窗口越来越大,Claude 支持 200K token,Gemini 支持 1M token。如果数据量不大,直接塞进去可能比搞 RAG 简单得多。

怎么选?

  • 数据量小(几万字以内)→ 长上下文,最简单
  • 数据量大、经常更新 → RAG
  • 需要改变模型的行为和风格 → Fine-tuning
  • 需要引用来源、可解释性 → RAG
  • 预算有限、不想维护基础设施 → 长上下文或 API 调用

说实话,很多人搞 RAG 是因为觉得它"高级",但如果你的数据量不大,长上下文方案可能更简单有效。别为了用技术而用技术。

我踩过的坑

坑一:chunk_size 设太大

一开始我把 chunk_size 设成 2000,觉得大一点上下文更完整。结果检索出来的每块都很大,top_k=5 就把 context window 塞满了,模型反而抓不住重点。

后来改成 500,配合 reranking,效果好了很多。宁可切小一点,用 reranking 来保证相关性,也别切太大导致噪音太多。

坑二:没做 metadata 过滤

我的知识库里有文档、issue 记录、配置文件三种类型的数据。用户问"怎么配置 XXX"的时候,向量搜索同时返回了文档说明和 issue 讨论,两种内容混在一起,模型的回答自相矛盾。

加上 metadata 过滤之后,可以指定只搜某一类文档:

python
1
results = collection.query(
2
    query_texts=["怎么配置 Redis"],
3
    where={"type": "documentation"},  # 只搜文档,不搜 issue
4
    n_results=5
5
)

坑三:embedding 模型和查询语言不匹配

我的文档是中文的,但用户有时候用英文提问。用中文 embedding 模型处理英文查询,效果很差。

解决方案是用多语言 embedding 模型,比如 BGE-M3 或者 Jina embeddings v3,中英文都能处理。

坑四:忘了处理文档更新

一开始我只做了"索引",没做"更新"。文档改了之后,向量数据库里还是旧的版本,回答出来的信息过时了。

后来加了一个简单的机制:给每个文档块算 hash,文档更新的时候对比 hash,变了的重新 embedding。

python
1
import hashlib
2
 
3
def get_hash(text):
4
    return hashlib.md5(text.encode()).hexdigest()
5
 
6
# 更新时检查
7
for chunk in new_chunks:
8
    chunk_hash = get_hash(chunk)
9
    if chunk_hash != stored_hashes.get(chunk_id):
10
        # 内容变了,重新 embedding
11
        new_embedding = embed(chunk)
12
        collection.update(ids=[chunk_id], embeddings=[new_embedding])

实际动手:20 分钟搭一个最小 RAG

说了这么多,不如自己动手试试。下面是一个最小可运行的 RAG 示例,用 ChromaDB + OpenAI:

python
1
# pip install chromadb openai
2
 
3
import chromadb
4
from openai import OpenAI
5
 
6
# 初始化
7
client = chromadb.PersistentClient(path="./my_rag_db")
8
collection = client.get_or_create_collection("docs")
9
openai_client = OpenAI()
10
 
11
# 索引文档
12
documents = [
13
    "Python 的虚拟环境可以用 venv 创建,命令是 python -m venv myenv",
14
    "Docker 容器可以用 docker run -d -p 80:80 nginx 来启动",
15
    "Git 分支可以用 git checkout -b feature-x 来创建",
16
    "SSH 密钥生成用 ssh-keygen -t ed25519 命令",
17
]
18
 
19
for i, doc in enumerate(documents):
20
    collection.add(documents=[doc], ids=[f"doc_{i}"])
21
 
22
# 查询
23
def ask(question):
24
    results = collection.query(query_texts=[question], n_results=2)
25
    context = "\n".join(results["documents"][0])
26
 
27
    response = openai_client.chat.completions.create(
28
        model="gpt-4o-mini",
29
        messages=[
30
            {"role": "system", "content": f"根据以下参考资料回答问题。如果资料里没有相关内容,就说不知道。\n\n参考资料:\n{context}"},
31
            {"role": "user", "content": question}
32
        ]
33
    )
34
    return response.choices[0].message.content
35
 
36
# 测试
37
print(ask("怎么创建 Python 虚拟环境?"))

就这么多代码。能跑,能回答问题,能引用来源。

当然,生产环境还需要加很多东西:错误处理、日志、文档更新机制、reranking、混合搜索。但这个最小版本能让你在 20 分钟内理解 RAG 的核心流程。

工具推荐

如果你想快速搭一个 RAG 应用,不用从零开始,这些工具可以帮到你:

  • LangChain:最流行的 RAG 框架,生态丰富,但 API 变动频繁,文档有时候跟不上代码。
  • LlamaIndex:专注 RAG 的框架,比 LangChain 更聚焦,数据索引和查询的设计更优雅。
  • Dify:开源的 LLM 应用平台,拖拽式搭建 RAG 工作流,不用写代码。
  • RAGFlow:国产开源项目,支持深度文档解析(表格、图片 OCR),中文文档处理能力很强。
  • FastGPT:也是国产,UI 友好,适合快速搭建知识库问答系统。

如果你只是想给自己的项目加个文档问答,LlamaIndex 够用了。如果要做成产品给用户用,Dify 或 RAGFlow 更合适。

写在最后

RAG 不是什么银弹。它解决的是"让模型访问你的数据"这个问题,但它引入了新的复杂度:文档切分策略、embedding 模型选择、向量数据库运维、检索质量调优。

2026 年的好处是工具链成熟了很多,不用自己造轮子。ChromaDB、LlamaIndex、Dify 这些工具让搭建 RAG 的门槛低了不少。

但核心的那些问题——怎么切分文档、怎么保证检索质量、怎么处理数据更新——还是得你自己根据业务场景去调。没有一套配置适用于所有场景。

我后面打算试试 Agentic RAG,让 Agent 自己决定检索策略,而不是写死流程。到时候再写一篇踩坑记录。

有啥问题评论区聊。

advertisement

RAG 实战指南:从零搭建一个真正能用的知识问答系统 — AI Hub