چرا Chunking مهمترین بخش یک سیستم RAG است؟ تجربه طراحی چند استراتژی Chunking
Chunking چگونه کیفیت RAG را تغییر میدهد؟ در این مقاله Recursive، Semantic، Sliding Window، Token-based، Markdown، Code، Table و Composite Chunking را مقایسه میکنیم.
- RAG Chunking
- Chunking چیست
- بهترین روش Chunking
- Semantic Chunking
- Recursive Chunking
- RAG Architecture

چرا Chunking مهمترین بخش یک سیستم RAG است؟
در بسیاری از آموزشهای RAG، Chunking فقط یک مرحله کوتاه قبل از embedding است: متن را به قطعات ۵۰۰ توکنی تقسیم کن، کمی overlap بده و تمام. در پروژه واقعی ما، Chunking از یک utility ساده به یکی از مهمترین لایههای سیستم تبدیل شد.
دلیلش ساده است: مدل embedding چیزی را میبیند که Chunker به آن میدهد. اگر مرز Chunkها بد انتخاب شود، حتی بهترین embedding model هم نمیتواند اطلاعاتی را که اشتباه تقسیم شدهاند دوباره سرهم کند.
یک مثال ساده
فرض کنید بخشی از سند این باشد:
## Redis
Redis برای cache استفاده میشود...
## PostgreSQL
Connection Pool باید متناسب با تعداد workerها تنظیم شود...
ERR_DB-7312 زمانی رخ میدهد که ...
اگر Chunker صرفاً براساس تعداد کاراکتر ببرد، ممکن است انتهای بخش Redis و ابتدای PostgreSQL داخل یک Chunk قرار بگیرد. embedding حاصل، نماینده دو موضوع متفاوت خواهد شد.
اگر Chunk بیش از حد کوچک باشد نیز ممکن است ERR_DB-7312 از توضیح علتش جدا شود.
Chunking در واقع تلاش برای پیدا کردن یک تعادل میان انسجام معنایی و اندازه مناسب برای retrieval است.
معیار یک Chunk خوب چیست؟
از تجربه ما یک Chunk خوب معمولاً این ویژگیها را دارد:
- تا حد ممکن درباره یک موضوع مشخص است.
- بدون context اضافی بیش از حد بزرگ نیست.
- اطلاعات ضروری اطراف خود را کاملاً از دست نمیدهد.
- از محدودیت token مدل embedding عبور نمیکند.
- metadata ساختاری مثل heading، page و source را حفظ میکند.
- قابل ردیابی به متن اصلی است.
هیچ الگوریتم واحدی همه این شرایط را برای همه نوع سند تضمین نمیکند.
Recursive Chunking
Recursive Chunking یکی از بهترین baselineهاست. ایده این است که ابتدا سعی کنیم متن را با جداکنندههای معناییتر مثل heading، پاراگراف یا newline تقسیم کنیم و اگر قطعه هنوز بزرگ بود، سراغ separator کوچکتر برویم.
مزایا:
- سریع است.
- dependency خاصی ندارد.
- برای prose عمومی خوب عمل میکند.
- رفتار آن قابل پیشبینی است.
معایب:
- تغییر موضوع همیشه با newline مشخص نمیشود.
- ممکن است دو پاراگراف معنایی نزدیک را جدا کند.
- از semantic similarity آگاه نیست.
در بسیاری از سیستمها Recursive باید baseline باشد؛ نه لزوماً راهحل نهایی.
Semantic Chunking
Semantic Chunking تلاش میکند مرز Chunk را با تغییر معنی متن پیدا کند.
یک روش رایج این است:
- متن را به واحدهای کوچکتر مثل جمله تقسیم کنیم.
- برای هر واحد embedding بسازیم.
- similarity میان واحدهای متوالی را محاسبه کنیم.
- افت معنیدار similarity را بهعنوان boundary در نظر بگیریم.
مثلاً اگر چند جمله درباره PostgreSQL باشند و ناگهان متن وارد موضوع Kubernetes شود، فاصله embedding میتواند این تغییر را نشان دهد.
در نسخه ما adaptive semantic threshold نیز در نظر گرفته شد تا threshold کاملاً ثابت نباشد.
Semantic Chunking همیشه بهتر نیست
خیر. این روش هزینه بیشتری دارد و روی بعضی اسناد ساختاری، headingها اطلاعات مطمئنتری از embedding میدهند. همچنین اسناد دارای کد، جدول یا شناسههای فنی لزوماً نباید براساس شباهت جمله شکسته شوند.
بنابراین Semantic Chunking را باید یک ابزار در جعبهابزار دید، نه یک پاسخ جهانی.
Sliding Window
Sliding Window بخشهایی با اندازه ثابت میسازد که مقداری overlap دارند.
مثلاً:
Chunk 1: token 0 تا 500
Chunk 2: token 450 تا 950
Chunk 3: token 900 تا 1400
مزیت اصلی، کاهش احتمال از دست رفتن اطلاعات در مرزهاست.
اما overlap زیاد هزینه index را افزایش میدهد و میتواند duplicate candidate ایجاد کند. در retrieval باید diversity و deduplication مناسبی داشته باشیم.
Token-based Chunking
تقسیم براساس token از تقسیم براساس character دقیقتر است؛ چون محدودیت مدلها برحسب token تعریف میشود.
در ماژول abstraction برای token counter داریم تا بتوان از:
- Approximate Token Counter
- HuggingFace tokenizer
- tiktoken
استفاده کرد.
این طراحی باعث میشود Chunking با model budget هماهنگ باشد.
Markdown-aware Chunking
Markdown ساختار صریح دارد:
# عنوان
## بخش
### زیربخش
نادیده گرفتن این ساختار اشتباه است. headingها اطلاعات semantic و hierarchical مهمی دارند.
Markdown-aware Chunker میتواند مسیر بخش را در metadata نگه دارد و بعداً title representation یا parent context از آن استفاده کند.
Code Chunking
کد را نباید مثل prose برید.
اگر وسط یک function یا class chunk ایجاد کنیم، بخش زیادی از معنا از دست میرود. در Python میتوان از AST کمک گرفت و boundaryها را براساس function، class و ساختار syntax انتخاب کرد.
در ماژول برای Python از AST-aware chunking استفاده شده و برای زبانهای دیگر hook مربوط به Tree-sitter در نظر گرفته شده است.
یک Chunk کد خوب بهتر است یک واحد منطقی کامل باشد، حتی اگر اندازه آن کمی با chunkهای prose متفاوت باشد.
Table-aware Chunking
جدول یک مسئله خاص است.
اگر جدول را خط به خط و بدون header ببریم، rowها معنای خود را از دست میدهند. مثلاً مقدار 87 بدون نام ستون تقریباً بیمعنی است.
در Table-aware Chunking بهتر است header همراه rowها نگه داشته شود و ساختار جدول تا جای ممکن حفظ شود.
این موضوع برای گزارشهای مالی، specificationها و documentهای عملیاتی بسیار مهم است.
Composite Chunking؛ نتیجه تجربه با اسناد واقعی
اسناد واقعی معمولاً خالص نیستند. یک README ممکن است همزمان شامل:
- توضیح prose
- heading
- code block
- table
- list
باشد.
اگر کل فایل را با Code Chunker یا Semantic Chunker پردازش کنیم، بخشی از ساختار را خراب میکنیم.
Composite Chunking ابتدا نوع بخش را تشخیص میدهد و بعد strategy مناسب را روی همان بخش اعمال میکند.
مثلاً:
Markdown heading -> hierarchy-aware
Prose -> recursive/semantic
Code fence -> code-aware
Table -> table-aware
این رویکرد در عمل از انتخاب یک strategy جهانی بسیار مقاومتر بود.
Overlap؛ بیشتر همیشه بهتر نیست
Overlap برای جلوگیری از مشکل boundary مفید است، اما هزینه دارد:
- storage بیشتر
- embedding بیشتر
- نتایج مشابه بیشتر
- context تکراری
بنابراین overlap باید سیاست مشخص داشته باشد. برای exact identifier query حتی ممکن است context expansion و overlap زیاد مطلوب نباشد؛ چون کاربر پاسخ بسیار دقیق میخواهد.
Parent/Child؛ راه جایگزین برای Chunkهای خیلی بزرگ
یکی از الگوهای مهم این است که retrieval روی chunk کوچک انجام شود ولی context از parent بزرگتر ساخته شود.
مثلاً:
Section Parent
├── Chunk A
├── Chunk B
└── Chunk C
Chunk B ممکن است بهترین hit باشد، اما مدل برای پاسخ به Section Parent یا Chunk A و C هم نیاز داشته باشد.
به این ترتیب لازم نیست embedding را روی Chunk بسیار بزرگ اجرا کنیم تا context کافی داشته باشیم.
Stable IDs و مشکل duplicate chunk
یک مسئله کمتر دیدهشده identity است.
اگر دو Chunk داخل یک source متن یکسان داشته باشند و ID صرفاً از hash متن ساخته شود، collision منطقی ایجاد میشود و ممکن است یکی جای دیگری را بگیرد.
در hardening ما deterministic IDها بهصورت duplicate-safe اصلاح شدند تا موقعیت/ساختار source نیز در identity نقش داشته باشد.
این موضوع نشان میدهد Chunking فقط text splitting نیست؛ lifecycle داده هم بخشی از آن است.
چگونه strategy مناسب را انتخاب کنیم؟
یک راهنمای عملی:
| نوع محتوا | Strategy پیشنهادی |
|---|---|
| متن عمومی | Recursive یا Semantic |
| Markdown | Markdown/Composite |
| کد Python | AST-aware Code |
| جدول | Table-aware |
| سند ترکیبی | Composite |
| متن طولانی بدون ساختار | Semantic + overlap |
| متن با محدودیت شدید مدل | Token-based |
اما این جدول حکم قطعی نیست. بهترین انتخاب باید با dataset واقعی اندازهگیری شود.
بهینهسازی Chunking با Evaluation
ما در ماژول Chunking Policy Optimizer اضافه کردیم تا strategyها و اندازههای مختلف را روی evaluation set مقایسه کنیم.
به جای اینکه بگوییم «Chunk 512 بهتر است»، باید سؤالهایی شبیه این داشته باشیم:
- Recall@K چه تغییری کرد؟
- Candidate Recall بهتر شد؟
- Citation Coverage چطور شد؟
- latency و index size چقدر تغییر کرد؟
- مرز Chunkها چقدر با ساختار مورد انتظار منطبقاند؟
Chunking باید مثل یک hyperparameter سیستم retrieval اندازهگیری شود.
جمعبندی
اگر فقط یک درس از تجربه ساخت این ماژول بخواهیم استخراج کنیم، این است:
کیفیت RAG از قبل از embedding شروع میشود.
Chunker تعیین میکند چه اطلاعاتی کنار هم قرار بگیرند، چه چیزی از هم جدا شود، چه metadataای حفظ شود و مدل retrieval در نهایت چه representationی از سند ببیند.
به همین دلیل ما به جای یک Chunker عمومی، مجموعهای از strategyها و یک Composite Router ساختیم.
در مقاله بعدی بررسی میکنیم چرا حتی پس از Chunking خوب، Vector Search به تنهایی کافی نیست و Hybrid Search چگونه این مشکل را حل میکند.
بهروزرسانی:
همهٔ نوشتهها