上一篇《RAG 实战教程(一)》我们把 RAG 的完整工作原理拆了一遍:
text
文档
↓
分片 Chunk
↓
Embedding
↓
向量索引
↓
召回 Retrieval
↓
重排 Rerank
↓
Context
↓
LLM
↓
Answer
但理解原理只是第一步。
这一篇我们不再停留在概念层面,而是直接动手:

从 0 写一个真正可以运行的 RAG 知识库。
我们会用:
text
Python
+
OpenAI Compatible API
+
Embedding
+
Qdrant
+
LLM
完成这样一个效果:
text
准备 Markdown / TXT 文档
↓
运行索引程序
↓
文档自动分片
↓
生成 Embedding
↓
写入 Qdrant
↓
用户输入问题
↓
向量检索相关知识
↓
拼接 Context
↓
交给大模型
↓
生成带来源的回答
最终可以直接这样使用:
bash
python app.py
然后提问:
text
请输入问题:RAG 为什么需要文本分片?
系统返回:
text
根据知识库中的资料,RAG 进行文本分片主要有两个原因:
1. 大模型和 Embedding 模型都有上下文长度限制;
2. 较小的 Chunk 可以提高检索粒度,让系统更准确地找到与问题相关的内容。
来源:
- rag.md
这才是一个真正意义上的最小 RAG。
一、这次我们不使用 LangChain
很多 RAG 教程上来就是:
python
from langchain...
然后十几行代码跑起来。
这种方式当然很方便,但对于第一次学习 RAG 的人来说,有一个问题:
你很容易"跑起来了",但不知道里面到底发生了什么。
所以这一篇我故意不使用:
text
LangChain
LlamaIndex
Dify
RAGFlow
我们自己把几个关键模块写出来:
text
Loader
Chunker
Embedding
VectorStore
Retriever
Prompt
Generator
这样你才能真正理解:
text
一段文档到底是怎么进入向量数据库的?
Query 到底怎么进行检索?
Qdrant 里面保存的是什么?
Context 是怎么拼起来的?
大模型最终拿到的 Prompt 是什么?
等你理解之后,再使用各种 RAG 框架,反而会简单很多。
二、最终项目结构
先建立项目:
bash
mkdir rag-demo
cd rag-demo
整个项目结构:
text
rag-demo/
├── data/
│ ├── rag.md
│ └── ai.txt
│
├── config.py
├── loader.py
├── chunking.py
├── embedding.py
├── vector_store.py
├── indexer.py
├── rag.py
├── app.py
│
├── requirements.txt
├── .env
├── docker-compose.yml
└── README.md
各模块职责:
text
loader.py
负责读取文档
chunking.py
负责文本分片
embedding.py
负责生成 Embedding
vector_store.py
负责操作 Qdrant
indexer.py
负责构建知识库索引
rag.py
负责检索 + Prompt + LLM
app.py
程序入口
这样后面继续增加:
text
PDF
Word
BM25
Reranker
FastAPI
Vue
都不需要推倒重写。
三、创建 Python 虚拟环境
推荐使用 Python 3.11 或以上版本。
创建:
bash
python -m venv .venv
macOS / Linux:
bash
source .venv/bin/activate
Windows:
bash
.venv\Scripts\activate
安装依赖:
bash
pip install openai qdrant-client python-dotenv
创建:
text
requirements.txt
内容:
txt
openai
qdrant-client
python-dotenv
以后可以直接:
bash
pip install -r requirements.txt
四、启动 Qdrant
这一篇不再使用第一篇里的:
python
documents = []
这种内存 Vector Store。
我们直接上真正的向量数据库:
text
Qdrant
Qdrant 是一个专门用于向量搜索的数据库,非常适合做:
text
RAG
语义搜索
推荐系统
图片搜索
知识库
Agent Memory
开发环境最方便的方式就是 Docker。
直接运行:
bash
docker run -d \
--name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant
其中:
text
6333
主要是 HTTP API。
text
6334
主要是 gRPC。
启动以后访问:
text
http://localhost:6333/dashboard
就可以看到 Qdrant Dashboard。
五、使用 Docker Compose 启动
我个人更推荐开发项目直接准备:
text
docker-compose.yml
写入:
yaml
services:
qdrant:
image: qdrant/qdrant:latest
container_name: rag-qdrant
restart: unless-stopped
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
启动:
bash
docker compose up -d
查看:
bash
docker compose ps
日志:
bash
docker compose logs -f qdrant
停止:
bash
docker compose down
注意:
bash
docker compose down
不会删除:
text
./qdrant_storage
里面的数据。
所以重新启动后知识库还在。
六、配置 API
创建:
text
.env
写入:
env
OPENAI_API_KEY=sk-xxxxxxxx
OPENAI_BASE_URL=https://api.openai.com/v1
CHAT_MODEL=gpt-5.6
EMBEDDING_MODEL=text-embedding-3-small
QDRANT_URL=http://localhost:6333
QDRANT_COLLECTION=rag_knowledge
这里有一点比较重要。
我们的代码使用:
python
OpenAI(
api_key=...,
base_url=...
)
所以只要你的服务兼容 OpenAI API,一般都可以修改:
env
OPENAI_BASE_URL
接入自己的模型服务。
例如:
text
OpenAI
兼容 OpenAI 协议的中转服务
本地模型网关
统一模型 API Gateway
都可以按照这个思路接入。
七、统一读取配置
创建:
text
config.py
代码:
python
import os
from dotenv import load_dotenv
load_dotenv()
OPENAI_API_KEY = os.getenv(
"OPENAI_API_KEY"
)
OPENAI_BASE_URL = os.getenv(
"OPENAI_BASE_URL",
"https://api.openai.com/v1"
)
CHAT_MODEL = os.getenv(
"CHAT_MODEL",
"gpt-5.6"
)
EMBEDDING_MODEL = os.getenv(
"EMBEDDING_MODEL",
"text-embedding-3-small"
)
QDRANT_URL = os.getenv(
"QDRANT_URL",
"http://localhost:6333"
)
QDRANT_COLLECTION = os.getenv(
"QDRANT_COLLECTION",
"rag_knowledge"
)
以后整个项目统一引用这里。
八、准备我们的第一份知识库
创建:
text
data/rag.md
写一些内容:
markdown
# 什么是 RAG?
RAG 全称 Retrieval-Augmented Generation,
中文通常翻译为检索增强生成。
RAG 会在大语言模型生成回答之前,
先从外部知识库中检索与用户问题相关的资料。
然后把这些资料和用户问题一起发送给大语言模型。
# 为什么需要 Chunk?
真实文档可能非常长。
如果直接把整篇文档转换成一个 Embedding,
检索粒度会非常粗。
所以 RAG 系统一般会先进行文本分片,
将大文档拆分成多个较小的 Chunk。
例如:
Chunk Size 可以设置为 500。
Overlap 可以设置为 50。
Overlap 可以降低句子或语义被分割边界截断的问题。
# 什么是 Embedding?
Embedding 可以将文字转换成高维向量。
语义越相近的文本,
对应的向量通常也越接近。
因此可以通过向量距离实现语义搜索。
# 什么是 Qdrant?
Qdrant 是一个向量数据库。
它可以保存 Embedding 向量以及对应的 Payload,
并且提供高效的向量相似度搜索能力。
在 RAG 系统中,
Qdrant 可以用于保存知识库 Chunk 的向量。
现在它就是我们的:
text
Knowledge Base
九、实现文档 Loader
创建:
text
loader.py
代码:
python
from pathlib import Path
SUPPORTED_EXTENSIONS = {
".txt",
".md",
}
def load_file(path: str):
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(
f"文件不存在: {path}"
)
if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
raise ValueError(
f"暂不支持文件格式: {file_path.suffix}"
)
return file_path.read_text(
encoding="utf-8"
)
def load_directory(directory: str):
directory_path = Path(directory)
documents = []
for path in directory_path.rglob("*"):
if not path.is_file():
continue
if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
continue
content = load_file(
str(path)
)
documents.append({
"content": content,
"source": str(path),
})
return documents
测试:
python
from loader import load_directory
documents = load_directory(
"./data"
)
for doc in documents:
print(
doc["source"]
)
print(
doc["content"][:200]
)
十、实现 Chunk 文本分片
创建:
text
chunking.py
上一篇已经详细讲过 Chunk。
这里直接实现。
python
def chunk_text(
text: str,
chunk_size: int = 500,
overlap: int = 50
):
if chunk_size <= 0:
raise ValueError(
"chunk_size 必须大于 0"
)
if overlap < 0:
raise ValueError(
"overlap 不能小于 0"
)
if overlap >= chunk_size:
raise ValueError(
"overlap 必须小于 chunk_size"
)
chunks = []
start = 0
text_length = len(text)
while start < text_length:
end = min(
start + chunk_size,
text_length
)
chunk = text[
start:end
].strip()
if chunk:
chunks.append(
chunk
)
if end >= text_length:
break
start += (
chunk_size - overlap
)
return chunks
测试:
python
from chunking import chunk_text
text = """
RAG 是一种检索增强生成技术。
它会在回答问题之前检索知识库。
知识库通常会进行文本分片,
然后生成 Embedding。
"""
chunks = chunk_text(
text,
chunk_size=50,
overlap=10
)
for index, chunk in enumerate(chunks):
print(
f"Chunk {index}"
)
print(
chunk
)
print(
"-" * 50
)
十一、不要只保存 Chunk 内容
真正写入向量数据库的时候,不应该只有:
json
{
"content": "..."
}
还应该保存 Metadata。
例如:
json
{
"content": "RAG 是一种检索增强生成技术。",
"source": "data/rag.md",
"chunk_index": 3
}
所以可以进一步封装:
python
def chunk_document(
document,
chunk_size=500,
overlap=50
):
chunks = chunk_text(
document["content"],
chunk_size=chunk_size,
overlap=overlap
)
results = []
for index, chunk in enumerate(chunks):
results.append({
"content": chunk,
"source": document["source"],
"chunk_index": index,
})
return results
以后可以继续添加:
text
title
page
author
department
category
created_at
updated_at
version
permission
这些 Metadata 在企业 RAG 中非常重要。
十二、实现 Embedding
现在进入 RAG 的核心。
创建:
text
embedding.py
代码:
python
from openai import OpenAI
from config import (
OPENAI_API_KEY,
OPENAI_BASE_URL,
EMBEDDING_MODEL,
)
client = OpenAI(
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL,
)
def create_embeddings(
texts
):
if isinstance(
texts,
str
):
texts = [
texts
]
if not texts:
return []
response = client.embeddings.create(
model=EMBEDDING_MODEL,
input=texts,
)
return [
item.embedding
for item in response.data
]
def create_embedding(
text: str
):
return create_embeddings(
[text]
)[0]
测试:
python
from embedding import create_embedding
vector = create_embedding(
"什么是 RAG?"
)
print(
type(vector)
)
print(
len(vector)
)
print(
vector[:10]
)
你会得到类似:
text
[
0.01234,
-0.02341,
0.00882,
...
]
这就是:
text
Query Vector
十三、为什么要批量生成 Embedding?
不要这样写:
python
for chunk in chunks:
vector = create_embedding(
chunk["content"]
)
假设有:
text
10000 个 Chunk
就可能产生:
text
10000 次 API 请求
更合理的方法是批量:
python
texts = [
chunk["content"]
for chunk in chunks
]
vectors = create_embeddings(
texts
)
真实项目可以进一步做:
python
BATCH_SIZE = 100
例如:
python
def batch_list(
data,
batch_size
):
for i in range(
0,
len(data),
batch_size
):
yield data[
i:i + batch_size
]
使用:
python
for batch in batch_list(
chunks,
100
):
texts = [
item["content"]
for item in batch
]
vectors = create_embeddings(
texts
)
这样效率会高很多。
十四、连接 Qdrant
接下来创建:
text
vector_store.py
首先连接:
python
from qdrant_client import (
QdrantClient
)
from qdrant_client.models import (
Distance,
PointStruct,
VectorParams,
)
from config import (
QDRANT_URL,
QDRANT_COLLECTION,
)
client = QdrantClient(
url=QDRANT_URL
)
现在我们的 Python 已经可以连接:
text
Qdrant
十五、创建 Collection
Qdrant 中和传统数据库:
text
Table
比较类似的概念叫:
text
Collection
我们创建一个:
text
rag_knowledge
代码:
python
def ensure_collection(
vector_size: int
):
if client.collection_exists(
QDRANT_COLLECTION
):
return
client.create_collection(
collection_name=QDRANT_COLLECTION,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE,
),
)
这里:
python
Distance.COSINE
代表使用:
text
Cosine Similarity
进行向量相似度判断。
十六、为什么 Vector Size 必须一致?
假设 Embedding 模型产生:
text
1536 维
向量。
那么 Collection 就必须配置对应维度。
不能:
text
Collection = 768
然后写进去:
text
1536
否则会报错。
所以我们这里不直接写死:
python
1536
而是:
python
vector = create_embedding(
"测试"
)
vector_size = len(
vector
)
这样即使以后更换 Embedding 模型,也更方便。
十七、写入 Qdrant
继续完善:
text
vector_store.py
增加:
python
def upsert_chunks(
chunks,
vectors
):
points = []
for index, (
chunk,
vector
) in enumerate(
zip(chunks, vectors)
):
points.append(
PointStruct(
id=index,
vector=vector,
payload={
"content": chunk["content"],
"source": chunk["source"],
"chunk_index": chunk["chunk_index"],
}
)
)
client.upsert(
collection_name=QDRANT_COLLECTION,
points=points,
)
这里真正写入 Qdrant 的结构可以理解成:
json
{
"id": 1,
"vector": [
0.123,
-0.332,
0.781
],
"payload": {
"content": "RAG 是一种检索增强生成技术。",
"source": "data/rag.md",
"chunk_index": 0
}
}
这里有两个部分非常重要:
text
Vector
负责:
text
检索
而:
text
Payload
负责保存:
text
真正的文档内容
来源
页码
分类
Metadata
十八、实现完整知识库索引
现在创建:
text
indexer.py
把:
text
文档读取
↓
Chunk
↓
Embedding
↓
Qdrant
全部串起来。
python
from loader import load_directory
from chunking import (
chunk_document
)
from embedding import (
create_embeddings
)
from vector_store import (
ensure_collection,
upsert_chunks,
)
DATA_DIR = "./data"
def build_index():
print(
"1. 正在读取知识库..."
)
documents = load_directory(
DATA_DIR
)
print(
f"读取文档数量: {len(documents)}"
)
all_chunks = []
print(
"2. 正在进行文本分片..."
)
for document in documents:
chunks = chunk_document(
document,
chunk_size=500,
overlap=50,
)
all_chunks.extend(
chunks
)
print(
f"Chunk 数量: {len(all_chunks)}"
)
if not all_chunks:
print(
"没有可索引的内容"
)
return
print(
"3. 正在生成 Embedding..."
)
texts = [
chunk["content"]
for chunk in all_chunks
]
vectors = create_embeddings(
texts
)
print(
f"Embedding 数量: {len(vectors)}"
)
vector_size = len(
vectors[0]
)
print(
f"向量维度: {vector_size}"
)
print(
"4. 创建 Qdrant Collection..."
)
ensure_collection(
vector_size
)
print(
"5. 写入 Qdrant..."
)
upsert_chunks(
all_chunks,
vectors
)
print(
"知识库索引构建完成!"
)
if __name__ == "__main__":
build_index()
现在执行:
bash
python indexer.py
输出类似:
text
1. 正在读取知识库...
读取文档数量: 2
2. 正在进行文本分片...
Chunk 数量: 18
3. 正在生成 Embedding...
Embedding 数量: 18
向量维度: 1536
4. 创建 Qdrant Collection...
5. 写入 Qdrant...
知识库索引构建完成!
这一步执行完:
我们的知识已经真正进入向量数据库了。
十九、查看 Qdrant 中的数据
打开:
text
http://localhost:6333/dashboard
可以看到:
text
Collections
其中应该出现:
text
rag_knowledge
里面就保存了:
text
Chunk 1
→ Vector 1
Chunk 2
→ Vector 2
Chunk 3
→ Vector 3
...
到这里:
text
Indexing Pipeline
已经完成。
二十、接下来进入 Query Pipeline
前面执行的是离线流程:
text
Document
↓
Chunk
↓
Embedding
↓
Qdrant
用户提问以后执行的是另外一套流程:
text
Question
↓
Query Embedding
↓
Qdrant Search
↓
Top K
↓
Context
↓
LLM
↓
Answer
这两个流程一定要区分清楚。
二十一、实现向量召回
继续修改:
text
vector_store.py
增加搜索:
python
def search(
query_vector,
top_k=5
):
result = client.query_points(
collection_name=QDRANT_COLLECTION,
query=query_vector,
with_payload=True,
limit=top_k,
)
return result.points
现在我们就拥有了:
text
Vector Search
能力。
二十二、测试 Retrieval
创建一个临时测试:
python
from embedding import (
create_embedding
)
from vector_store import (
search
)
query = "为什么 RAG 要进行分片?"
query_vector = create_embedding(
query
)
results = search(
query_vector,
top_k=5
)
for result in results:
print(
"Score:",
result.score
)
print(
"Content:",
result.payload["content"]
)
print(
"Source:",
result.payload["source"]
)
print(
"=" * 80
)
执行之后,可能得到:
text
Score: 0.82
Content:
真实文档可能非常长。
如果直接把整篇文档转换成一个 Embedding,
检索粒度会非常粗。
所以 RAG 系统一般会先进行文本分片。
Source:
data/rag.md
这就意味着:
text
Query
和这个 Chunk:
text
语义很接近
所以 Qdrant 把它召回来了。
二十三、这一步就是 RAG 最核心的 Retrieval
整个过程实际上是:
text
为什么 RAG 要进行分片?
↓
Embedding Model
↓
Query Vector
↓
Qdrant
↓
计算向量相似度
↓
Top 5
注意:
现在还没有调用大模型。
目前发生的一切都属于:
text
Retrieval
很多 RAG 系统效果不好,真正的问题往往就发生在这里。
二十四、把召回结果转换成统一格式
我们不希望后面的业务代码直接依赖:
text
Qdrant SDK Object
所以可以包装一下:
python
def search_documents(
query_vector,
top_k=5
):
points = search(
query_vector,
top_k
)
results = []
for point in points:
results.append({
"score": point.score,
"content":
point.payload.get(
"content",
""
),
"source":
point.payload.get(
"source",
""
),
"chunk_index":
point.payload.get(
"chunk_index"
),
})
return results
这样后面的代码只面对:
python
[
{
"score": 0.82,
"content": "...",
"source": "rag.md"
}
]
更容易维护。
二十五、开始构造 Context
假设搜索得到:
text
Top 5
我们需要把它们组装成:
text
Context
例如:
text
[资料 1]
来源:data/rag.md
内容:
RAG 系统一般会对长文档进行分片......
[资料 2]
来源:data/ai.txt
内容:
Embedding 可以把文字转换成向量......
创建:
text
rag.py
首先实现:
python
def build_context(
documents
):
blocks = []
for index, doc in enumerate(
documents,
start=1
):
block = f"""
[资料 {index}]
来源:
{doc["source"]}
内容:
{doc["content"]}
""".strip()
blocks.append(
block
)
return "\n\n".join(
blocks
)
二十六、为什么一定要保存 Source?
因为一个真正好用的 RAG 不应该只返回:
text
答案
最好还能告诉用户:
text
答案来自哪里?
例如:
text
RAG 需要进行文本分片,是为了提高检索粒度,
同时避免单个文本过长。
来源:
《rag.md》
以后处理 PDF 的时候,还可以做到:
text
来源:
《员工手册.pdf》第 18 页
这对于:
text
企业知识库
法律文件
论文
内部制度
技术文档
非常重要。
因为用户需要:
验证大模型说的话到底有没有依据。
二十七、构造 RAG Prompt
继续:
text
rag.py
增加:
python
def build_prompt(
query,
context
):
return f"""
你是一个专业的知识库问答助手。
请严格根据下面提供的知识库资料回答用户问题。
要求:
1. 只能优先依据提供的知识库内容回答;
2. 不要编造知识库中不存在的事实;
3. 如果知识库无法回答,请明确说明;
4. 回答尽量清晰、有条理;
5. 最后列出你参考的资料来源。
====================
知识库资料
====================
{context}
====================
用户问题
====================
{query}
====================
回答
====================
""".strip()
这里有一个非常重要的思想:
RAG 并不是把资料扔给模型就结束了。
Context 如何组织、Prompt 如何约束,同样会影响最终效果。
二十八、调用大模型生成答案
继续:
text
rag.py
引入:
python
from openai import OpenAI
from config import (
OPENAI_API_KEY,
OPENAI_BASE_URL,
CHAT_MODEL,
)
llm_client = OpenAI(
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL,
)
增加:
python
def generate_answer(
prompt
):
response = llm_client.responses.create(
model=CHAT_MODEL,
input=prompt,
)
return response.output_text
这样:
text
Retrieval
和:
text
Generation
终于连起来了。
二十九、实现完整 RAG
继续:
text
rag.py
完整:
python
from openai import OpenAI
from config import (
OPENAI_API_KEY,
OPENAI_BASE_URL,
CHAT_MODEL,
)
from embedding import (
create_embedding
)
from vector_store import (
search_documents
)
llm_client = OpenAI(
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL,
)
def build_context(
documents
):
blocks = []
for index, doc in enumerate(
documents,
start=1
):
block = f"""
[资料 {index}]
来源:
{doc["source"]}
内容:
{doc["content"]}
""".strip()
blocks.append(
block
)
return "\n\n".join(
blocks
)
def build_prompt(
query,
context
):
return f"""
你是一个专业的知识库问答助手。
请严格根据下面提供的知识库资料回答用户问题。
要求:
1. 优先依据知识库回答;
2. 不允许编造不存在的事实;
3. 如果知识库没有答案,请明确说明;
4. 给出简洁、准确、有条理的回答;
5. 在回答最后注明资料来源。
======== 知识库 ========
{context}
======== 用户问题 ========
{query}
======== 回答 ========
""".strip()
def generate_answer(
prompt
):
response = llm_client.responses.create(
model=CHAT_MODEL,
input=prompt,
)
return response.output_text
def ask(
query,
top_k=5
):
# 1. Query Embedding
query_vector = create_embedding(
query
)
# 2. Retrieval
documents = search_documents(
query_vector,
top_k=top_k
)
# 3. Context
context = build_context(
documents
)
# 4. Prompt
prompt = build_prompt(
query,
context
)
# 5. Generation
answer = generate_answer(
prompt
)
return {
"answer": answer,
"documents": documents,
}
我们的 RAG 核心代码已经完成。
三十、实现命令行问答
最后创建:
text
app.py
代码:
python
from rag import ask
def main():
print(
"=" * 60
)
print(
"RAG 知识库问答系统"
)
print(
"输入 exit 退出"
)
print(
"=" * 60
)
while True:
query = input(
"\n请输入问题:"
).strip()
if not query:
continue
if query.lower() in {
"exit",
"quit",
"q"
}:
print(
"Bye."
)
break
result = ask(
query,
top_k=5
)
print(
"\n回答:\n"
)
print(
result["answer"]
)
print(
"\n召回资料:"
)
for index, doc in enumerate(
result["documents"],
start=1
):
print(
f"\n[{index}] "
f"score={doc['score']:.4f}"
)
print(
f"source={doc['source']}"
)
if __name__ == "__main__":
main()
三十一、真正跑起来
整个项目启动分三步。
第一步:启动 Qdrant
bash
docker compose up -d
确认:
bash
docker compose ps
第二步:构建知识库
bash
python indexer.py
第一次必须执行。
因为要完成:
text
Document
↓
Chunk
↓
Embedding
↓
Qdrant
第三步:开始问答
bash
python app.py
例如:
text
请输入问题:什么是 RAG?
系统执行:
text
什么是 RAG?
↓
Embedding
↓
Qdrant Search
↓
Top 5
↓
Context
↓
LLM
↓
Answer
最终回答:
text
RAG 全称 Retrieval-Augmented Generation,
中文通常称为检索增强生成。
它的核心方式是在大模型回答用户问题之前,
先从外部知识库中检索与问题相关的信息,
然后将这些信息作为上下文提供给大模型。
这种方式可以让模型使用训练数据之外的知识,
同时降低回答脱离实际资料的概率。
来源:
data/rag.md
至此:
第一个真正完整的 RAG 已经跑起来了。
三十二、把 Debug 信息打印出来
做 RAG 时,我非常建议大家不要只打印:
text
最终答案
而应该打印:
text
Query
Retrieval Results
Score
Context
Prompt
Answer
例如:
python
def debug_retrieval(
documents
):
print(
"\n====== Retrieval ======"
)
for index, doc in enumerate(
documents,
start=1
):
print(
f"\nDocument {index}"
)
print(
f"Score: {doc['score']}"
)
print(
f"Source: {doc['source']}"
)
print(
doc["content"][:500]
)
为什么这么做?
假设用户问:
text
Qdrant 有什么作用?
大模型回答错了。
不要第一反应就是:
text
是不是 GPT 不够强?
先看:
text
Retrieval
有没有找到:
text
Qdrant 是一个向量数据库......
如果没有:
text
这是检索问题。
如果已经找到了,大模型却回答错:
text
才应该继续检查 Prompt 或 Generation。
这个 Debug 思路非常重要。
三十三、RAG 系统其实有两条 Pipeline
到这里应该很容易理解:
Indexing Pipeline
知识进入系统:
text
Markdown
↓
Loader
↓
Chunk
↓
Embedding
↓
Vector
↓
Qdrant
一般:
text
文档新增
文档修改
重新建立索引
的时候执行。
Query Pipeline
用户进行查询:
text
Question
↓
Query Embedding
↓
Qdrant
↓
Top K
↓
Context
↓
Prompt
↓
LLM
↓
Answer
每次用户提问都会执行。
把这两条链路分清楚,基本就掌握了 RAG 项目的骨架。
三十四、我们为什么没有把整个文档直接交给模型?
有人可能会问:
我就这一两个 Markdown,为什么不直接全部发给 GPT?
因为我们现在练习的是可以扩展的架构。
现在可能只有:
text
2 个文件
以后可能是:
text
100 个文件
10000 个文件
10 万个 Chunk
100 万个 Chunk
真正的 RAG 应该做到:
text
1000000 Chunk
↓
Retrieval
↓
Top 20
↓
Rerank
↓
Top 5
↓
LLM
而不是:
text
把 100 万个 Chunk 全塞给 LLM。
三十五、当前版本还有哪些问题?
虽然我们的 RAG 已经可以运行,但它仍然只是:
text
RAG V1
还有很多问题。
例如:
1. Chunk 方法比较简单
现在是:
text
固定字符长度
后面可以升级成:
text
按 Markdown 标题
按段落
按句子
按 Token
Semantic Chunking
2. 只有 Vector Search
目前:
text
Query
↓
Embedding
↓
Vector Search
后面可以加入:
text
BM25
形成:
text
Vector Search
+
BM25
↓
Hybrid Search
3. 没有 Reranker
现在:
text
Vector Top 5
↓
LLM
更好的方案:
text
Vector Top 20
↓
Reranker
↓
Top 5
↓
LLM
4. 只支持 Markdown 和 TXT
后面可以增加:
text
PDF
Word
HTML
网页
Excel
数据库
GitHub
5. 没有权限控制
企业 RAG 还必须考虑:
text
A 部门的数据
不能被 B 部门搜索
所以后面需要:
text
Metadata Filter
三十六、下一步可以增加 Metadata Filter
例如我们写入:
json
{
"content": "...",
"department": "finance",
"source": "财务制度.pdf"
}
检索的时候,就可以限制:
text
只搜索 finance
这样可以形成:
text
Query
↓
Filter
↓
Vector Search
它会是企业知识库权限控制的重要基础。
三十七、为什么我更建议自己先写一遍?
我平时做后端、AI 工具和各种应用时,一个比较明显的体会是:
AI 时代很多框架会帮我们把代码变得越来越少,但理解底层链路反而越来越重要。
比如 RAG。
直接用框架:
python
rag = SomeFramework(...)
rag.ask(...)
可能一天就能做出来。
但是客户问你:
text
为什么这个答案没搜出来?
为什么知识库明明有这句话?
为什么两个文档搜索结果冲突?
为什么换 Embedding 模型后报维度错误?
为什么 Chunk Size 从 500 改成 1000 反而变差?
最后还是要回到底层:
text
Loader
Chunk
Embedding
Vector
Retrieval
Rerank
Context
Generation
所以我更建议第一次做 RAG 的时候:
至少自己手写一遍最小实现。
三十八、我们这次到底完成了什么?
回顾一下。
首先准备原始知识:
text
Markdown / TXT
然后:
text
Loader
读取:
text
Document
接着:
text
Chunk
产生:
text
Chunk 1
Chunk 2
Chunk 3
...
然后:
text
Embedding Model
转换成:
text
Vector 1
Vector 2
Vector 3
...
保存:
text
Qdrant
用户提问之后:
text
Query
↓
Query Embedding
↓
Qdrant Retrieval
↓
Top K
↓
Context
↓
LLM
↓
Answer
这已经是一个完整 RAG 系统的核心骨架。
三十九、完整流程图
整个项目最终可以画成:
text
Indexing Pipeline
┌──────────────┐
│ Markdown/TXT │
└──────┬───────┘
↓
┌──────────────┐
│ Loader │
└──────┬───────┘
↓
┌──────────────┐
│ Chunk │
└──────┬───────┘
↓
┌──────────────┐
│ Embedding │
└──────┬───────┘
↓
┌──────────────┐
│ Qdrant │
└──────────────┘
Query Pipeline
┌──────────────┐
│ User Question│
└──────┬───────┘
↓
┌──────────────┐
│ Embedding │
└──────┬───────┘
↓
┌──────────────┐
│ Qdrant │
│ Vector Search│
└──────┬───────┘
↓
┌──────────────┐
│ Top K │
└──────┬───────┘
↓
┌──────────────┐
│ Context │
└──────┬───────┘
↓
┌──────────────┐
│ Prompt │
└──────┬───────┘
↓
┌──────────────┐
│ LLM │
└──────┬───────┘
↓
┌──────────────┐
│ Answer │
└──────────────┘
这就是:
text
RAG V1
四十、最后
如果你完整跟着这两篇做下来,现在应该已经不只是知道:
text
"RAG 是检索增强生成。"
而是真的知道一个问题从进入系统开始,到最后生成答案,中间发生了什么:
text
Question
↓
Embedding
↓
Retrieval
↓
Context
↓
Generation
第一篇,我们解决的是:
RAG 为什么能够工作?
第二篇,我们解决的是:
RAG 到底怎么跑起来?
但真实项目还有一个非常现实的问题:
text
企业知识不是几个 TXT。
真实场景通常是:
text
PDF
Word
Markdown
网页
技术文档
产品手册
公司制度
论文
各种复杂文件
特别是 PDF。
一个 PDF 里面可能还有:
text
标题
段落
页码
表格
图片
页眉
页脚
扫描件
如何把这些东西正确解析成适合 RAG 的知识,其实又是一整个工程问题。
所以这个系列下一篇,我准备继续往真实项目推进。
RAG 实战教程系列
text
第一篇
RAG 工作原理与完整流程
------分片、索引、召回、重排和生成
第二篇
从 0 搭建一个可运行的 RAG 知识库
------Embedding、Qdrant 与问答实战
第三篇
PDF、Word、Markdown 知识库实战
------文档解析、清洗与多格式数据接入
第四篇
Hybrid Search 实战
------Vector Search + BM25
第五篇
Reranker 重排实战
------为什么召回之后还需要重新排序
第六篇
Query Rewrite 与 Multi Query
------提升复杂问题的召回率
第七篇
RAG 评估体系
------Recall@K、MRR 与答案质量评估
第八篇
FastAPI + Vue
------把 RAG 做成真正的 Web 产品
第九篇
企业级 RAG
------权限、Metadata、增量索引与数据隔离
第十篇
Agentic RAG
------让 AI Agent 自己决定搜索什么
关于作者
我是王仕宇,JavaPub 作者,也是一名长期做后端开发、AI 应用和开源项目的技术创作者。
这几年一个很明显的感受是:
AI 正在让"写代码"越来越便宜,但把技术组合成一个真正可以使用的产品,依然需要理解工程、架构、业务和用户。
RAG 就是一个很典型的例子。
Demo 很容易。
真正做到:
text
能搜到
搜得准
回答可信
来源可追溯
权限不串数据
文档可以持续更新
才是 RAG 真正开始产生价值的地方。
后面的文章,我也会继续以这种方式,把 RAG 从一个 Demo,一步一步做到真正能落地的项目。
现在是最好的时代。中国有全世界最高性价比的制造业,有发达的网络和全球物流。
只要你愿意,你几乎可以买到这个世界上任何地方生产的、任何你想要的商品。你可以用很低的成本,撬动全球的资源为你服务。
不过,真正稀缺的,从来不是商品,而是注意力、判断力,以及把事情做成的能力。
所以,这是最好的时代,也是最坏的时代。
我是王仕宇,关注 AI、Web3 与开源,持续探索如何把技术做成产品,把产品转化为真实价值。
JavaPub:
text
https://javapub.net.cn/