Module 4: Evaluation and Chunking Strategies
Semantic Chunking: Respecting Document Structure
Capsule overview
Semantic chunking splits text at natural boundaries (sentences, paragraphs, sections) instead of every N tokens. This preserves coherence and context, producing chunks that are more understandable for humans and LLMs.
In this capsule you'll learn sentence-based chunking, paragraph-based chunking, how to use LangChain's RecursiveCharacterTextSplitter, and the trade-offs vs fixed-size. You'll also implement production-ready semantic chunking.
By the end, you'll be able to choose between fixed-size and semantic depending on your use case.
Sentence-Based Chunking
Concept:
# Split by sentences
# Accumulate sentences until reaching ~N tokens
text = """
Python is a programming language. It was created in 1991.
Python is known for simplicity. It supports multiple paradigms.
"""
# Fixed-size (cuts mid-sentence):
# Chunk 1: "Python is a programming language. It wa"
# Chunk 2: "s created in 1991. Python is known for"
# ❌ Cuts badly
# Sentence-based (respects sentences):
# Chunk 1: "Python is a programming language. It was created in 1991."
# Chunk 2: "Python is known for simplicity. It supports multiple paradigms."
# ✅ Coherent
Implementation with a simple split:
import tiktoken
class SentenceChunker:
"""Sentence-based chunker"""
def __init__(self, max_tokens=500, model="gpt-4"):
self.max_tokens = max_tokens
self.encoding = tiktoken.encoding_for_model(model)
def chunk(self, text):
"""Split into chunks respecting sentences"""
# Split by periods (simple sentence boundary)
sentences = text.split('. ')
chunks = []
current_chunk = []
current_tokens = 0
for sentence in sentences:
sentence = sentence.strip() + '.'
sentence_tokens = len(self.encoding.encode(sentence))
# If adding this sentence exceeds the limit, close the current chunk
if current_tokens + sentence_tokens > self.max_tokens and current_chunk:
chunks.append(' '.join(current_chunk))
current_chunk = []
current_tokens = 0
current_chunk.append(sentence)
current_tokens += sentence_tokens
# Add the last chunk
if current_chunk:
chunks.append(' '.join(current_chunk))
return chunks
# Usage
chunker = SentenceChunker(max_tokens=30)
text = """
Python is a high-level programming language. It was created by Guido van Rossum.
Python emphasizes code readability. It supports multiple programming paradigms.
Python has a large standard library. It is widely used in data science and AI.
"""
chunks = chunker.chunk(text)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}:")
print(chunk)
print(f"Tokens: {len(chunker.encoding.encode(chunk))}\n")
Output:
Chunk 1:
Python is a high-level programming language. It was created by Guido van Rossum. Python emphasizes code readability.
Tokens: 23
Chunk 2:
It supports multiple programming paradigms. Python has a large standard library. It is widely used in data science and AI..
Tokens: 25
✅ Each chunk ends at a natural boundary (end of sentence).
Paragraph-Based Chunking
Concept:
# Split by paragraphs (double newline: \n\n)
# More context than sentences, respects the author's structure
text = """
Python is a programming language created in 1991.
It emphasizes code readability and simplicity.
Python supports multiple paradigms including OOP.
It has a large ecosystem of libraries and frameworks.
Python is widely used in data science and AI.
Popular libraries include NumPy, Pandas, and TensorFlow.
"""
# Paragraph-based:
# Chunk 1: "Python is a programming language... simplicity."
# Chunk 2: "Python supports multiple paradigms... frameworks."
# Chunk 3: "Python is widely used... TensorFlow."
# ✅ Preserves the author's logical structure
Implementation:
class ParagraphChunker:
"""Paragraph-based chunker"""
def __init__(self, max_tokens=500, model="gpt-4"):
self.max_tokens = max_tokens
self.encoding = tiktoken.encoding_for_model(model)
def chunk(self, text):
"""Split by paragraphs"""
# Split by double newline
paragraphs = text.split('\n\n')
chunks = []
current_chunk = []
current_tokens = 0
for paragraph in paragraphs:
paragraph = paragraph.strip()
if not paragraph:
continue
para_tokens = len(self.encoding.encode(paragraph))
# If the paragraph alone exceeds the limit, split by sentences
if para_tokens > self.max_tokens:
# Fall back to sentence-based for this paragraph
sentence_chunker = SentenceChunker(self.max_tokens)
para_chunks = sentence_chunker.chunk(paragraph)
chunks.extend(para_chunks)
continue
# If adding this paragraph exceeds the limit
if current_tokens + para_tokens > self.max_tokens and current_chunk:
chunks.append('\n\n'.join(current_chunk))
current_chunk = []
current_tokens = 0
current_chunk.append(paragraph)
current_tokens += para_tokens
# Add the last chunk
if current_chunk:
chunks.append('\n\n'.join(current_chunk))
return chunks
LangChain RecursiveCharacterTextSplitter
What it is:
LangChain's recursive chunker: It tries to split by hierarchical separators (\n\n → \n → space → character).
Advantage: A balance between semantic and fixed-size.
Code with LangChain:
from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
# Create the splitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # Maximum size in characters
chunk_overlap=20, # Overlap
length_function=len, # Function to measure size
separators=["\n\n", "\n", " ", ""] # Hierarchical separators
)
text = """
# Python Programming
Python is a high-level programming language.
It was created by Guido van Rossum in 1991.
## Features
Python emphasizes code readability and simplicity.
It supports multiple programming paradigms.
Python has a large standard library.
The ecosystem includes frameworks like Django and Flask.
## Use Cases
Python is widely used in:
- Data Science
- Machine Learning
- Web Development
- Automation
"""
# Split
chunks = text_splitter.split_text(text)
print(f"Total chunks: {len(chunks)}\n")
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}:")
print(chunk)
print(f"Length: {len(chunk)} chars\n")
Output:
Total chunks: 4
Chunk 1:
# Python Programming
Python is a high-level programming language.
It was created by Guido van Rossum in 1991.
## Features
Length: 123 chars
Chunk 2:
## Features
Python emphasizes code readability and simplicity.
It supports multiple programming paradigms.
Length: 107 chars
Chunk 3:
Python has a large standard library.
The ecosystem includes frameworks like Django and Flask.
## Use Cases
Length: 107 chars
Chunk 4:
## Use Cases
Python is widely used in:
- Data Science
- Machine Learning
- Web Development
- Automation
Length: 104 chars
✅ It respects headers, paragraphs, and lists (and the overlap repeats ## Features and ## Use Cases between chunks).
Token-Aware Recursive Splitter
The problem with RecursiveCharacterTextSplitter:
# LangChain uses characters, not tokens
# chunk_size=500 characters ≠ 500 tokens
# Example:
text = "Python" * 100 # 600 characters
# Characters: 600
# Tokens (tiktoken): ~100 tokens
# If you set chunk_size=500 characters,
# but the real limit is 500 tokens,
# the chunk can exceed the token limit!
Solution: Token-aware splitter
from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
# Token counting function
def tiktoken_len(text):
encoding = tiktoken.encoding_for_model("gpt-4")
return len(encoding.encode(text))
# Token-aware splitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # Now in TOKENS
chunk_overlap=50,
length_function=tiktoken_len, # ← Use tiktoken
separators=["\n\n", "\n", " ", ""]
)
text = "Python is great. " * 200 # ~600 tokens
chunks = text_splitter.split_text(text)
print(f"Total chunks: {len(chunks)}")
for i, chunk in enumerate(chunks):
tokens = tiktoken_len(chunk)
print(f"Chunk {i+1}: {tokens} tokens")
✅ It guarantees that each chunk ≤ 500 tokens.
Trade-Offs: Fixed vs Semantic
Comparison:
| Criterion | Fixed-Size | Semantic |
|---|---|---|
| Coherence | ❌ Can cut mid-sentence | ✅ Respects boundaries |
| Chunk size | ✅ Predictable (~500 tokens) | ⚠️ Variable (200-1000) |
| Speed | ✅ Fast (~1ms) | ⚠️ Slower (~10ms) |
| Complexity | ✅ Simple (slice tokens) | ⚠️ Requires parsing |
| Context preservation | ❌ Can lose context | ✅ Preserves better |
When to use each one:
Fixed-Size:
✅ Critical speed (latency <5ms)
✅ Documents without clear structure
✅ Simplicity required
✅ Predictable size is important
Semantic:
✅ Critical coherence (legal, medical)
✅ Structured documents (headers, paragraphs)
✅ Context is important
✅ OK with variable-size chunks
Exercises
Exercise 1: Sentence-based chunker
Implement a chunker that splits by sentences:
import tiktoken
def sentence_chunker(text, max_tokens=100):
"""Split by sentences up to max_tokens"""
# Implement here
pass
# Test
text = "Python is great. JavaScript is popular. Go is fast. Rust is safe."
chunks = sentence_chunker(text, max_tokens=50)
print(chunks)
See solution
import tiktoken
def sentence_chunker(text, max_tokens=100):
"""Split by sentences"""
encoding = tiktoken.encoding_for_model("gpt-4")
sentences = [s.strip() + '.' for s in text.split('.') if s.strip()]
chunks = []
current = []
current_tokens = 0
for sentence in sentences:
tokens = len(encoding.encode(sentence))
if current_tokens + tokens > max_tokens and current:
chunks.append(' '.join(current))
current = []
current_tokens = 0
current.append(sentence)
current_tokens += tokens
if current:
chunks.append(' '.join(current))
return chunks
# Test
text = "Python is great. JavaScript is popular. Go is fast. Rust is safe."
chunks = sentence_chunker(text, max_tokens=20)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk}")
Exercise 2: LangChain token-aware
Use RecursiveCharacterTextSplitter with tiktoken:
See solution
from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
def tiktoken_len(text):
encoding = tiktoken.encoding_for_model("gpt-4")
return len(encoding.encode(text))
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=10,
length_function=tiktoken_len,
separators=["\n\n", "\n", " ", ""]
)
text = """
Python is a programming language.
It is very popular for AI and data science.
JavaScript is used for web development.
It runs in the browser and on servers with Node.js.
"""
chunks = text_splitter.split_text(text)
for i, chunk in enumerate(chunks):
tokens = tiktoken_len(chunk)
print(f"Chunk {i+1} ({tokens} tokens):")
print(chunk)
print()
Summary
What you learned:
- ✅ Sentence-based: Respects sentences, more coherent
- ✅ Paragraph-based: Respects paragraphs, preserves structure
- ✅ LangChain Recursive: Balance of fixed/semantic
- ✅ Token-aware: Guarantees chunks ≤ max_tokens
- ✅ Trade-offs: Fixed (fast) vs Semantic (coherent)
Key concepts:
- Semantic chunking preserves coherence
- RecursiveCharacterTextSplitter = best of both worlds
- Token-aware is critical for API limits
Additional resources
- LangChain Text Splitters - Official docs
- RecursiveCharacterTextSplitter - API reference
- Chunking Strategies - Pinecone guide
In the next capsule
Capsule 04: Retrieval Metrics
You'll learn:
- nDCG (Normalized Discounted Cumulative Gain)
- MRR (Mean Reciprocal Rank)
- Recall@K, Precision@K
- Implementation in Python
From chunking to quantitative evaluation.
Module 4 - Embeddings Deep Dive Guide Semantic chunking: respecting the document structure