Agentic Research

RAG In-Depth Principles and Practice: A Complete Guide from Chunking to Rerank

2026/05/1019 min readBryan Chan閱讀中文原文
TopicsRAGLLMDeep Learning

What is RAG?

RAG (Retrieval-Augmented Generation) lets an LLM retrieve relevant documents from an external knowledge base before answering, then generate an answer based on the retrieved results. This solves two core problems of LLMs:

  1. Knowledge cutoff: LLMs only know information from their training data
  2. Hallucination: LLMs may fabricate facts that do not exist
User question → Embedding → Vector search → Top-K documents → LLM generation → Answer with sources

Core Component Breakdown

1. Embedding Model Selection

ModelDimensionsLanguageCostApplicable Scenarios
text-embedding-3-small1536Multilingual$0.02/1M tokensGeneral purpose, cost-sensitive
text-embedding-3-large3072Multilingual$0.13/1M tokensHigh-precision requirements
bge-large-zh-v1.51024ChineseFree (local)Preferred for Chinese scenarios
bge-m31024MultilingualFree (local)Mixed multilingual scenarios
jina-embeddings-v31024MultilingualFree (API)Long documents (8K tokens)

Recommendation: For Chinese-first scenarios, use bge-large-zh-v1.5; for multilingual scenarios, use bge-m3; for API solutions, use text-embedding-3-small.

# Ollama Local Embedding
ollama pull nomic-embed-text
# or bge-m3
ollama pull bge-m3

2. Chunking Strategies

This is the most easily overlooked yet most critical step. Incorrect chunking directly leads to retrieval failure.

StrategySuitable ScenariosProsCons
Fixed size (500 tokens)General purposeSimple, predictableMay truncate mid-sentence
Semantic splitting (by paragraph/section)Structured documentsPreserves semantic integrityInconsistent chunk sizes
Recursive splitting (by \n\n → \n → 。)Mixed documentsAdaptiveComplex to implement
Sentence windowPrecise retrievalEach sentence is independentLoses context

Best practices:

  • Chunk size: 256-512 tokens (too small loses context, too large dilutes semantics)
  • Overlap: 10-20% (ensures key information is not truncated)
  • Use LangChain's RecursiveCharacterTextSplitter
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ",", " ", ""]
)
chunks = splitter.split_text(document)

3. Vector Database Comparison

OptionDeploymentScaleSuitable Scenarios
ChromaLocal/Embedded<100K docsPrototyping/Personal projects
QdrantLocal/DockerMillion-scaleProduction
MilvusDistributedHundred-million scaleEnterprise-grade
PineconeCloud SaaSUnlimitedNo infrastructure management
pgvectorPostgreSQL extensionMillion-scaleProjects already using PG

Recommended path: Use Chroma for prototyping → use Qdrant Docker for production → use Milvus for very large scale.

# Qdrant Quick Start
docker run -p 6333:6333 qdrant/qdrant

4. Rerank (Reranking)

After initial vector retrieval, use a more precise Cross-encoder to rerank the Top-K results, significantly improving accuracy.

ModelLanguageDescription
bge-reranker-v2-m3MultilingualBest open source
Cohere RerankMultilingualAPI, best performance
bge-reranker-largeChinese/EnglishRecommended for Chinese scenarios

Why is Rerank needed? Vector similarity ≠ semantic relevance. Rerank uses a Cross-encoder to compare query and document pairwise, making it more precise.

# Typical workflow
# 1. Vector retrieval → 20 candidates
candidates = vector_db.search(query, top_k=20)

# 2. Rerank → take top 5
from FlagEmbedding import FlagReranker
reranker = FlagReranker('BAAI/bge-reranker-v2-m3')
scores = reranker.compute_score([[query, doc] for doc in candidates])
top_5 = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]

# 3. LLM generation
answer = llm.generate(query, context=top_5)

Complete RAG Pipeline Code

from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.llms import Ollama

# 1. Load document
with open("knowledge_base.txt") as f:
    text = f.read()

# 2. Chunking
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(text)

# 3. Embedding + store in vector store
embeddings = OllamaEmbeddings(model="bge-m3")
vectorstore = Chroma.from_texts(chunks, embeddings)

# 4. Retrieval
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
docs = retriever.get_relevant_documents("What is RAG?")

# 5. Generation
llm = Ollama(model="qwen2.5:7b")
context = "\n".join([d.page_content for d in docs])
answer = llm.invoke(f"Answer the question based on the following documents:\n{context}\n\nQuestion: What is RAG?")

Common Pitfalls

PitfallCauseSolution
Irrelevant retrieval resultsChunk size too large or too smallTune to 256-512, add overlap
Slow retrievalNo index builtBuild an HNSW index with Qdrant/Milvus
High embedding costCalls the API on every queryDeploy bge-m3 locally
Answer hallucinationLLM ignores retrieval resultsForce source citations in the prompt
Mixed Chinese-English queries failEmbedding model does not support themUse the bge-m3 multilingual model

Recommended Reading