חלוקה סמנטית (Semantic Chunking) ב-RAG לשנת 2026: השוואת שיטות עם קוד Python
איך לבחור בין Recursive, Semantic, Agentic ו-Late Chunking עבור צינור RAG ב-2026. השוואה מלאה עם קוד Python להרצה, טבלת עלויות, ומדריך לבחירת גודל chunk והערכת איכות.
חלוקה סמנטית (Semantic Chunking) היא שיטה לפיצול מסמכים לצינור RAG על סמך הדמיון הסמנטי בין משפטים סמוכים, במקום לפי מספר תווים או טוקנים קבוע. במקום לחתוך כל 512 תווים ולסכן קטיעה באמצע רעיון, השיטה מודדת עקומת דמיון של embeddings וקובעת נקודות שבירה במעברים בין נושאים. התוצאה: chunks בגדלים משתנים שמכילים רעיון שלם, שיפור של 8–18% ב-Recall@10 ברוב benchmarks של 2026, ופחות רעש בהקשר שמועבר ל-LLM. במאמר הזה אשווה ארבע גישות מרכזיות ל-2026 (פיצול רקורסיבי, Semantic Chunking קלאסי, Agentic Chunking מבוסס LLM ו-Late Chunking) עם קוד Python רץ לכל אחת מהן.
חלוקה סמנטית מזהה נקודות שבירה בין נושאים על ידי מדידת cosine distance בין embeddings של משפטים סמוכים, במקום לפצל לפי אורך קבוע.
ל-LangChain SemanticChunker יש ארבע שיטות סף: percentile, standard_deviation, interquartile ו-gradient. כל אחת מתאימה לסוג טקסט אחר.
Agentic Chunking (חלוקה מבוססת LLM) מספק את האיכות הגבוהה ביותר בהיררכיות מורכבות, אך עולה פי 20–50 יותר מ-Semantic Chunking סטנדרטי.
Late Chunking של Jina (שהוצג ב-2024, מיושם בהרחבה ב-2026) מייצר embeddings לכל המסמך תחילה, ורק אז מחלק. כך משמר קונטקסט ארוך-טווח.
לרוב המקרים בייצור: recursive chunking עם 500–800 טוקנים ו-15% overlap הוא בסיס טוב; החליפו ל-Semantic Chunking כשיש עדות ל-context loss ב-evals.
גודל chunk אופטימלי ב-2026 עבור מודלי embedding מודרניים (text-embedding-3-large, voyage-3-large) הוא 400–700 טוקנים. קטן מדי פוגע ב-context, גדול מדי מדלל את הסיגנל.
מהי חלוקה סמנטית ומדוע היא חשובה ב-2026
ב-RAG קלאסי, המסמך מפוצל לפני האינדקוסינג ל-chunks. השיטה הפשוטה ביותר, fixed-size chunking, חותכת כל N תווים או טוקנים. הבעיה: החיתוך יכול ליפול באמצע משפט, באמצע טבלה, או בין השאלה לתשובה שלה. מנוע retrieval יאחזר chunk שמתחיל באמצע פסקה, כאשר כל ההקשר החשוב שלה נמצא במסמך שכן.
אני זוכר שנתקלתי בזה בפרויקט אחד לפני כמה חודשים. המערכת החזירה תשובות שנשמעו סבירות, אבל המספרים היו לא נכונים. מסתבר שהחיתוך "עגלגל" גזר את השורה שהגדירה את יחידת המידה, וה-LLM המציא הנחה סבירה במקום. לא כיף.
Recursive chunking (למשל RecursiveCharacterTextSplitter) פותר חלק מהבעיה על ידי חלוקה בהיררכיה: קודם לפי פסקאות, ואם עדיין ארוך אז לפי משפטים, ואם עדיין ארוך אז לפי מילים. זה הבסיס המומלץ עד היום ב-2026 לרוב המקרים, אבל הוא עדיין מבוסס גבולות תחביריים ולא סמנטיים. פסקה ארוכה שמדברת על שני נושאים תישאר כ-chunk אחד; שני משפטים על אותו נושא במיקומים סמוכים יכולים להיחתך לרעיונות נפרדים.
חלוקה סמנטית, שהוצגה ב-notebook המכונן של Greg Kamradt "5 Levels of Text Splitting" ב-2024 והפכה לסטנדרט תעשייתי ב-2026, מודדת את המרחק הסמנטי בין משפטים סמוכים. כאשר המרחק חוצה סף מסוים, נקבעת נקודת שבירה. זה מבטיח ש-chunk אחד מכיל רעיון אחד, ומתנהג טוב במיוחד עם מסמכים ארוכים כמו דוחות רבעוניים, תיעוד טכני, ומאמרים מדעיים.
למי שרוצה להעמיק בכל השרשרת של בניית RAG ייצורי (כולל embedding, retrieval, reranking וניהול הקשר), המדריך המעשי לצינורות RAG לייצור ב-2026 מפרט את הצינור המלא שהחלוקה היא רק החוליה הראשונה בו.
השוואת שיטות חלוקה, טבלת סיכום
לפני שנצלול לקוד, הנה סיכום ארבע השיטות הרלוונטיות ל-2026, כולל עלות יחסית וקריטריון הבחירה המרכזי לכל אחת:
קריטריון
Recursive
Semantic Chunking
Agentic Chunking
Late Chunking
מנגנון
הפרדה היררכית לפי תווים
cosine distance בין embeddings של משפטים
LLM מזהה גבולות סמנטיים
embed קודם, chunk אחר-כך
עלות ל-1M טוקנים (2026)
~$0 (חישוב מקומי)
$0.13 (text-embedding-3-small)
$3–$15 (GPT-4.1, Claude Sonnet 5)
$0.13 (embed בודד)
מהירות (עמודי A4 לשנייה)
~200
~30
~1–3
~25
איכות retrieval יחסית
Baseline (1.00×)
1.08×–1.18×
1.15×–1.25×
1.12×–1.22×
מתאים ל-
ברירת מחדל, טקסט הומוגני
דוחות, blog posts, wiki
מסמכים משפטיים, PRDs מורכבים
מסמכים ארוכים מעל 10K טוקנים
תלות במודל embedding
אין
גבוהה
עקיפה
גבוהה מאוד (דורש long-context embedding)
איך עובד Semantic Chunking מבפנים
האלגוריתם הבסיסי, כפי שהוגדר על ידי Kamradt ומיושם היום ב-LangChain SemanticChunker, פועל בחמישה שלבים:
פיצול למשפטים: לרוב עם regex פשוט על ., ?, !, או עם spaCy למקרים מורכבים.
יצירת "buffered sentences": כל משפט מוחלף בשרשור של עצמו עם המשפט הקודם והבא (חלון של 3), כדי להוסיף context מקומי ל-embedding.
יצירת embeddings: כל buffered sentence עובר דרך מודל embedding (בדרך כלל text-embedding-3-small או text-embedding-3-large ב-2026).
מדידת cosine distance: בין כל זוג embeddings סמוכים. התוצאה היא סדרת מרחקים.
זיהוי נקודות שבירה: מיקומים שבהם המרחק חוצה סף (percentile 95, למשל) הופכים לגבולות chunk.
הקריטי כאן הוא בחירת הסף. LangChain מציע ארבע שיטות ב-2026:
Percentile: סף = ה-percentile ה-N של המרחקים. פשוט וחזק לרוב המקרים.
Standard Deviation: סף = ממוצע + K סטיות תקן. עדיף לטקסטים הומוגניים.
Interquartile Range (IQR): עמיד ל-outliers, טוב לטקסטים עם רעש.
Gradient: הגישה החדשה ביותר; מזהה קפיצות חדות בגרדיאנט של סדרת המרחקים במקום ערך מוחלט. הכי טוב לתחומים בעלי אוצר מילים דומה (למשל מסמכים משפטיים).
מימוש עם LangChain SemanticChunker
הדוגמה הבאה משתמשת ב-langchain-experimental (עדיין הבית של SemanticChunker ב-LangChain 0.3+) עם OpenAI embeddings. הקוד מוכן להרצה. צריך רק OPENAI_API_KEY בסביבה:
# requirements: langchain-experimental==0.3.4, langchain-openai==0.3.5
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# breakpoint_threshold_type: "percentile" | "standard_deviation"
# | "interquartile" | "gradient"
splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95.0, # 95th percentile
buffer_size=1, # context window in sentences
min_chunk_size=200, # avoid tiny chunks
)
with open("q3_2026_earnings.md", encoding="utf-8") as f:
text = f.read()
docs = splitter.create_documents([text])
print(f"Produced {len(docs)} chunks")
for i, d in enumerate(docs[:3]):
print(f"--- chunk {i} ({len(d.page_content)} chars) ---")
print(d.page_content[:200], "...\n")
שני שיקולים חשובים בייצור:
Batching: SemanticChunker קורא ל-embedding API פעם אחת לכל משפט. למסמכים של 500 עמודים זה מהר יכול להגיע לאלפי קריאות. עטפו את ה-embedder ב-batching מקומי או השתמשו ב-OpenAIEmbeddings(chunk_size=1000) כדי לצבור בקשות.
Caching: אם אתם מעדכנים אינדקס, שמרו embeddings לפי hash של המשפט. השילוב עם אופטימיזציית עלויות דרך Prompt Caching יכול לחסוך 60–80% מעלות ה-re-indexing.
מימוש עם LlamaIndex SemanticSplitterNodeParser
LlamaIndex מציע גרסה מקבילה עם ממשק שונה במקצת. SemanticSplitterNodeParser משתלב ישירות ב-IngestionPipeline של LlamaIndex ומחזיר TextNode עם metadata של מיקום מקורי:
# requirements: llama-index==0.13.2, llama-index-embeddings-openai==0.4.1
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(model="text-embedding-3-small")
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=embed_model,
)
with open("product_docs.md", encoding="utf-8") as f:
doc = Document(text=f.read(), metadata={"source": "product_docs.md"})
nodes = splitter.get_nodes_from_documents([doc])
print(f"Produced {len(nodes)} nodes")
# Downstream: index and query
index = VectorStoreIndex(nodes, embed_model=embed_model)
query_engine = index.as_query_engine(similarity_top_k=8)
response = query_engine.query("איך מגדירים webhooks עבור התוכנית Enterprise?")
print(response)
ההבדל המעשי המרכזי בין LangChain ל-LlamaIndex בהקשר של חלוקה סמנטית הוא ה-metadata. LlamaIndex שומר אוטומטית את מיקומי ההתחלה והסיום המקוריים של כל node ואת ה-node ה-"קודם" וה-"הבא", מה שמאפשר sentence-window retrieval או auto-merging retrieval בשלב הבא. עם LangChain צריך להוסיף metadata בעצמכם.
Agentic Chunking עם Claude ו-GPT-4.1
Agentic chunking היא הגישה היקרה ביותר אבל גם החכמה ביותר: משתמשים ב-LLM כדי לזהות גבולות סמנטיים. יש שתי צורות עיקריות ב-2026:
Proposition-based chunking: LLM מפרק את הטקסט להצהרות עצמאיות ("propositions") ואז מקבץ אותן לפי נושא.
LLM boundary detection: LLM עובר על הטקסט בחלונות של 4–8K טוקנים ומחזיר רשימת אינדקסי גבולות.
הנה מימוש קצר עם Claude Sonnet 5 (מזהה מודל: claude-sonnet-5) שמזהה גבולות בטקסט ומחזיר JSON structured:
# requirements: anthropic==0.68.0
import json
from anthropic import Anthropic
client = Anthropic()
SYSTEM = """You split documents into semantically coherent chunks.
Return a JSON array of objects: [{"start": int, "end": int, "topic": str}]
where start/end are 0-indexed character offsets in the input.
Each chunk should contain ONE coherent idea, between 300 and 1200 characters.
Return ONLY the JSON array, no prose."""
def agentic_chunk(text: str) -> list[dict]:
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=4096,
system=SYSTEM,
messages=[{"role": "user", "content": text}],
)
boundaries = json.loads(resp.content[0].text)
return [
{"topic": b["topic"], "text": text[b["start"]:b["end"]]}
for b in boundaries
]
with open("legal_contract.md", encoding="utf-8") as f:
contract = f.read()
chunks = agentic_chunk(contract)
for c in chunks[:3]:
print(f"[{c['topic']}] {c['text'][:120]}...")
Late Chunking של Jina, הגישה החדשה ל-2026
Late Chunking הוא רעיון שהוצג ב-מאמר של Jina AI מספטמבר 2024 וקיבל אימוץ רחב במהלך 2025–2026. הרעיון: במקום לחלק את המסמך לפני ה-embedding (מה שמאבד קונטקסט ארוך-טווח), מעבירים את כל המסמך דרך מודל embedding בעל long-context (עד 8K–32K טוקנים), מקבלים embeddings ברמת הטוקן, ורק אז מפצלים ל-chunks על ידי ממוצע embeddings של הטוקנים בכל chunk.
היתרון: כל chunk "יודע" מה היה לפניו ואחריו, כי ה-attention של המודל ראה את כל המסמך. זה פותר בעיה מוכרת שבה chunk שמזכיר "החברה" מאבד את המידע שהחברה הזו נקראה "Acme" בפסקה השלישית.
# requirements: transformers==4.56.0, torch==2.5.1, numpy==2.1
import numpy as np
import torch
from transformers import AutoModel, AutoTokenizer
model_name = "jinaai/jina-embeddings-v3"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name, trust_remote_code=True)
def late_chunk(text: str, chunk_boundaries: list[tuple[int, int]]):
"""chunk_boundaries: list of (start_token, end_token) tuples"""
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=8192)
with torch.no_grad():
outputs = model(**inputs)
token_embeddings = outputs.last_hidden_state[0] # (seq_len, dim)
chunk_embeddings = []
for start, end in chunk_boundaries:
chunk_vec = token_embeddings[start:end].mean(dim=0)
chunk_embeddings.append(chunk_vec.numpy())
return np.stack(chunk_embeddings)
# Get boundaries from any splitter (recursive/semantic/etc.)
# but embed AFTER splitting — that's the "late" part.
boundaries = [(0, 128), (128, 256), (256, 512)]
with open("annual_report.md", encoding="utf-8") as f:
embeddings = late_chunk(f.read(), boundaries)
print(embeddings.shape) # (3, 1024) for jina-v3
Late Chunking עובד הכי טוב עם מודלי embedding שאומנו לתמוך בו. jina-embeddings-v3, voyage-3-large, ו-nomic-embed-text-v2-moe נבחנו כולם ב-2026 והראו שיפור עקבי ב-Recall@10 של 12–22% על מסמכים ארוכים. השילוב הרגיש ביותר הוא Late Chunking + Reranking: מדריך Reranking ב-RAG 2026 מפרט את הצינור המלא שמנצל את שני הכלים יחד.
בחירת גודל chunk ו-overlap
שאלה שחוזרת בכל פרויקט RAG: מה גודל ה-chunk האופטימלי? אין תשובה אחת, אבל יש מסגרת החלטה שעובדת ב-2026:
400–800 טוקנים: נקודת האופטימום המומלצת ב-2026 עבור text-embedding-3-large, voyage-3-large ו-jina-embeddings-v3. מאזן בין דיוק ל-context.
800–1500 טוקנים: טוב לשאלות סינתטיות ("סכם את הפרק"), פחות טוב לפקטואליות.
מעל 1500 טוקנים: עדיף לעבור ל-Late Chunking או להעביר את הצינור לארכיטקטורת summary-index / small-to-big.
Overlap של 10–15% בין chunks סמוכים הוא סטנדרט. פחות מ-10% מסכן איבוד רעיונות שמתחלקים בין chunks; מעל 20% מנפח את האינדקס ומייקר את ה-retrieval בלי שיפור מעשי. בשילוב עם הנדסת הקשר, התכנון של מה נכנס לחלון ההקשר של ה-LLM ומה לא, גודל ה-chunk הוא אחד המשתנים הבודדים הכי מרכזיים בביצועי RAG.
קוד: מדידת התפלגות גדלים אחרי חלוקה
import statistics
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
def summarize_chunks(chunks: list[str]) -> dict:
token_counts = [len(enc.encode(c)) for c in chunks]
return {
"n_chunks": len(chunks),
"min": min(token_counts),
"max": max(token_counts),
"mean": round(statistics.mean(token_counts), 1),
"median": statistics.median(token_counts),
"p95": statistics.quantiles(token_counts, n=20)[-1],
}
# Use after any splitter
print(summarize_chunks([d.page_content for d in docs]))
# {'n_chunks': 87, 'min': 42, 'max': 1204, 'mean': 512.3, 'median': 487, 'p95': 923}
איך מודדים איכות חלוקה?
הטעות הגדולה של רוב הצוותים היא לבחור שיטת חלוקה על סמך תחושה או benchmark חיצוני, במקום למדוד את ההשפעה על הצינור שלהם. שיטת המדידה המומלצת ב-2026 כוללת שלוש שכבות:
מדדי retrieval: Recall@K, MRR ו-nDCG על סט שאלות-תשובות מייצג. אלה מדדים "לא-LLM", מדויקים, זולים, וניתנים להריץ בכל push.
Faithfulness ו-context relevance: האם התשובה של ה-LLM נאמנה ל-chunks שאוחזרו, והאם ה-chunks עצמם רלוונטיים? מדדים אלה נמדדים ב-RAGAS, DeepEval או TruLens.
מטריקות end-to-end: Answer correctness מול ground truth, נמדד עם LLM-as-Judge (Claude Sonnet 5 או GPT-4.1). זה היקר ביותר אך הכי חשוב.
מסגרת עבודה מומלצת: הגדירו סט של 100–500 שאלות מייצגות, הריצו את הצינור המלא עבור כל שיטת חלוקה, ומדדו את שלוש השכבות. אם Semantic Chunking שיפר את ה-Recall@10 ב-10% אבל את ה-Answer Correctness ב-1% בלבד, כנראה שהצוואר בקבוק שלכם הוא ב-generation, לא ב-retrieval. פרטים מעמיקים על הרצת evals לצינורות LLM נמצאים ב-מדריך בדיקות אפליקציות LLM עם DeepEval.
שאלות נפוצות
מהי חלוקה סמנטית ב-RAG?
חלוקה סמנטית היא שיטה לפיצול מסמכים לפני אינדוקס וקטורי לפי משמעות: המערכת מודדת cosine distance בין embeddings של משפטים סמוכים ושוברת את המסמך בנקודות שבהן המרחק חוצה סף מוגדר (למשל percentile 95). התוצאה היא chunks בגדלים משתנים שכל אחד מהם מכיל רעיון קוהרנטי אחד, במקום קטעים באורך קבוע שעלולים לחתוך רעיונות באמצע.
מה עדיף, חלוקה סמנטית או Recursive Chunking?
Recursive Chunking הוא בסיס טוב ומהיר לרוב המקרים, והוא ברירת המחדל המומלצת ב-2026. עברו ל-Semantic Chunking כאשר יש עדות ב-evals ל-context loss (Answer Correctness נמוך למרות Recall גבוה), או כשאתם עובדים עם מסמכים הטרוגניים כמו earnings reports, wikis ומאמרים אקדמיים. לתחומים אחידים כמו קטלוגי מוצרים ו-FAQs, ההפרש בין שתי השיטות בדרך כלל זניח.
מהו גודל ה-chunk האופטימלי ב-2026?
עבור מודלי embedding מודרניים כמו text-embedding-3-large, voyage-3-large ו-jina-embeddings-v3, נקודת האופטימום היא 400–700 טוקנים עם overlap של 10–15%. פחות מ-200 טוקנים מאבד קונטקסט; מעל 1500 מדלל את הסיגנל ב-embedding וגם צורך יותר טוקנים בהזרקה ל-LLM. עבור שאלות סינתטיות ארוכות (סיכומים, השוואות) עברו ל-Late Chunking או ארכיטקטורת summary-index.
האם Agentic Chunking שווה את העלות?
ברוב הפרויקטים, לא. Agentic Chunking עולה פי 20–50 יותר מ-Semantic Chunking ומאט את ה-ingestion בסדר גודל, אך משפר Recall רק ב-5–10% נוספים על Semantic Chunking. הוא מצדיק את עצמו במסמכים משפטיים ארוכים, PRDs מורכבים עם היררכיה עמוקה, ותרחישים שבהם עלות טעות retrieval אחת גבוהה מאלפי קריאות LLM. עבור chatbot תמיכה, הישארו עם Semantic או Recursive.
מה ההבדל בין Late Chunking ל-Semantic Chunking?
Semantic Chunking מחלק את המסמך לפני ה-embedding, כך שכל chunk מקבל embedding עצמאי שלא "יודע" מה היה במסמך מסביב לו. Late Chunking עושה את ההפך: מעביר את כל המסמך דרך מודל embedding בעל long-context (עד 8K–32K טוקנים) ורק אז מפצל, כך שכל chunk יורש קונטקסט ארוך-טווח מה-attention של המודל. Late Chunking דורש מודל embedding שאומן לכך (jina-embeddings-v3, voyage-3-large) ומשפר Recall במיוחד על מסמכים שבהם מוזכרים ישויות בשם מלא בהתחלה וב-pronoun בהמשך.
Reranking הוא צעד שני של אחזור ב-RAG שמשפר את דיוק הטופ-3 ב-15-40%. משווים בין Cohere Rerank 3.5, Voyage rerank-2.5, Jina Reranker v2 ו-BGE-reranker-v2-m3 עם קוד עובד, טבלה ומדריך החלטה לשנת 2026.
מדריך מעשי עם דוגמאות קוד לבניית צינורות RAG מוכנים לייצור ב-2026. מכסה אסטרטגיות חיתוך, מסדי וקטורים, חיפוש היברידי, RAG סוכני, הערכת RAGAS ושיקולי ייצור קריטיים שיעזרו לכם להימנות עם ה-10% שמצליחים.